From ba045bc1c709f4a40a2286d62c2103d5a82b96d9 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 6 Mar 2023 10:31:50 +0100 Subject: [PATCH 001/200] Initial commit --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 000000000..9bf3320a1 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# binderhub-service \ No newline at end of file From 87cc2053cf8d6b42a18f50176cb23404098771ca Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 7 Mar 2023 14:17:29 +0100 Subject: [PATCH 002/200] Initial setup mash --- .github/dependabot.yaml | 16 ++ .github/workflows/release.yaml | 110 +++++++++++ .github/workflows/test-chart.yaml | 166 +++++++++++++++++ .github/workflows/test-docs.yaml | 37 ++++ .github/workflows/watch-dependencies.yaml | 52 ++++++ .gitignore | 174 ++++++++++++++++++ .pre-commit-config.yaml | 66 +++++++ .readthedocs.yaml | 17 ++ binderhub-service/.helmignore | 23 +++ binderhub-service/Chart.yaml | 24 +++ binderhub-service/templates/NOTES.txt | 71 +++++++ binderhub-service/templates/_helpers.tpl | 82 +++++++++ binderhub-service/templates/deployment.yaml | 53 ++++++ binderhub-service/templates/ingress.yaml | 41 +++++ binderhub-service/templates/service.yaml | 15 ++ .../templates/serviceaccount.yaml | 12 ++ binderhub-service/values.yaml | 82 +++++++++ chartpress.yaml | 23 +++ ci/publish | 42 +++++ ci/refreeze | 16 ++ dev-config.yaml | 0 dev-requirements.txt | 15 ++ docs/Makefile | 38 ++++ docs/README.md | 22 +++ docs/requirements.txt | 6 + docs/source/_static/images/logo/favicon.ico | Bin 0 -> 1439 bytes docs/source/_static/images/logo/logo.svg | 36 ++++ docs/source/conf.py | 85 +++++++++ docs/source/contributing.md | 1 + docs/source/explanation/architecture.md | 3 + docs/source/explanation/implementation.md | 11 ++ docs/source/index.md | 43 +++++ docs/source/reference/changelog.md | 9 + images/binderhub-service/Dockerfile | 68 +++++++ images/binderhub-service/README.md | 11 ++ images/binderhub-service/requirements.in | 6 + images/binderhub-service/requirements.txt | 140 ++++++++++++++ tools/generate-json-schema.py | 64 +++++++ tools/templates/lint-and-validate-values.yaml | 0 tools/templates/lint-and-validate.py | 114 ++++++++++++ tools/templates/yamllint-config.yaml | 8 + tools/validate-against-schema.py | 30 +++ 42 files changed, 1832 insertions(+) create mode 100644 .github/dependabot.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/test-chart.yaml create mode 100644 .github/workflows/test-docs.yaml create mode 100644 .github/workflows/watch-dependencies.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 binderhub-service/.helmignore create mode 100644 binderhub-service/Chart.yaml create mode 100644 binderhub-service/templates/NOTES.txt create mode 100644 binderhub-service/templates/_helpers.tpl create mode 100644 binderhub-service/templates/deployment.yaml create mode 100644 binderhub-service/templates/ingress.yaml create mode 100644 binderhub-service/templates/service.yaml create mode 100644 binderhub-service/templates/serviceaccount.yaml create mode 100644 binderhub-service/values.yaml create mode 100644 chartpress.yaml create mode 100755 ci/publish create mode 100755 ci/refreeze create mode 100644 dev-config.yaml create mode 100644 dev-requirements.txt create mode 100644 docs/Makefile create mode 100644 docs/README.md create mode 100644 docs/requirements.txt create mode 100644 docs/source/_static/images/logo/favicon.ico create mode 100644 docs/source/_static/images/logo/logo.svg create mode 100644 docs/source/conf.py create mode 100644 docs/source/contributing.md create mode 100644 docs/source/explanation/architecture.md create mode 100644 docs/source/explanation/implementation.md create mode 100644 docs/source/index.md create mode 100644 docs/source/reference/changelog.md create mode 100644 images/binderhub-service/Dockerfile create mode 100644 images/binderhub-service/README.md create mode 100644 images/binderhub-service/requirements.in create mode 100644 images/binderhub-service/requirements.txt create mode 100755 tools/generate-json-schema.py create mode 100644 tools/templates/lint-and-validate-values.yaml create mode 100755 tools/templates/lint-and-validate.py create mode 100644 tools/templates/yamllint-config.yaml create mode 100755 tools/validate-against-schema.py diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 000000000..48c8c6fb9 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,16 @@ +# dependabot.yaml reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +# +# Notes: +# - Status and logs from dependabot are provided at +# https://github.com/2i2c-org/binderhub-service/network/updates. +# - YAML anchors are not supported here or in GitHub Workflows. +# +version: 2 +updates: + # Maintain dependencies in our GitHub Workflows + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: monthly + time: "05:00" + timezone: "Etc/UTC" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 000000000..05cb33c00 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,110 @@ +# This is a GitHub workflow defining a set of jobs with a set of steps. +# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +# +name: Release + +on: + pull_request: + paths-ignore: + - "docs/**" + - "**.md" + - ".github/workflows/*" + - "!.github/workflows/release.yaml" + push: + paths-ignore: + - "docs/**" + - "**.md" + - ".github/workflows/*" + - "!.github/workflows/release.yaml" + branches-ignore: + - "dependabot/**" + - "pre-commit-ci-update-config" + - "update-*" + tags: + - "**" + +jobs: + # Builds and pushes docker images to quay.io and packages the Helm chart and + # publishes it at 2i2c-org/binderhub-service@gh-pages which is a Helm chart + # repository with a index.yaml file and packaged Helm charts. + # + # ref: https://2i2c.org/binderhub-service/index.yaml + # + release: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + # chartpress needs git history + fetch-depth: 0 + + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Decide to publish or not + id: publishing + shell: python + run: | + import os + repo = "${{ github.repository }}" + event = "${{ github.event_name }}" + ref = "${{ github.event.ref }}" + publishing = "" + if ( + repo == "2i2c-org/binderhub-service" + and event == "push" + and ( + ref.startswith("refs/tags/") + or ref == "refs/heads/main" + ) + ): + publishing = "true" + print("Publishing chart") + print(f"::set-output name=publishing::{publishing}") + + - name: Set up QEMU (for docker buildx) + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx (for chartpress multi-arch builds) + uses: docker/setup-buildx-action@v2 + + - name: Install chart publishing dependencies (chartpress, pyyaml, helm) + run: | + pip install chartpress pyyaml + pip list + + # helm is already installed + helm version + + - name: Generate values.schema.json from values.schema.yaml + run: ./tools/generate-json-schema.py + + # chartpress will make a commit when pushing to gh-pages, so we need to + # configure a git user. + - name: Configure a git user + run: | + git config --global user.email "github-actions@example.local" + git config --global user.name "GitHub Actions user" + + - name: Setup docker push rights to quay.io + if: steps.publishing.outputs.publishing + run: docker login -u "${{ secrets.DOCKER_USERNAME }}" -p "${{ secrets.DOCKER_PASSWORD }}" quay.io + + - name: Publish images and chart with chartpress + if: steps.publishing.outputs.publishing + run: ./ci/publish + env: + GITHUB_REPOSITORY: "${{ github.repository }}" + + - name: Package chart for actions/upload-artifact + if: steps.publishing.outputs.publishing == '' + run: helm package binderhub-service + + # ref: https://github.com/actions/upload-artifact + - uses: actions/upload-artifact@v3 + if: steps.publishing.outputs.publishing == '' + with: + name: binderhub-service-${{ github.sha }} + path: "binderhub-service-*.tgz" + if-no-files-found: error diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml new file mode 100644 index 000000000..0aba89206 --- /dev/null +++ b/.github/workflows/test-chart.yaml @@ -0,0 +1,166 @@ +# This is a GitHub workflow defining a set of jobs with a set of steps. +# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +# +name: Test chart + +on: + pull_request: + paths-ignore: + - "docs/**" + - "**.md" + - ".github/workflows/*" + - "!.github/workflows/test-chart.yaml" + push: + paths-ignore: + - "docs/**" + - "**.md" + - ".github/workflows/*" + - "!.github/workflows/test-chart.yaml" + branches-ignore: + - "dependabot/**" + - "pre-commit-ci-update-config" + - "update-*" + workflow_dispatch: + +jobs: + lint_and_validate_rendered_templates: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install dependencies + run: pip install chartpress yamllint + + - name: Lint and validate + run: tools/templates/lint-and-validate.py + + - name: Lint and validate (--strict, accept failure) + run: tools/templates/lint-and-validate.py --strict + continue-on-error: true + + lint_and_validate_templates_with_schema: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + # We run this job with the latest lowest helm version we support. + # + include: + - helm-version: "" # latest + - helm-version: v3.8.0 # minimal required version + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install helm ${{ matrix.helm-version }} + run: | + curl -sf https://raw.githubusercontent.com/helm/helm/HEAD/scripts/get-helm-3 | DESIRED_VERSION=${{ matrix.helm-version }} bash + + - name: Install dependencies + run: | + pip install pyyaml + + - name: Generate values.schema.json + run: tools/generate-json-schema.py + + - name: Helm lint (values.yaml) + run: helm lint ./binderhub-service + + - name: Helm lint (lint-and-validate-values.yaml) + run: helm lint ./binderhub-service --values tools/templates/lint-and-validate-values.yaml + + # FIXME: We can probably emit a GitHub workflow warning if these fail + # instead having them show as green without a warning or similar + # + # NOTE: --strict means that any warning is considered an error, and there + # are several warnings that we should ignore. + # + - name: Helm lint --strict (values.yaml) + run: helm lint --strict ./binderhub-service + continue-on-error: true + + - name: Helm lint --strict (lint-and-validate-values.yaml) + run: helm lint --strict ./binderhub-service + continue-on-error: true + + test: + runs-on: ubuntu-22.04 + timeout-minutes: 20 + + strategy: + fail-fast: false + matrix: + # We run this job multiple times with different parameterization + # specified below, these parameters have no meaning on their own and + # gain meaning on how job steps use them. + # + # k3s-version: https://github.com/rancher/k3s/tags + # k3s-channel: https://update.k3s.io/v1-release/channels + # + include: + - k3s-channel: latest + - k3s-channel: stable + + steps: + - uses: actions/checkout@v3 + with: + # chartpress needs git history + fetch-depth: 0 + + # Starts a k8s cluster with NetworkPolicy enforcement and installs both + # kubectl and helm + # + # ref: https://github.com/jupyterhub/action-k3s-helm/ + - uses: jupyterhub/action-k3s-helm@v3 + with: + k3s-channel: ${{ matrix.k3s-channel }} + metrics-enabled: false + traefik-enabled: false + docker-enabled: true + + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + pip install -r dev-requirements.txt + pip list + + # Build our images if needed and update Chart.yaml and values.yaml with + # version and tags + - run: chartpress + + - name: Generate values.schema.json from values.schema.yaml + run: tools/generate-json-schema.py + + # Validate rendered helm templates against the k8s api-server with the + # dedicated lint-and-validate-values.yaml config. + - name: "Helm template --validate (with lint and validate config)" + run: | + helm template --validate binderhub-service ./binderhub-service \ + --values tools/templates/lint-and-validate-values.yaml + + - name: Install local chart + run: | + helm upgrade --install binderhub-service ./binderhub-service \ + --values dev-config.yaml + + # ref: https://github.com/jupyterhub/action-k8s-await-workloads + - uses: jupyterhub/action-k8s-await-workloads@v2 + with: + timeout: 150 + max-restarts: 1 + + # ref: https://github.com/jupyterhub/action-k8s-namespace-report + - uses: jupyterhub/action-k8s-namespace-report@v1 + if: always() + with: + important-workloads: deploy/binderhub diff --git a/.github/workflows/test-docs.yaml b/.github/workflows/test-docs.yaml new file mode 100644 index 000000000..0a4f5d072 --- /dev/null +++ b/.github/workflows/test-docs.yaml @@ -0,0 +1,37 @@ +# This is a GitHub workflow defining a set of jobs with a set of steps. +# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +# +name: Test docs + +on: + pull_request: + paths: + - "docs/**" + - "**/schema.yaml" + - "**/test-docs.yaml" + push: + paths: + - "docs/**" + - "**/schema.yaml" + - "**/test-docs.yaml" + branches-ignore: + - "dependabot/**" + - "pre-commit-ci-update-config" + - "update-*" + workflow_dispatch: + +jobs: + linkcheck: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - run: pip install -r docs/requirements.txt + + - name: make linkcheck + run: | + cd docs + make linkcheck SPHINXOPTS='--color -W --keep-going' diff --git a/.github/workflows/watch-dependencies.yaml b/.github/workflows/watch-dependencies.yaml new file mode 100644 index 000000000..7bbab3bda --- /dev/null +++ b/.github/workflows/watch-dependencies.yaml @@ -0,0 +1,52 @@ +# This is a GitHub workflow defining a set of jobs with a set of steps. +# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +# +# This Workflow watches dependencies and automatically creates PRs to update +# them. +# +# - Refreeze images/*/requirements.txt based on images/*/requirements.in +# +name: Watch dependencies + +on: + push: + paths: + - "images/*/requirements.in" + - ".github/workflows/watch-dependencies.yaml" + branches: ["main"] + schedule: + # Run at 05:00 on day-of-month 1, ref: https://crontab.guru/#0_5_1_*_* + - cron: "0 5 1 * *" + workflow_dispatch: + +jobs: + refreeze-dockerfile-requirements-txt: + if: github.repository == '2i2c-org/binderhub-service' + runs-on: ubuntu-22.04 + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v3 + + - name: Refreeze requirements.txt based on requirements.in + run: ci/refreeze + + - name: git diff + run: git --no-pager diff --color=always + + # ref: https://github.com/peter-evans/create-pull-request + - uses: peter-evans/create-pull-request@v4 + with: + branch: update-image-requirements + labels: dependencies + commit-message: "binderhub-service image: refreeze requirements.txt" + title: "binderhub-service image: refreeze requirements.txt" + body: >- + The binderhub-service image's requirements.txt has been refrozen + based on requirements.in. + + The push to this branch was made by a bot account so all tests + aren't triggered to run. Close and re-open this PR to trigger them + manually. diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..04acaba4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,174 @@ +### Repository specific files that are generated + +binderhub-service/values.schema.json +tools/templates/rendered-templates/ + + +### Other misc things + +.vscode +*.DS_Store + + +### Python .gitignore from https://github.com/github/gitignore/blob/HEAD/Python.gitignore +# +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..234e861d3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,66 @@ +# pre-commit is a tool to perform a predefined set of tasks manually and/or +# automatically before git commits are made. +# +# Config reference: https://pre-commit.com/#pre-commit-configyaml---top-level +# +# Common tasks +# +# - Run on all files: pre-commit run --all-files +# - Register git hooks: pre-commit install --install-hooks +# +repos: + # Autoformat: Python code, syntax patterns are modernized + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + args: + - --py310-plus + + # Autoformat: Python code + - repo: https://github.com/PyCQA/autoflake + rev: v2.0.1 + hooks: + - id: autoflake + args: + - --in-place + + # Autoformat: Python code + - repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + args: + - --target-version=py310 + - --target-version=py311 + + # Autoformat: Python code + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: + - --profile=black + + # Autoformat: markdown, yaml (but not helm templates) + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.0.0-alpha.6 + hooks: + - id: prettier + + # Linting: Python code (see the file .flake8) + - repo: https://github.com/PyCQA/flake8 + rev: "6.0.0" + hooks: + - id: flake8 + # Ignore style and complexity + # E: style errors + # W: style warnings + # C: complexity + # + args: + - --ignore=E,C,W + +# pre-commit.ci config reference: https://pre-commit.ci/#configuration +ci: + autoupdate_schedule: monthly diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..f92c2a20f --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# Configuration on how ReadTheDocs (RTD) builds our documentation +# ref: https://readthedocs.org/projects/binderhub-service/ +# ref: https://docs.readthedocs.io/en/stable/config-file/v2.html +# +version: 2 + +sphinx: + configuration: docs/source/conf.py + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +python: + install: + - requirements: docs/requirements.txt diff --git a/binderhub-service/.helmignore b/binderhub-service/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/binderhub-service/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/binderhub-service/Chart.yaml b/binderhub-service/Chart.yaml new file mode 100644 index 000000000..a3251f86f --- /dev/null +++ b/binderhub-service/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: binderhub-service +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/binderhub-service/templates/NOTES.txt b/binderhub-service/templates/NOTES.txt new file mode 100644 index 000000000..876e2f7a9 --- /dev/null +++ b/binderhub-service/templates/NOTES.txt @@ -0,0 +1,71 @@ +{{- /* Generated with https://patorjk.com/software/taag/#p=display&h=0&f=Slant&t=BinderHub */}} +. ____ _ __ __ __ __ + / __ ) (_) ____ ____/ / ___ _____ / / / / __ __ / /_ + / __ | / / / __ \ / __ / / _ \ / ___/ / /_/ / / / / / / __ \ + / /_/ / / / / / / / / /_/ / / __/ / / / __ / / /_/ / / /_/ / +/_____/ /_/ /_/ /_/ \__,_/ \___/ /_/ /_/ /_/ \__,_/ /_.___/ + _____ _ + / ___/ ___ _____ _ __ (_) _____ ___ + \__ \ / _ \ / ___/ | | / / / / / ___/ / _ \ + ___/ / / __/ / / | |/ / / / / /__ / __/ + /____/ \___/ /_/ |___/ /_/ \___/ \___/ + +You have successfully installed the BinderHub Service Helm chart! + +### Installation info + + - Kubernetes namespace: {{ .Release.Namespace }} + - Helm release name: {{ .Release.Name }} + - Helm chart version: {{ .Chart.Version }} + - BinderHub version: {{ .Chart.AppVersion }} + - Python packages: See https://github.com/2i2c-org/binderhub-service/blob/{{ include "binderhub-service.chart-version-to-git-ref" .Chart.Version }}/images/binderhub-service/requirements.txt + +### Followup links + + - Documentation: https://2i2c.org/binderhub-service + - Issue tracking: https://github.com/2i2c-org/binderhub-service/issues + +### Post-installation checklist + + - Verify that created Pods enter a Running state: + + kubectl --namespace={{ .Release.Namespace }} get pod + + If a pod is stuck with a Pending or ContainerCreating status, diagnose with: + + kubectl --namespace={{ .Release.Namespace }} describe pod + + If a pod keeps restarting, diagnose with: + + kubectl --namespace={{ .Release.Namespace }} logs --previous + {{- println }} + + + +{{- /* + Breaking changes. +*/}} + +{{- $breaking := "" }} +{{- $breaking_title := "\n" }} +{{- $breaking_title = print $breaking_title "\n#################################################################################" }} +{{- $breaking_title = print $breaking_title "\n###### BREAKING: The config values passed contained no longer accepted #####" }} +{{- $breaking_title = print $breaking_title "\n###### options. See the messages below for more details. #####" }} +{{- $breaking_title = print $breaking_title "\n###### #####" }} +{{- $breaking_title = print $breaking_title "\n###### To verify your updated config is accepted, you can use #####" }} +{{- $breaking_title = print $breaking_title "\n###### the `helm template` command. #####" }} +{{- $breaking_title = print $breaking_title "\n#################################################################################" }} + + +{{- /* + This is an example (in a helm template comment) on how to detect and + communicate with regards to a breaking chart config change. + + {{- if hasKey .Values.singleuser.cloudMetadata "enabled" }} + {{- $breaking = print $breaking "\n\nCHANGED: singleuser.cloudMetadata.enabled must as of 1.0.0 be configured using singleuser.cloudMetadata.blockWithIptables with the opposite value." }} + {{- end }} +*/}} + +{{- if $breaking }} +{{- fail (print $breaking_title $breaking "\n\n") }} +{{- end }} diff --git a/binderhub-service/templates/_helpers.tpl b/binderhub-service/templates/_helpers.tpl new file mode 100644 index 000000000..c53465918 --- /dev/null +++ b/binderhub-service/templates/_helpers.tpl @@ -0,0 +1,82 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "binderhub-service.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "binderhub-service.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "binderhub-service.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "binderhub-service.labels" -}} +helm.sh/chart: {{ include "binderhub-service.chart" . }} +{{ include "binderhub-service.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "binderhub-service.selectorLabels" -}} +app.kubernetes.io/name: {{ include "binderhub-service.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "binderhub-service.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "binderhub-service.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + + + +{{- /* + binderhub-service.chart-version-to-git-ref: + Renders a valid git reference from a chartpress generated version string. + In practice, either a git tag or a git commit hash will be returned. + + - The version string will follow a chartpress pattern, see + https://github.com/jupyterhub/chartpress#examples-chart-versions-and-image-tags. + + - The regexReplaceAll function is a sprig library function, see + https://masterminds.github.io/sprig/strings.html. + + - The regular expression is in golang syntax, but \d had to become \\d for + example. +*/}} +{{- define "binderhub-service.chart-version-to-git-ref" -}} +{{- regexReplaceAll ".*[.-]n\\d+[.]h(.*)" . "${1}" }} +{{- end }} diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml new file mode 100644 index 000000000..98b50517b --- /dev/null +++ b/binderhub-service/templates/deployment.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "binderhub-service.fullname" . }} + labels: + {{- include "binderhub-service.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "binderhub-service.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- . | toYaml | nindent 8 }} + {{- end }} + labels: + {{- include "binderhub-service.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- . | toYaml | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "binderhub-service.serviceAccountName" . }} + securityContext: + {{- .Values.podSecurityContext | toYaml | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- .Values.securityContext | toYaml | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + {{- with .Values.image.pullPolicy }} + imagePullPolicy: {{ . }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + resources: + {{- .Values.resources | toYaml | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- . | toYaml | nindent 8 }} + {{- end }} diff --git a/binderhub-service/templates/ingress.yaml b/binderhub-service/templates/ingress.yaml new file mode 100644 index 000000000..fbab7374c --- /dev/null +++ b/binderhub-service/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "binderhub-service.fullname" . }} + labels: + {{- include "binderhub-service.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- . | toYaml | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- range . }} + - hosts: + {{- .hosts | toYaml | nindent 8 }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "binderhub-service.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/binderhub-service/templates/service.yaml b/binderhub-service/templates/service.yaml new file mode 100644 index 000000000..15a6b6895 --- /dev/null +++ b/binderhub-service/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "binderhub-service.fullname" . }} + labels: + {{- include "binderhub-service.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "binderhub-service.selectorLabels" . | nindent 4 }} diff --git a/binderhub-service/templates/serviceaccount.yaml b/binderhub-service/templates/serviceaccount.yaml new file mode 100644 index 000000000..ef917ccf9 --- /dev/null +++ b/binderhub-service/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "binderhub-service.serviceAccountName" . }} + labels: + {{- include "binderhub-service.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- . | toYaml | nindent 4 }} + {{- end }} +{{- end }} diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml new file mode 100644 index 000000000..a84bb5d08 --- /dev/null +++ b/binderhub-service/values.yaml @@ -0,0 +1,82 @@ +# Default values for binderhub-service. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/chartpress.yaml b/chartpress.yaml new file mode 100644 index 000000000..053831740 --- /dev/null +++ b/chartpress.yaml @@ -0,0 +1,23 @@ +# This is the configuration for chartpress, a CLI for Helm chart management. +# +# chartpress is used to: +# - Build images +# - Update Chart.yaml (version) and values.yaml (image tags) +# - Package and publish Helm charts to a GitHub based Helm chart repository +# +# For more information, see the projects README.md file: +# https://github.com/jupyterhub/chartpress +# +charts: + - name: binderhub-service + imagePrefix: quay.io/2i2c/ + baseVersion: "0.1.0-0.dev" + repo: + git: 2i2c-org/binderhub-service + published: https://2i2c.org/binderhub-service + + images: + # binderhub-service, the container where the binderhub the Python + # application is running. + binderhub-service: + valuesPath: binderhub.image diff --git a/ci/publish b/ci/publish new file mode 100755 index 000000000..ba9678234 --- /dev/null +++ b/ci/publish @@ -0,0 +1,42 @@ +#!/bin/bash +# This script publishes the Helm chart to the GitHub Pages based Helm chart repo +# in and pushes associated built docker images to Docker hub using chartpress. +# -------------------------------------------------------------------------- + +# Exit on errors, assert env vars, log commands +set -eux + +PUBLISH_ARGS="--push --publish-chart \ + --builder docker-buildx \ + --platform linux/amd64 --platform linux/arm64 \ +" + +# chartpress use git to push to our Helm chart repository, which is the gh-pages +# branch of 2i2c-org/binderhub-service. +if [[ $GITHUB_REF != refs/tags/* ]]; then + # Using --extra-message, we help readers of merged PRs to know what version + # they need to bump to in order to make use of the PR. This is enabled by a + # GitHub notificaiton in the PR like "Github Action user pushed a commit to + # 2i2c-org/binderhub-service that referenced this pull request..." + # + # ref: https://github.com/jupyterhub/chartpress#usage + # + # NOTE: GitHub merge commits contain a PR reference like #123. `sed` looks + # to extract either a PR reference like #123 or fall back to create a + # commit hash reference like @123abcd. Combined with GITHUB_REPOSITORY + # we craft a commit message like 2i2c-org/binderhub-service#123 or + # 2i2c-org/binderhub-service@123abcd which will be understood as a + # reference by GitHub. + PR_OR_HASH=$(git log -1 --pretty=%h-%B | head -n1 | sed 's/^.*\(#[0-9]*\).*/\1/' | sed 's/^\([0-9a-f]*\)-.*/@\1/') + LATEST_COMMIT_TITLE=$(git log -1 --pretty=%B | head -n1) + EXTRA_MESSAGE="${GITHUB_REPOSITORY}${PR_OR_HASH} ${LATEST_COMMIT_TITLE}" + chartpress $PUBLISH_ARGS --extra-message "${EXTRA_MESSAGE}" +else + # Setting a tag explicitly enforces a rebuild if this tag had already been + # built and we wanted to override it. + chartpress $PUBLISH_ARGS --tag "${GITHUB_REF:10}" +fi + +# Let us log the changes chartpress did, it should include replacements for +# fields in values.yaml, such as what tag for various images we are using. +git --no-pager diff --color=always diff --git a/ci/refreeze b/ci/refreeze new file mode 100755 index 000000000..a3142cb5b --- /dev/null +++ b/ci/refreeze @@ -0,0 +1,16 @@ +#!/bin/bash +set -xeuo pipefail + +# Because `pip-compile` resolves `requirements.txt` with the current Python for +# the current platform, it should be run on the same Python version and platform +# as our Dockerfile as done by this script. + +cd images/binderhub-service +docker run \ + --rm \ + --env=CUSTOM_COMPILE_COMMAND='Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml' \ + --volume="$PWD:/io" \ + --workdir=/io \ + --user=root \ + python:3.11-bullseye \ + sh -c 'pip install pip-tools==6.* && pip-compile --resolver=backtracking --upgrade' diff --git a/dev-config.yaml b/dev-config.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 000000000..423bcad36 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,15 @@ +# chartpress is important for local development, CI and CD +# - builds images and can push them also (--push) +# - updates image names and tags in values.yaml +# - can publish the built Helm chart (--publish) +# +# ref: https://github.com/jupyterhub/chartpress +# +chartpress + +# pytest run tests that require requests and pyyaml +pytest +pyyaml + +# tbump is making releases +tbump diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..c1a96d05b --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,38 @@ +# Makefile for Sphinx documentation generated by sphinx-quickstart +# ---------------------------------------------------------------------------- + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) + + +# Manually added commands +# ---------------------------------------------------------------------------- + +# For local development: +# - builds and rebuilds html on changes to source +# - starts a livereload enabled webserver and opens up a browser +devenv: + sphinx-autobuild -b html --open-browser "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) + +# For local development and CI: +# - verifies that links are valid +linkcheck: + $(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)/linkcheck" $(SPHINXOPTS) + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..e55a017bb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +# About the documentation + +This documentation is automatically built on each commit [as configured on +ReadTheDocs](https://readthedocs.org/projects/binderhub-service/) and in the +`.readthedocs.yaml` file, and made available on +[binderhub-service.readthedocs.io](https://binderhub-service.readthedocs.io/en/latest/). + +The documentation is meant to be structured according to the Diataxis framework +as documented in https://diataxis.fr/. + +## Local documentation development + +```shell +cd docs +pip install -r requirements.txt + +# automatic build and livereload enabled web-server +make devenv + +# automatic check of links validity +make linkcheck +``` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..63f4be100 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,6 @@ +myst-parser +sphinx-autobuild +sphinx-book-theme +sphinx-copybutton +sphinxext-opengraph +sphinxext-rediraffe diff --git a/docs/source/_static/images/logo/favicon.ico b/docs/source/_static/images/logo/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..09ca4b304904a4d332056c2e5a360f88565d6a1d GIT binary patch literal 1439 zcmV;Q1z`G#P)ipxmy2As|&~DTBk}mdgftFm8Bj%@_uPmVvRSdddvE5P_UjJceiBV`yHl zb%OehD*BECLr&yi00_F>S>_R(L>4CFGGm~}D>BU1pntj-+bplx^Wz*4G`L^J)u*`Q zXBGjD4h$W7TgFJx!%Ki_3P2~KW=3{Sa6(z#!3fZ_>|Nw&%!B#$#e&^hz3COdd##oJ z+VZDIL&q0}9ov`#007$(1sO^0edLme_y2w4_WS0rSYI9#CtQ-;n`C5-6NVX9qJXPSLvu(x`r2UnGOm1@iy5?NUREsT-4!ka zjZ0XuIEexvvYv?TGOJ||9j{|honRK`R0j}Fu$~WdW^^S=fy+k#P!YDH3~+{kOcD@I zHM4;?A-O)rY%kYGPdPA|)3Tg@Mf6*9hg0I`g?q*Pz`h;#ZeIUKl-_uA>FQ=ZbuiZ8 z=Bm_r2~GXO(OjT@ZSS|RIV8@nKQ2DoI3h$JTp021cu|a`-d}r2+!k9FkM&&?PbR(= z1LC^(kz9~6R7`SaHvO&xE3p8n^DHufAU;@sI8^XMP|qem66yG5tr~z)Flev8eb#Dh z4p*eBBXkTnRFbZRqA_2pxUU=zt5Hwz$ zfw2QsXA(@5wIwk@b`VP{>2qYMAs%ct#PHf```SroD;_u=EXgKF1FiK^FN5!svjOA*V|O+#8BJp9Y#+T zb)`@7!h5;9t}V-SUIiRW6INYWoqLB;lpf=jHFetScl6>bxC7l>o`AGd4LI{elKec9yG#Y#pxQJDPLD2U4qv zWc%8kD}RV58t#sRF0nN20zc1qMY0GJ0Jx&#&DtHALI2yxi@&F!g>ktBKOn?T6TO#& zbxW3ZuyF*kQgpL#sE=I0WqipEn=sX?4Z}D9vS+0Sp&o0>;XTh%B(2 z!7T@Ef>^3(f+)~o*XgHSoeBMYhTO7!Y3TNRvbAwTmF2gAz3%bGFIlP+=nB?Iycx$7 zu6UVV?57Gw&mFwq6=y;fL5){uf*QAVN+DQbt25}fBLf42v(*`$e1w1)ug+)(Kt(=M zrHOJtl+_s$qvjoe&t!^1{f`WXR=4I+;bS7$*XA_oHtpmrEIYO$L97(*m ty8@;I*hxA|V?F>-?mIyaIcNV0FaUW~7g#;piZK8H002ovPDHLkV1iOPkh=f? literal 0 HcmV?d00001 diff --git a/docs/source/_static/images/logo/logo.svg b/docs/source/_static/images/logo/logo.svg new file mode 100644 index 000000000..43fe1ff5a --- /dev/null +++ b/docs/source/_static/images/logo/logo.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 000000000..eae21d133 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,85 @@ +# Configuration file for Sphinx to build our documentation to HTML. +# +# Configuration reference: https://www.sphinx-doc.org/en/master/usage/configuration.html +# +import datetime + +# -- Project information ----------------------------------------------------- +# ref: https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +# +project = "binderhub-service" +copyright = f"{datetime.date.today().year}, 2i2c.org" +author = "2i2c.org" + + +# -- General Sphinx configuration --------------------------------------------------- +# ref: https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration +# +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# +extensions = [ + "myst_parser", + "sphinx_copybutton", + "sphinxext.opengraph", + "sphinxext.rediraffe", +] +root_doc = "index" +source_suffix = [".md"] + + +# -- Options for HTML output ------------------------------------------------- +# ref: https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output +# +html_logo = "_static/images/logo/logo.png" +html_favicon = "_static/images/logo/favicon.ico" +html_static_path = ["_static"] + +# sphinx_book_theme reference: https://sphinx-book-theme.readthedocs.io/en/latest/?badge=latest +html_theme = "sphinx_book_theme" +html_theme_options = { + "home_page_in_toc": True, + "repository_url": "https://github.com/2i2c-org/binderhub-service/", + "repository_branch": "main", + "path_to_docs": "docs/source", + "use_download_button": False, + "use_edit_page_button": True, + "use_issues_button": True, + "use_repository_button": True, +} + + +# -- Options for linkcheck builder ------------------------------------------- +# ref: https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-the-linkcheck-builder +# +linkcheck_ignore = [ + r"(.*)github\.com(.*)#", # javascript based anchors + r"(.*)/#%21(.*)/(.*)", # /#!forum/jupyter - encoded anchor edge case + r"https://github.com/[^/]*$", # too many github usernames / searches in changelog + "https://github.com/2i2c-org/binderhub-service/pull/", # too many PRs in changelog + "https://github.com/2i2c-org/binderhub-service/compare/", # too many comparisons in changelog +] +linkcheck_anchors_ignore = [ + "/#!", + "/#%21", +] + + +# -- Options for the opengraph extension ------------------------------------- +# ref: https://github.com/wpilibsuite/sphinxext-opengraph#options +# +# ogp_site_url is set automatically by RTD +ogp_image = "_static/logo.svg" +ogp_use_first_image = True + + +# -- Options for the rediraffe extension ------------------------------------- +# ref: https://github.com/wpilibsuite/sphinxext-rediraffe#readme +# +# This extensions help us relocated content without breaking links. If a +# document is moved internally, we should configure a redirect like below. +# +rediraffe_branch = "main" +rediraffe_redirects = { + # "old-file": "new-folder/new-file-name", +} diff --git a/docs/source/contributing.md b/docs/source/contributing.md new file mode 100644 index 000000000..340edab54 --- /dev/null +++ b/docs/source/contributing.md @@ -0,0 +1 @@ +## Contributing diff --git a/docs/source/explanation/architecture.md b/docs/source/explanation/architecture.md new file mode 100644 index 000000000..54ebbdd22 --- /dev/null +++ b/docs/source/explanation/architecture.md @@ -0,0 +1,3 @@ +# Architecture + +## Goals diff --git a/docs/source/explanation/implementation.md b/docs/source/explanation/implementation.md new file mode 100644 index 000000000..cb77e3c65 --- /dev/null +++ b/docs/source/explanation/implementation.md @@ -0,0 +1,11 @@ +# Implementation + +## Technical stack + +[jupyterhub]: https://jupyterhub.readthedocs.io/en/stable/ +[jupyterhub rbac]: https://jupyterhub.readthedocs.io/en/stable/rbac/index.html +[readthedocs]: https://readthedocs.org/ +[sphinx]: https://www.sphinx-doc.org/en/master/ +[sphinx-book-theme]: https://sphinx-book-theme.readthedocs.io/en/stable/ +[myst-parser]: https://myst-parser.readthedocs.io/en/stable/ +[github actions]: https://github.com/features/actions diff --git a/docs/source/index.md b/docs/source/index.md new file mode 100644 index 000000000..c8b5709d5 --- /dev/null +++ b/docs/source/index.md @@ -0,0 +1,43 @@ +# binderhub-service + +Write description here... + +## Tutorials + +**TODO** + +```{toctree} +:maxdepth: 2 +``` + +## How-to guides + +**TODO** + +- making some kind of change of relevance + +```{toctree} +:maxdepth: 2 +``` + +## Reference + +**TODO** + +- Changelog + +```{toctree} +:maxdepth: 2 +reference/changelog +``` + +## Explanation + +**TODO** + +- Architecture +- Implementation + +```{toctree} +:maxdepth: 2 +``` diff --git a/docs/source/reference/changelog.md b/docs/source/reference/changelog.md new file mode 100644 index 000000000..c08020f4d --- /dev/null +++ b/docs/source/reference/changelog.md @@ -0,0 +1,9 @@ +(changelog)= + +# Changelog + +No releases made yet + +## 0.1.0 + +Test diff --git a/images/binderhub-service/Dockerfile b/images/binderhub-service/Dockerfile new file mode 100644 index 000000000..646a65715 --- /dev/null +++ b/images/binderhub-service/Dockerfile @@ -0,0 +1,68 @@ +# syntax = docker/dockerfile:1.3 + + +# The build stage +# --------------- +# This stage is building Python wheels for use in later stages by using a base +# image that has more pre-requisites to do so, such as a C++ compiler. +# +# NOTE: If the image version is updated, also update it in ci/refreeze! +# +FROM python:3.11-bullseye as build-stage + +# install node as required to build binderhub from source +RUN echo "deb https://deb.nodesource.com/node_16.x bullseye main" > /etc/apt/sources.list.d/nodesource.list \ + && curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - +RUN apt-get update \ + && apt-get install --yes \ + nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Build wheels +# +# We set pip's cache directory and expose it across build stages via an +# ephemeral docker cache (--mount=type=cache,target=${PIP_CACHE_DIR}). We use +# the same technique for the directory /tmp/wheels. +# +COPY requirements.txt requirements.txt +ARG PIP_CACHE_DIR=/tmp/pip-cache +RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ + pip install build \ + && pip wheel \ + --wheel-dir=/tmp/wheels \ + -r requirements.txt + + +# The final stage +# --------------- +# This stage is built and published as quay.io/2i2c/binderhub-service. +# +FROM python:3.11-slim-bullseye + +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get upgrade --yes \ + && apt-get install --yes \ + git \ + # required by binderhub + libcurl4 \ + # required by pycurl, an optional binderhub dependency + tini \ + # tini is used as an entrypoint to not loose track of SIGTERM + # signals as sent before SIGKILL, for example when "docker stop" + # or "kubectl delete pod" is run. By doing that the pod can + # terminate very quickly. + && rm -rf /var/lib/apt/lists/* + +# install wheels built in the build stage +COPY requirements.txt requirements.txt +ARG PIP_CACHE_DIR=/tmp/pip-cache +RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ + --mount=type=cache,from=build-stage,source=/tmp/wheels,target=/tmp/wheels \ + pip install --find-links=/tmp/wheels/ \ + -r requirements.txt + +ENV PYTHONUNBUFFERED=1 +EXPOSE 8585 +ENTRYPOINT ["tini", "--", "python", "-m", "binderhub"] +CMD ["--config", "/etc/binderhub/config/binderhub_config.py"] diff --git a/images/binderhub-service/README.md b/images/binderhub-service/README.md new file mode 100644 index 000000000..a830d0b59 --- /dev/null +++ b/images/binderhub-service/README.md @@ -0,0 +1,11 @@ +# About this folder + +The Dockerfile in this folder is built by +[chartpress](https://github.com/jupyterhub/chartpress#readme), using the +requirements.txt file. The requirements.txt file is updated based on the +requirements.in file using [`pip-compile`](https://pip-tools.readthedocs.io). + +## How to update requirements.txt + +Use the "Run workflow" button at +https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml. diff --git a/images/binderhub-service/requirements.in b/images/binderhub-service/requirements.in new file mode 100644 index 000000000..4669728bf --- /dev/null +++ b/images/binderhub-service/requirements.in @@ -0,0 +1,6 @@ +# This file is the input to requirements.txt, which is a frozen version of this. +# To update requirements.txt, use the "Run workflow" button at +# https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml + +binderhub +pycurl diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt new file mode 100644 index 000000000..40e2f446c --- /dev/null +++ b/images/binderhub-service/requirements.txt @@ -0,0 +1,140 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml +# +alembic==1.10.0 + # via jupyterhub +async-generator==1.10 + # via jupyterhub +attrs==22.2.0 + # via jsonschema +binderhub==0.1.0 + # via -r requirements.in +cachetools==5.3.0 + # via google-auth +certifi==2022.12.7 + # via + # kubernetes + # requests +certipy==0.1.3 + # via jupyterhub +cffi==1.15.1 + # via cryptography +charset-normalizer==3.1.0 + # via requests +cryptography==39.0.2 + # via pyopenssl +docker==6.0.1 + # via binderhub +escapism==1.0.1 + # via binderhub +google-auth==2.16.2 + # via kubernetes +greenlet==2.0.2 + # via sqlalchemy +idna==3.4 + # via requests +jinja2==3.1.2 + # via + # binderhub + # jupyterhub +jsonschema==4.17.3 + # via jupyter-telemetry +jupyter-telemetry==0.1.0 + # via jupyterhub +jupyterhub==3.1.1 + # via binderhub +kubernetes==26.1.0 + # via binderhub +mako==1.2.4 + # via alembic +markupsafe==2.1.2 + # via + # jinja2 + # mako +oauthlib==3.2.2 + # via + # jupyterhub + # requests-oauthlib +packaging==23.0 + # via + # docker + # jupyterhub +pamela==1.0.0 + # via jupyterhub +prometheus-client==0.16.0 + # via + # binderhub + # jupyterhub +pyasn1==0.4.8 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.2.8 + # via google-auth +pycparser==2.21 + # via cffi +pycurl==7.45.2 + # via -r requirements.in +pyopenssl==23.0.0 + # via certipy +pyrsistent==0.19.3 + # via jsonschema +python-dateutil==2.8.2 + # via + # jupyterhub + # kubernetes +python-json-logger==2.0.7 + # via + # binderhub + # jupyter-telemetry +pyyaml==6.0 + # via kubernetes +requests==2.28.2 + # via + # docker + # jupyterhub + # kubernetes + # requests-oauthlib +requests-oauthlib==1.3.1 + # via kubernetes +rsa==4.9 + # via google-auth +ruamel-yaml==0.17.21 + # via jupyter-telemetry +six==1.16.0 + # via + # google-auth + # kubernetes + # python-dateutil +sqlalchemy==2.0.5.post1 + # via + # alembic + # jupyterhub +tornado==6.2 + # via + # binderhub + # jupyterhub +traitlets==5.9.0 + # via + # binderhub + # jupyter-telemetry + # jupyterhub +typing-extensions==4.5.0 + # via + # alembic + # sqlalchemy +urllib3==1.26.14 + # via + # docker + # kubernetes + # requests +websocket-client==1.5.1 + # via + # docker + # kubernetes + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/tools/generate-json-schema.py b/tools/generate-json-schema.py new file mode 100755 index 000000000..bf1116609 --- /dev/null +++ b/tools/generate-json-schema.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" +This script reads values.schema.yaml and generates a values.schema.json that we +can package with the Helm chart, allowing helm the CLI perform validation. + +While we can directly generate a values.schema.json from values.schema.yaml, it +contains a lot of description text we use to generate our configuration +reference that isn't helpful to ship along the validation schema. Due to that, +we trim away everything that isn't needed. +""" + +import json +import os +from collections.abc import MutableMapping + +import yaml + +here_dir = os.path.abspath(os.path.dirname(__file__)) +schema_yaml = os.path.join(here_dir, os.pardir, "binderhub-service", "values.schema.yaml") +values_schema_json = os.path.join( + here_dir, os.pardir, "binderhub-service", "values.schema.json" +) + + +def clean_jsonschema(d, parent_key=""): + """ + Modifies a dictionary representing a jsonschema in place to not contain + jsonschema keys not relevant for a values.schema.json file solely for use by + the helm CLI. + """ + JSONSCHEMA_KEYS_TO_REMOVE = {"description"} + + # start by cleaning up the current level + for k in set.intersection(JSONSCHEMA_KEYS_TO_REMOVE, set(d.keys())): + del d[k] + + # Recursively cleanup nested levels, bypassing one level where there could + # be a valid Helm chart configuration named just like the jsonschema + # specific key to remove. + if "properties" in d: + for k, v in d["properties"].items(): + if isinstance(v, MutableMapping): + clean_jsonschema(v, k) + + +def run(): + # Using these sets, we can validate further manually by printing the results + # of set operations. + with open(schema_yaml) as f: + schema = yaml.safe_load(f) + + # Drop what isn't relevant for a values.schema.json file packaged with the + # Helm chart, such as the description keys only relevant for our + # configuration reference. + clean_jsonschema(schema) + + # dump schema to values.schema.json + with open(values_schema_json, "w") as f: + json.dump(schema, f) + + print("binderhub-service/values.schema.json created") + + +run() diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/tools/templates/lint-and-validate.py b/tools/templates/lint-and-validate.py new file mode 100755 index 000000000..05b1b2fbb --- /dev/null +++ b/tools/templates/lint-and-validate.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Lints and validates the chart's template files and their rendered output without +any cluster interaction. For this script to function, you must install yamllint. + +USAGE: + + tools/templates/lint-and-validate.py + +DEPENDENCIES: + +yamllint: https://github.com/adrienverge/yamllint + + pip install yamllint +""" + +import argparse +import os +import pipes +import subprocess +import sys + +os.chdir(os.path.dirname(sys.argv[0])) + + +def check_call(cmd, **kwargs): + """Run a subcommand and exit if it fails""" + try: + subprocess.check_call(cmd, **kwargs) + except subprocess.CalledProcessError as e: + print( + "`{}` exited with status {}".format( + " ".join(map(pipes.quote, cmd)), + e.returncode, + ), + file=sys.stderr, + ) + sys.exit(e.returncode) + + +def lint(yamllint_config, values, output_dir, strict, debug): + """Calls `helm lint`, `helm template`, and `yamllint`.""" + + print("### Clearing output directory") + check_call(["mkdir", "-p", output_dir]) + check_call(["rm", "-rf", f"{output_dir}/*"]) + + print("### Linting started") + print("### 1/3 - helm lint: lint helm templates") + helm_lint_cmd = ["helm", "lint", "../../jupyterhub", f"--values={values}"] + if strict: + helm_lint_cmd.append("--strict") + if debug: + helm_lint_cmd.append("--debug") + check_call(helm_lint_cmd) + + print("### 2/3 - helm template: generate kubernetes resources") + helm_template_cmd = [ + "helm", + "template", + "../../jupyterhub", + f"--values={values}", + f"--output-dir={output_dir}", + ] + if debug: + helm_template_cmd.append("--debug") + check_call(helm_template_cmd) + + print("### 3/3 - yamllint: yaml lint generated kubernetes resources") + check_call(["yamllint", "-c", yamllint_config, output_dir]) + + print() + print( + "### Linting and validation of helm templates and generated kubernetes resources OK!" + ) + + +if __name__ == "__main__": + argparser = argparse.ArgumentParser() + argparser.add_argument( + "--debug", + action="store_true", + help="Run helm lint and helm template with the --debug flag", + ) + argparser.add_argument( + "--strict", + action="store_true", + help="Run helm lint with the --strict flag", + ) + argparser.add_argument( + "--values", + default="lint-and-validate-values.yaml", + help="Specify Helm values in a YAML file (can specify multiple)", + ) + argparser.add_argument( + "--output-dir", + default="rendered-templates", + help="Output directory for the rendered templates. Warning: content in this will be wiped.", + ) + argparser.add_argument( + "--yamllint-config", + default="yamllint-config.yaml", + help="Specify a yamllint config", + ) + + args = argparser.parse_args() + + lint( + args.yamllint_config, + args.values, + args.output_dir, + args.strict, + args.debug, + ) diff --git a/tools/templates/yamllint-config.yaml b/tools/templates/yamllint-config.yaml new file mode 100644 index 000000000..ade69db12 --- /dev/null +++ b/tools/templates/yamllint-config.yaml @@ -0,0 +1,8 @@ +rules: + indentation: + spaces: 2 # Default: consistent + indent-sequences: whatever # Default true (*) + line-length: disable # Default: { max: 80, ... } + +# (*) toYaml's emitted sequences/lists will have no indentation and we have no +# control over it, so we compromise by setting "whatever". diff --git a/tools/validate-against-schema.py b/tools/validate-against-schema.py new file mode 100755 index 000000000..8599e1783 --- /dev/null +++ b/tools/validate-against-schema.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import os + +import jsonschema +import yaml + +here_dir = os.path.abspath(os.path.dirname(__file__)) +schema_yaml = os.path.join(here_dir, os.pardir, "binderhub-service", "values.schema.yaml") +values_yaml = os.path.join(here_dir, os.pardir, "binderhub-service", "values.yaml") +lint_and_validate_values_yaml = os.path.join( + here_dir, "templates", "lint-and-validate-values.yaml" +) + +with open(schema_yaml) as f: + schema = yaml.safe_load(f) +with open(values_yaml) as f: + values = yaml.safe_load(f) +with open(lint_and_validate_values_yaml) as f: + lint_and_validate_values = yaml.safe_load(f) + +# Validate values.yaml against schema +print("Validating values.yaml against values.schema.yaml...") +jsonschema.validate(values, schema) +print("OK!") +print() + +# Validate lint-and-validate-values.yaml against schema +print("Validating lint-and-validate-values.yaml against values.schema.yaml...") +jsonschema.validate(lint_and_validate_values, schema) +print("OK!") From b2786b94ba3339476c516653601fdf18394630c4 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 7 Mar 2023 14:52:19 +0100 Subject: [PATCH 003/200] Initial setup mash 2 --- .github/workflows/test-chart.yaml | 2 +- .pre-commit-config.yaml | 1 + LICENSE | 1 + README.md | 2 +- RELEASE.md | 48 ++++++++++ binderhub-service/.helmignore | 10 ++ binderhub-service/Chart.yaml | 34 +++---- binderhub-service/templates/service.yaml | 6 +- binderhub-service/values.schema.yaml | 26 ++++++ binderhub-service/values.yaml | 92 +++++++------------ chartpress.yaml | 2 +- dev-requirements.txt | 8 +- images/binderhub-service/requirements.txt | 2 +- pyproject.toml | 83 +++++++++++++++++ tests/conftest.py | 6 ++ tools/generate-json-schema.py | 4 +- tools/templates/lint-and-validate-values.yaml | 1 + tools/templates/lint-and-validate.py | 4 +- tools/validate-against-schema.py | 4 +- 19 files changed, 242 insertions(+), 94 deletions(-) create mode 100644 LICENSE create mode 100644 RELEASE.md create mode 100644 binderhub-service/values.schema.yaml create mode 100644 pyproject.toml create mode 100644 tests/conftest.py diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index 0aba89206..eabc97b62 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -32,7 +32,7 @@ jobs: python-version: "3.11" - name: Install dependencies - run: pip install chartpress yamllint + run: pip install -r dev-requirements.txt - name: Lint and validate run: tools/templates/lint-and-validate.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 234e861d3..44d9c1688 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,6 +47,7 @@ repos: rev: v3.0.0-alpha.6 hooks: - id: prettier + exclude: binderhub-service/templates/.* # Linting: Python code (see the file .flake8) - repo: https://github.com/PyCQA/flake8 diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..1333ed77b --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO diff --git a/README.md b/README.md index 9bf3320a1..2df013cc5 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# binderhub-service \ No newline at end of file +# binderhub-service diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000..6d5d38832 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,48 @@ +# How to make a release + +`binderhub-service` is a Helm chart available in the Helm chart repository +`https://2i2c.org/binderhub-service`. + +## Pre-requisites + +- Push rights to [2i2c-org/binderhub-service] + +## Steps to make a release + +1. Create a PR updating `docs/source/changelog.md` with [github-activity] and + continue only when its merged. + + ```shell + pip install github-activity + + github-activity --heading-level=3 2i2c-org/binderhub-service + ``` + +1. Checkout main and make sure it is up to date. + + ```shell + git checkout main + git fetch origin main + git reset --hard origin/main + ``` + +1. Update the version, make commits, and push a git tag with `tbump`. + + ```shell + pip install tbump + tbump --dry-run ${VERSION} + + tbump ${VERSION} + ``` + + Following this, the [CI system] will build and publish a release. + +1. Reset the version back to dev, e.g. `1.1.0-0.dev` after releasing `1.0.0` + + ```shell + tbump --no-tag ${NEXT_VERSION}-0.dev + ``` + +[2i2c-org/binderhub-service]: https://github.com/2i2c-org/binderhub-service +[github-activity]: https://github.com/executablebooks/github-activity +[ci system]: https://github.com/2i2c-org/binderhub-service/actions/workflows/release.yaml diff --git a/binderhub-service/.helmignore b/binderhub-service/.helmignore index 0e8a0eb36..1205485fa 100644 --- a/binderhub-service/.helmignore +++ b/binderhub-service/.helmignore @@ -1,3 +1,13 @@ +# Anything within the root folder of the Helm chart, where Chart.yaml resides, +# will be embedded into the packaged Helm chart. This is reasonable since only +# when the templates render after the chart has been packaged and distributed, +# will the templates logic evaluate that determines if other files were +# referenced, such as our our files/hub/jupyterhub_config.py. +# +# Here are files that we intentionally ignore to avoid them being packaged, +# because we don't want to reference them from our templates anyhow. +values.schema.yaml + # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. diff --git a/binderhub-service/Chart.yaml b/binderhub-service/Chart.yaml index a3251f86f..02aaa7a15 100644 --- a/binderhub-service/Chart.yaml +++ b/binderhub-service/Chart.yaml @@ -1,24 +1,14 @@ +# Chart.yaml v2 reference: https://helm.sh/docs/topics/charts/#the-chartyaml-file apiVersion: v2 name: binderhub-service -description: A Helm chart for Kubernetes - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "1.16.0" +version: 0.0.1-set.by.chartpress +appVersion: "1.0.0" +description: A BinderHub installation separate from JupyterHub +keywords: [binderhub, binderhub-service, repo2docker, jupyterhub, jupyter] +home: https://2i2c.org/binderhub-service +sources: [https://github.com/2i2c-org/binderhub-service] +icon: https://binderhub.readthedocs.io/en/latest/_static/logo.png +kubeVersion: ">=1.23.0-0" +maintainers: + - name: Erik Sundell + email: erik@sundellopensource.se diff --git a/binderhub-service/templates/service.yaml b/binderhub-service/templates/service.yaml index 15a6b6895..077592f55 100644 --- a/binderhub-service/templates/service.yaml +++ b/binderhub-service/templates/service.yaml @@ -2,8 +2,7 @@ apiVersion: v1 kind: Service metadata: name: {{ include "binderhub-service.fullname" . }} - labels: - {{- include "binderhub-service.labels" . | nindent 4 }} + labels: {{- include "binderhub-service.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: @@ -11,5 +10,4 @@ spec: targetPort: http protocol: TCP name: http - selector: - {{- include "binderhub-service.selectorLabels" . | nindent 4 }} + selector: {{- include "binderhub-service.selectorLabels" . | nindent 4 }} diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml new file mode 100644 index 000000000..0522ae5ec --- /dev/null +++ b/binderhub-service/values.schema.yaml @@ -0,0 +1,26 @@ +# This schema (a JSONSchema in YAML format) is used to generate +# values.schema.json to be packaged with the Helm chart. +# +# This schema is also planned to be used by our documentation system to build +# the configuration reference section based on the description fields. See +# docs/source/conf.py for that logic in the future! +# +# We look to document everything we have default values for in values.yaml, but +# we don't look to enforce the perfect validation logic within this file. +# +# ref: https://helm.sh/docs/topics/charts/#schema-files +# ref: https://json-schema.org/learn/getting-started-step-by-step.html +# +$schema: http://json-schema.org/draft-07/schema# +type: object +additionalProperties: true +required: + - global +properties: + nameOverride: + type: string + fullnameOverride: + type: string + global: + type: object + additionalProperties: true diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index a84bb5d08..f4422f84f 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -1,82 +1,60 @@ -# Default values for binderhub-service. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. +# General configuration +# ----------------------------------------------------------------------------- +# +nameOverride: "" +fullnameOverride: "" +global: {} +# Deployment resource +# ----------------------------------------------------------------------------- +# replicaCount: 1 image: - repository: nginx - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "" + repository: quay.io/2i2c/binderhub-service + tag: "set-by-chartpress" + pullPolicy: "" + pullSecrets: [] +securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 +resources: {} -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" +podAnnotations: {} +podSecurityContext: {} +nodeSelector: {} +tolerations: [] +affinity: {} +# ServiceAccount resource +# ----------------------------------------------------------------------------- +# serviceAccount: - # Specifies whether a service account should be created create: true - # Annotations to add to the service account annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template name: "" -podAnnotations: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - +# Service resource +# ----------------------------------------------------------------------------- +# service: type: ClusterIP port: 80 +# Ingress resource +# ----------------------------------------------------------------------------- +# ingress: enabled: false className: "" annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" hosts: - host: chart-example.local paths: - path: / pathType: ImplementationSpecific tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -nodeSelector: {} - -tolerations: [] - -affinity: {} diff --git a/chartpress.yaml b/chartpress.yaml index 053831740..4eb593efe 100644 --- a/chartpress.yaml +++ b/chartpress.yaml @@ -20,4 +20,4 @@ charts: # binderhub-service, the container where the binderhub the Python # application is running. binderhub-service: - valuesPath: binderhub.image + valuesPath: image diff --git a/dev-requirements.txt b/dev-requirements.txt index 423bcad36..4d16ff995 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -7,9 +7,11 @@ # chartpress -# pytest run tests that require requests and pyyaml +# pytest is used to run tests pytest + +# pyyaml is used by script under tools/ pyyaml -# tbump is making releases -tbump +# yamllint is used by tools/templates/lint-and-validate.py script +yamllint diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 40e2f446c..6bfd16dc8 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,7 +4,7 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -alembic==1.10.0 +alembic==1.10.1 # via jupyterhub async-generator==1.10 # via jupyterhub diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..c2ffcd993 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,83 @@ +# NOTE: This github repository houses a Helm chart and some Python based scripts +# - not a python package. This file is only used to provide configuration +# for misc Python based utilities. +# + +# autoflake is used for autoformatting Python code +# +# ref: https://github.com/PyCQA/autoflake#readme +# +[tool.autoflake] +ignore-init-module-imports = true +remove-all-unused-imports = true +remove-duplicate-keys = true +remove-unused-variables = true + + +# isort is used for autoformatting Python code +# +# ref: https://pycqa.github.io/isort/ +# +[tool.isort] +profile = "black" + + +# black is used for autoformatting Python code +# +# ref: https://black.readthedocs.io/en/stable/ +# +[tool.black] +target_version = [ + "py310", + "py311", +] + + +# pytest is used for running Python based tests +# +# ref: https://docs.pytest.org/en/stable/ +# +[tool.pytest.ini_options] +addopts = "--verbose --color=yes --durations=10" +asyncio_mode = "auto" + + +# tbump is a tool to update version fields that we use +# to update baseVersion in chartpress.yaml as documented +# in RELEASE.md +# +# Config reference: https://github.com/your-tools/tbump#readme +# +[tool.tbump] +github_url = "https://github.com/2i2c-org/binderhub-service" + +[tool.tbump.version] +current = "0.1.0-0.dev" + +# match our prerelease prefixes +# -alpha.1 +# -beta.2 +# -0.dev +regex = ''' + (?P\d+) + \. + (?P\d+) + \. + (?P\d+) + (\- + (?P + ( + (alpha|beta|rc)\.\d+| + 0\.dev + ) + ) + )? +''' + +[tool.tbump.git] +message_template = "Bump to {new_version}" +tag_template = "{new_version}" + +[[tool.tbump.file]] +src = "chartpress.yaml" +search = 'baseVersion: "{current_version}"' diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..f3d5fee12 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +""" +conftest.py is read by pytest automatically and can be used to declare fixtures +referenced by test functions. + +ref: https://docs.pytest.org/en/latest/writing_plugins.html#conftest-py-plugins +""" diff --git a/tools/generate-json-schema.py b/tools/generate-json-schema.py index bf1116609..929e36b32 100755 --- a/tools/generate-json-schema.py +++ b/tools/generate-json-schema.py @@ -16,7 +16,9 @@ import yaml here_dir = os.path.abspath(os.path.dirname(__file__)) -schema_yaml = os.path.join(here_dir, os.pardir, "binderhub-service", "values.schema.yaml") +schema_yaml = os.path.join( + here_dir, os.pardir, "binderhub-service", "values.schema.yaml" +) values_schema_json = os.path.join( here_dir, os.pardir, "binderhub-service", "values.schema.json" ) diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml index e69de29bb..490a0b7ed 100644 --- a/tools/templates/lint-and-validate-values.yaml +++ b/tools/templates/lint-and-validate-values.yaml @@ -0,0 +1 @@ +global: {} diff --git a/tools/templates/lint-and-validate.py b/tools/templates/lint-and-validate.py index 05b1b2fbb..6d574d611 100755 --- a/tools/templates/lint-and-validate.py +++ b/tools/templates/lint-and-validate.py @@ -47,7 +47,7 @@ def lint(yamllint_config, values, output_dir, strict, debug): print("### Linting started") print("### 1/3 - helm lint: lint helm templates") - helm_lint_cmd = ["helm", "lint", "../../jupyterhub", f"--values={values}"] + helm_lint_cmd = ["helm", "lint", "../../binderhub-service", f"--values={values}"] if strict: helm_lint_cmd.append("--strict") if debug: @@ -58,7 +58,7 @@ def lint(yamllint_config, values, output_dir, strict, debug): helm_template_cmd = [ "helm", "template", - "../../jupyterhub", + "../../binderhub-service", f"--values={values}", f"--output-dir={output_dir}", ] diff --git a/tools/validate-against-schema.py b/tools/validate-against-schema.py index 8599e1783..4e12cf5e6 100755 --- a/tools/validate-against-schema.py +++ b/tools/validate-against-schema.py @@ -5,7 +5,9 @@ import yaml here_dir = os.path.abspath(os.path.dirname(__file__)) -schema_yaml = os.path.join(here_dir, os.pardir, "binderhub-service", "values.schema.yaml") +schema_yaml = os.path.join( + here_dir, os.pardir, "binderhub-service", "values.schema.yaml" +) values_yaml = os.path.join(here_dir, os.pardir, "binderhub-service", "values.yaml") lint_and_validate_values_yaml = os.path.join( here_dir, "templates", "lint-and-validate-values.yaml" From 5fe4c0d25823bb5b25da96d29e6e3a3cb749f86f Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 24 Mar 2023 11:08:00 +0100 Subject: [PATCH 004/200] ci: fix deprecated use of set-output --- .github/workflows/release.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 05cb33c00..1726e5333 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -61,7 +61,8 @@ jobs: ): publishing = "true" print("Publishing chart") - print(f"::set-output name=publishing::{publishing}") + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"publishing={publishing}\n") - name: Set up QEMU (for docker buildx) uses: docker/setup-qemu-action@v2 From d72b9c2e3c44a8efc7ed7e4b1f49d0c8d3476197 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 24 Mar 2023 11:52:57 +0100 Subject: [PATCH 005/200] ci: add permissions to push to gh-pages branch --- .github/workflows/release.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1726e5333..f5a316274 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -32,6 +32,9 @@ jobs: # release: runs-on: ubuntu-22.04 + permissions: + contents: write + steps: - uses: actions/checkout@v3 with: From 39fbe932518a9f808defad41b28ac555dabbc2ee Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 24 Mar 2023 10:57:03 +0100 Subject: [PATCH 006/200] Build image with binderhub from feature development branch --- images/binderhub-service/Dockerfile | 4 ++-- images/binderhub-service/requirements.in | 5 ++--- images/binderhub-service/requirements.txt | 22 +++++++++++++--------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/images/binderhub-service/Dockerfile b/images/binderhub-service/Dockerfile index 646a65715..b656319fd 100644 --- a/images/binderhub-service/Dockerfile +++ b/images/binderhub-service/Dockerfile @@ -64,5 +64,5 @@ RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ ENV PYTHONUNBUFFERED=1 EXPOSE 8585 -ENTRYPOINT ["tini", "--", "python", "-m", "binderhub"] -CMD ["--config", "/etc/binderhub/config/binderhub_config.py"] +ENTRYPOINT ["tini", "--"] +CMD ["python", "-m", "binderhub", "--config", "/etc/binderhub/config/binderhub_config.py"] diff --git a/images/binderhub-service/requirements.in b/images/binderhub-service/requirements.in index 4669728bf..a16784d01 100644 --- a/images/binderhub-service/requirements.in +++ b/images/binderhub-service/requirements.in @@ -1,6 +1,5 @@ # This file is the input to requirements.txt, which is a frozen version of this. # To update requirements.txt, use the "Run workflow" button at # https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml - -binderhub -pycurl +# +binderhub[pycurl] @ git+https://github.com/consideratio/binderhub@opt-out-of-launch diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 6bfd16dc8..d6ba01d8c 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,13 +4,13 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -alembic==1.10.1 +alembic==1.10.2 # via jupyterhub async-generator==1.10 # via jupyterhub attrs==22.2.0 # via jsonschema -binderhub==0.1.0 +binderhub @ git+https://github.com/consideratio/binderhub@opt-out-of-launch # via -r requirements.in cachetools==5.3.0 # via google-auth @@ -24,13 +24,13 @@ cffi==1.15.1 # via cryptography charset-normalizer==3.1.0 # via requests -cryptography==39.0.2 +cryptography==40.0.0 # via pyopenssl docker==6.0.1 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.16.2 +google-auth==2.16.3 # via kubernetes greenlet==2.0.2 # via sqlalchemy @@ -41,7 +41,9 @@ jinja2==3.1.2 # binderhub # jupyterhub jsonschema==4.17.3 - # via jupyter-telemetry + # via + # binderhub + # jupyter-telemetry jupyter-telemetry==0.1.0 # via jupyterhub jupyterhub==3.1.1 @@ -77,8 +79,10 @@ pyasn1-modules==0.2.8 pycparser==2.21 # via cffi pycurl==7.45.2 - # via -r requirements.in -pyopenssl==23.0.0 + # via binderhub +pyjwt==2.6.0 + # via binderhub +pyopenssl==23.1.0 # via certipy pyrsistent==0.19.3 # via jsonschema @@ -109,7 +113,7 @@ six==1.16.0 # google-auth # kubernetes # python-dateutil -sqlalchemy==2.0.5.post1 +sqlalchemy==2.0.7 # via # alembic # jupyterhub @@ -126,7 +130,7 @@ typing-extensions==4.5.0 # via # alembic # sqlalchemy -urllib3==1.26.14 +urllib3==1.26.15 # via # docker # kubernetes From dba54b416c368f1a34b4010fc3bf0bc91906aa45 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 24 Mar 2023 13:19:56 +0100 Subject: [PATCH 007/200] image: fix failure to install pre-built wheel --- images/binderhub-service/Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/images/binderhub-service/Dockerfile b/images/binderhub-service/Dockerfile index b656319fd..a3a989d7c 100644 --- a/images/binderhub-service/Dockerfile +++ b/images/binderhub-service/Dockerfile @@ -56,6 +56,14 @@ RUN apt-get update \ # install wheels built in the build stage COPY requirements.txt requirements.txt +# FIXME: As long as we reference git in requirements.txt (via requirements.in), +# we need to make sure we don't end up re-installing from git here but +# using the built wheel. +# +# This sed replace operation removes the " @ git+..." stuff, remove it +# when we reference version controlled releases of binderhub. +# +RUN sed -i -E 's/binderhub @ git.+/binderhub/' requirements.txt ARG PIP_CACHE_DIR=/tmp/pip-cache RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ --mount=type=cache,from=build-stage,source=/tmp/wheels,target=/tmp/wheels \ From c7580dce8ae9c88084fdc8d6688ae3c14818f0df Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 24 Mar 2023 13:25:42 +0100 Subject: [PATCH 008/200] ci: use docker buildkit for chartpress' build of images --- .github/workflows/test-chart.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index eabc97b62..0bb68f0ad 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -137,6 +137,8 @@ jobs: # Build our images if needed and update Chart.yaml and values.yaml with # version and tags - run: chartpress + env: + DOCKER_BUILDKIT: "1" - name: Generate values.schema.json from values.schema.yaml run: tools/generate-json-schema.py From 78fdf8db3973144c5f7be3b8e2867e46d5fff304 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 24 Mar 2023 13:30:47 +0100 Subject: [PATCH 009/200] ci: fix typo --- .github/workflows/test-chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index 0bb68f0ad..d16017139 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -165,4 +165,4 @@ jobs: - uses: jupyterhub/action-k8s-namespace-report@v1 if: always() with: - important-workloads: deploy/binderhub + important-workloads: deploy/binderhub-service From f6335b2a2f9a8d0d3cb6bcd89cca6807289072f2 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 24 Mar 2023 12:18:18 +0100 Subject: [PATCH 010/200] ci: use ssh deploy key for release.yaml to push to this repo --- .github/workflows/release.yaml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f5a316274..70507b4c3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -32,8 +32,6 @@ jobs: # release: runs-on: ubuntu-22.04 - permissions: - contents: write steps: - uses: actions/checkout@v3 @@ -91,6 +89,26 @@ jobs: git config --global user.email "github-actions@example.local" git config --global user.name "GitHub Actions user" + - name: Setup push rights to Helm chart repository's git repo + # This was setup by... + # + # 1. Generating a private/public key pair: + # ssh-keygen -t ed25519 -C "2i2c-org/binderhub-service" -f /tmp/id_ed25519 + # + # 2. Registering the private key (/tmp/id_ed25519) as a secret for this + # repo: https://github.com/2i2c-org/binderhub-service/settings/secrets/actions + # + # 3. Registering the public key (/tmp/id_ed25519.pub) as a deploy key + # with push rights for the Helm chart repository's git repo: + # https://github.com/2i2c-org/binderhub-service/settings/keys + # + if: steps.publishing.outputs.publishing + run: | + mkdir -p ~/.ssh + ssh-keyscan github.com >> ~/.ssh/known_hosts + echo "${{ secrets.HELM_CHART_REPO_DEPLOY_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + - name: Setup docker push rights to quay.io if: steps.publishing.outputs.publishing run: docker login -u "${{ secrets.DOCKER_USERNAME }}" -p "${{ secrets.DOCKER_PASSWORD }}" quay.io From 86a9bc798b2192522615f13fef55e846bc800946 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 24 Mar 2023 21:38:15 +0100 Subject: [PATCH 011/200] Fix rbac in templates and make follow best practices --- binderhub-service/Chart.yaml | 3 ++ binderhub-service/templates/_helpers.tpl | 37 ++++++++++---------- binderhub-service/templates/deployment.yaml | 25 +++++++------ binderhub-service/templates/role.yaml | 15 ++++++++ binderhub-service/templates/rolebinding.yaml | 16 +++++++++ binderhub-service/templates/service.yaml | 9 +++-- binderhub-service/values.yaml | 12 ++++--- 7 files changed, 81 insertions(+), 36 deletions(-) create mode 100644 binderhub-service/templates/role.yaml create mode 100644 binderhub-service/templates/rolebinding.yaml diff --git a/binderhub-service/Chart.yaml b/binderhub-service/Chart.yaml index 02aaa7a15..d37799576 100644 --- a/binderhub-service/Chart.yaml +++ b/binderhub-service/Chart.yaml @@ -2,6 +2,9 @@ apiVersion: v2 name: binderhub-service version: 0.0.1-set.by.chartpress +# FIXME: appVersion should represent the version of binderhub in +# images/binderhub-service/requirements.txt +# appVersion: "1.0.0" description: A BinderHub installation separate from JupyterHub keywords: [binderhub, binderhub-service, repo2docker, jupyterhub, jupyter] diff --git a/binderhub-service/templates/_helpers.tpl b/binderhub-service/templates/_helpers.tpl index c53465918..26321f7cc 100644 --- a/binderhub-service/templates/_helpers.tpl +++ b/binderhub-service/templates/_helpers.tpl @@ -1,14 +1,14 @@ -{{/* -Expand the name of the chart. +{{- /* + Expand the name of the chart. */}} {{- define "binderhub-service.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. +{{- /* + Create a default fully qualified app name. + We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). + If release name contains chart name it will be used as a full name. */}} {{- define "binderhub-service.fullname" -}} {{- if .Values.fullnameOverride }} @@ -23,15 +23,15 @@ If release name contains chart name it will be used as a full name. {{- end }} {{- end }} -{{/* -Create chart name and version as used by the chart label. +{{- /* + Create chart name and version as used by the chart label. */}} {{- define "binderhub-service.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} -{{/* -Common labels +{{- /* + Common labels */}} {{- define "binderhub-service.labels" -}} helm.sh/chart: {{ include "binderhub-service.chart" . }} @@ -42,22 +42,22 @@ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} -{{/* -Selector labels +{{- /* + Selector labels */}} {{- define "binderhub-service.selectorLabels" -}} app.kubernetes.io/name: {{ include "binderhub-service.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} -{{/* -Create the name of the service account to use +{{- /* + Create the name of the service account to use */}} {{- define "binderhub-service.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} -{{- default (include "binderhub-service.fullname" .) .Values.serviceAccount.name }} +{{- include "binderhub-service.fullname" . | default .Values.serviceAccount.name }} {{- else }} -{{- default "default" .Values.serviceAccount.name }} +{{- .Values.serviceAccount.name }} {{- end }} {{- end }} @@ -68,7 +68,8 @@ Create the name of the service account to use Renders a valid git reference from a chartpress generated version string. In practice, either a git tag or a git commit hash will be returned. - - The version string will follow a chartpress pattern, see + - The version string will follow a chartpress pattern, + like "0.1.0-0.dev.git.17.h8368bc0", see https://github.com/jupyterhub/chartpress#examples-chart-versions-and-image-tags. - The regexReplaceAll function is a sprig library function, see @@ -78,5 +79,5 @@ Create the name of the service account to use example. */}} {{- define "binderhub-service.chart-version-to-git-ref" -}} -{{- regexReplaceAll ".*[.-]n\\d+[.]h(.*)" . "${1}" }} +{{- regexReplaceAll ".*\\.git\\.\\d+\\.h(.*)" . "${1}" }} {{- end }} diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index 98b50517b..4718db314 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -5,7 +5,7 @@ metadata: labels: {{- include "binderhub-service.labels" . | nindent 4 }} spec: - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.replicas }} selector: matchLabels: {{- include "binderhub-service.selectorLabels" . | nindent 6 }} @@ -18,27 +18,30 @@ spec: labels: {{- include "binderhub-service.selectorLabels" . | nindent 8 }} spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- . | toYaml | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "binderhub-service.serviceAccountName" . }} - securityContext: - {{- .Values.podSecurityContext | toYaml | nindent 8 }} containers: - - name: {{ .Chart.Name }} + - name: binderhub securityContext: {{- .Values.securityContext | toYaml | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" {{- with .Values.image.pullPolicy }} imagePullPolicy: {{ . }} {{- end }} ports: - name: http containerPort: {{ .Values.service.port }} - protocol: TCP resources: {{- .Values.resources | toYaml | nindent 12 }} + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with include "binderhub-service.serviceAccountName" . }} + serviceAccountName: {{ . }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- . | toYaml | nindent 8 }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- . | toYaml | nindent 8 }} diff --git a/binderhub-service/templates/role.yaml b/binderhub-service/templates/role.yaml new file mode 100644 index 000000000..9d56b73cb --- /dev/null +++ b/binderhub-service/templates/role.yaml @@ -0,0 +1,15 @@ +{{- if .Values.rbac.create -}} +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ include "binderhub-service.fullname" . }} + labels: + {{- include "binderhub-service.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "watch", "list", "create", "delete"] + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] +{{- end }} diff --git a/binderhub-service/templates/rolebinding.yaml b/binderhub-service/templates/rolebinding.yaml new file mode 100644 index 000000000..c8265fc63 --- /dev/null +++ b/binderhub-service/templates/rolebinding.yaml @@ -0,0 +1,16 @@ +{{- if .Values.rbac.create -}} +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ include "binderhub-service.fullname" . }} + labels: + {{- include "binderhub-service.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + namespace: {{ .Release.Namespace }} + name: {{ include "binderhub-service.serviceAccountName" . }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "binderhub-service.fullname" . }} +{{- end }} diff --git a/binderhub-service/templates/service.yaml b/binderhub-service/templates/service.yaml index 077592f55..f2d37b6e7 100644 --- a/binderhub-service/templates/service.yaml +++ b/binderhub-service/templates/service.yaml @@ -3,11 +3,14 @@ kind: Service metadata: name: {{ include "binderhub-service.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- . | toYaml | nindent 4 }} + {{- end }} spec: type: {{ .Values.service.type }} ports: - - port: {{ .Values.service.port }} + - name: http + port: {{ .Values.service.port }} targetPort: http - protocol: TCP - name: http selector: {{- include "binderhub-service.selectorLabels" . | nindent 4 }} diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index f4422f84f..902421ddc 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -8,8 +8,7 @@ global: {} # Deployment resource # ----------------------------------------------------------------------------- # -replicaCount: 1 - +replicas: 1 image: repository: quay.io/2i2c/binderhub-service tag: "set-by-chartpress" @@ -23,20 +22,25 @@ securityContext: runAsNonRoot: true runAsUser: 1000 resources: {} - podAnnotations: {} podSecurityContext: {} nodeSelector: {} tolerations: [] affinity: {} +# RBAC resources +# ----------------------------------------------------------------------------- +# +rbac: + create: true + # ServiceAccount resource # ----------------------------------------------------------------------------- # serviceAccount: create: true - annotations: {} name: "" + annotations: {} # Service resource # ----------------------------------------------------------------------------- From fe349e9d4dc37c6015535c673f6913d82fe656cf Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 24 Mar 2023 21:39:14 +0100 Subject: [PATCH 012/200] Fix lint and validation and complete values.schema.yaml --- binderhub-service/values.schema.yaml | 118 +++++++++++++++++- tools/templates/lint-and-validate-values.yaml | 38 ++++++ 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 0522ae5ec..4175f9ff1 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -13,10 +13,23 @@ # $schema: http://json-schema.org/draft-07/schema# type: object -additionalProperties: true +additionalProperties: false required: + # General configuration + - nameOverride + - fullnameOverride - global + # Deployment resource + - image + # Other resources + - rbac + - serviceAccount + - service + - ingress properties: + # General configuration + # --------------------------------------------------------------------------- + # nameOverride: type: string fullnameOverride: @@ -24,3 +37,106 @@ properties: global: type: object additionalProperties: true + + # Deployment resource + # --------------------------------------------------------------------------- + # + replicas: + type: integer + image: + type: object + additionalProperties: false + required: [repository, tag] + properties: + repository: + type: string + tag: + type: string + pullPolicy: + enum: [null, "", IfNotPresent, Always, Never] + pullSecrets: + type: array + resources: + type: object + additionalProperties: true + securityContext: + type: object + additionalProperties: true + podSecurityContext: + type: object + additionalProperties: true + podAnnotations: &labels-and-annotations + type: object + additionalProperties: false + patternProperties: + ".*": + type: string + nodeSelector: + type: object + additionalProperties: true + tolerations: + type: array + affinity: + type: object + additionalProperties: true + + # RBAC resources + # --------------------------------------------------------------------------- + # + rbac: + type: object + additionalProperties: false + required: [create] + properties: + create: + type: boolean + + # ServiceAccount resource + # --------------------------------------------------------------------------- + # + serviceAccount: + type: object + additionalProperties: false + required: [create, name] + properties: + create: + type: boolean + name: + type: string + annotations: *labels-and-annotations + + # Service resource + # --------------------------------------------------------------------------- + # + service: + type: object + additionalProperties: false + required: [type, port] + properties: + type: + type: string + port: + type: integer + annotations: *labels-and-annotations + + # Ingress resource + # --------------------------------------------------------------------------- + # + ingress: + type: object + additionalProperties: false + required: [enabled] + properties: + enabled: + type: boolean + className: + type: [string, "null"] + hosts: + type: array + pathSuffix: + type: [string, "null"] + pathType: + enum: [Prefix, Exact, ImplementationSpecific] + tls: + type: array + annotations: *labels-and-annotations diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml index 490a0b7ed..f8ade6620 100644 --- a/tools/templates/lint-and-validate-values.yaml +++ b/tools/templates/lint-and-validate-values.yaml @@ -1 +1,39 @@ +# General configuration +# ----------------------------------------------------------------------------- +# +nameOverride: "" +fullnameOverride: "" global: {} + +# Deployment resource +# ----------------------------------------------------------------------------- +# +image: + repository: quay.io/2i2c/binderhub-service + tag: "set-by-chartpress" + +# RBAC resources +# ----------------------------------------------------------------------------- +# +rbac: + create: true + +# ServiceAccount resource +# ----------------------------------------------------------------------------- +# +serviceAccount: + create: true + name: "" + +# Service resource +# ----------------------------------------------------------------------------- +# +service: + type: ClusterIP + port: 80 + +# Ingress resource +# ----------------------------------------------------------------------------- +# +ingress: + enabled: false From 131fe604e512f65a256ddf6658ad366bb4ef4275 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 24 Mar 2023 22:49:31 +0100 Subject: [PATCH 013/200] Document a background and preliminary scope for the project --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index 2df013cc5..93affe332 100644 --- a/README.md +++ b/README.md @@ -1 +1,44 @@ # binderhub-service + +The binderhub-service is a Helm chart and guide to run BinderHub (the Python +software), as a standalone service to build and push images with repo2docker, +possibly configured for use with a JupyterHub chart installation. + +## Background + +The [binderhub chart]'s main use case has been to build images and launch +servers based on them for anonymous users without persistent home folder +storage. The binderhub chart does this by installing the [jupyterhub chart] +opinionatedly configured to not authenticate and provide users with home folder +storage. + +There are use cases for putting binderhub behind authentication though, so +support for that [was added]. There are also use cases for providing users with +persistent home folders, and this led to [persistent binderhub chart] being +developed. The persistent binderhub chart, by depending on the binderhub chart, +depending on the jupyterhub chart, is even more complex than the binderhub chart +though. Currently, the project isn't actively maintained. + +Could a new chart be developed to deploy binderhub next to an existing +jupyterhub instead, or even entirely on its own without the part where the built +image is launched in a jupyterhub? Could this enable existing jupyterhub chart +installations to add on binderhub like functionality? This is what this project +is exploring! + +## Project scope + +This project is currently developed to provide a Helm chart and documentation to +deploy and configure BinderHub the Python software for use either by itself, or +next to a JupyterHub Helm chart installation. + +The documentation should help configure the BinderHub service to: + +- run behind JupyterHub authentication and authorization +- in one or more ways be able to launch built images +- in one or more ways handle the issue repo2docker building an image with data + put where JupyterHub user home folders typically is mounted + +[binderhub chart]: https://github.com/jupyterhub/binderhub +[jupyterhub chart]: https://github.com/jupyterhub/zero-to-jupyterhub-k8s +[persistent binderhub chart]: https://github.com/gesiscss/persistent_binderhub +[was added]: https://github.com/jupyterhub/binderhub/pull/666 From 130ad72a8e17c1af694f716a95654df4a57b56f2 Mon Sep 17 00:00:00 2001 From: consideRatio Date: Sat, 1 Apr 2023 05:02:05 +0000 Subject: [PATCH 014/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index d6ba01d8c..24f0ee8dc 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -24,13 +24,13 @@ cffi==1.15.1 # via cryptography charset-normalizer==3.1.0 # via requests -cryptography==40.0.0 +cryptography==40.0.1 # via pyopenssl docker==6.0.1 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.16.3 +google-auth==2.17.1 # via kubernetes greenlet==2.0.2 # via sqlalchemy @@ -82,7 +82,7 @@ pycurl==7.45.2 # via binderhub pyjwt==2.6.0 # via binderhub -pyopenssl==23.1.0 +pyopenssl==23.1.1 # via certipy pyrsistent==0.19.3 # via jsonschema @@ -113,7 +113,7 @@ six==1.16.0 # google-auth # kubernetes # python-dateutil -sqlalchemy==2.0.7 +sqlalchemy==2.0.8 # via # alembic # jupyterhub From 851a419728bef43299f2601fc51f10172cd4fab3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 23:53:08 +0000 Subject: [PATCH 015/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/autoflake: v2.0.1 → v2.0.2](https://github.com/PyCQA/autoflake/compare/v2.0.1...v2.0.2) - [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 44d9c1688..1ebd8ecad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: # Autoformat: Python code - repo: https://github.com/PyCQA/autoflake - rev: v2.0.1 + rev: v2.0.2 hooks: - id: autoflake args: @@ -27,7 +27,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black args: From f85203489532267a0d1af7f5029aea144a64cd6a Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 7 Apr 2023 10:34:19 +0200 Subject: [PATCH 016/200] Allow binderhub to be configured via config and extraConfig --- .../mounted-files/binderhub_config.py | 51 +++++++++++++++++++ binderhub-service/templates/deployment.yaml | 12 ++++- binderhub-service/templates/secret.yaml | 23 +++++++++ binderhub-service/values.schema.yaml | 15 ++++++ binderhub-service/values.yaml | 17 +++++++ 5 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 binderhub-service/mounted-files/binderhub_config.py create mode 100644 binderhub-service/templates/secret.yaml diff --git a/binderhub-service/mounted-files/binderhub_config.py b/binderhub-service/mounted-files/binderhub_config.py new file mode 100644 index 000000000..5e51d2f70 --- /dev/null +++ b/binderhub-service/mounted-files/binderhub_config.py @@ -0,0 +1,51 @@ +""" +This configuration file is mounted to be read by binderhub with the sole purpose +of loading chart configuration passed via "config" and "extraConfig". +""" +from functools import lru_cache + +from ruamel.yaml import YAML + + +@lru_cache +def _read_chart_config(): + """ + Read chart configuration, mounted via a k8s Secret rendered by the chart. + """ + yaml = YAML(typ="safe") + with open("/etc/binderhub/config/config.yaml") as f: + return yaml.load(f) + + +@lru_cache +def get_chart_config(path, default=None): + """ + Returns the full chart configuration or a part of it based on a path like + "config.BinderHub". + """ + config = _read_chart_config() + for section in path.split("."): + if section not in config: + return default + if not isinstance(config, dict): + return default + config = config[section] + return config + + +# load the config object for traitlets based configuration +c = get_config() # noqa + +# load "config" (YAML values) +for section, value in get_chart_config("config").items(): + if not value: + continue + print(f"Loading config.{section}") + c[section].update(value) + +# load "extraConfig" (Python code) +for key, snippet in sorted(get_chart_config("extraConfig").items()): + if not snippet: + continue + print(f"Running extraConfig.{key}") + exec(snippet) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index 4718db314..ebdfabedc 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -18,10 +18,12 @@ spec: labels: {{- include "binderhub-service.selectorLabels" . | nindent 8 }} spec: + volumes: + - name: config + secret: + secretName: {{ include "binderhub-service.fullname" . }} containers: - name: binderhub - securityContext: - {{- .Values.securityContext | toYaml | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" {{- with .Values.image.pullPolicy }} imagePullPolicy: {{ . }} @@ -29,8 +31,14 @@ spec: ports: - name: http containerPort: {{ .Values.service.port }} + volumeMounts: + - name: config + mountPath: /etc/binderhub/config/ + readOnly: true resources: {{- .Values.resources | toYaml | nindent 12 }} + securityContext: + {{- .Values.securityContext | toYaml | nindent 12 }} {{- with .Values.image.pullSecrets }} imagePullSecrets: {{- . | toYaml | nindent 8 }} diff --git a/binderhub-service/templates/secret.yaml b/binderhub-service/templates/secret.yaml new file mode 100644 index 000000000..a3f2bc7e8 --- /dev/null +++ b/binderhub-service/templates/secret.yaml @@ -0,0 +1,23 @@ +{{- /* + Changes to this rendered manifest triggers a restart of the binderhub-service + pod as the pod specification includes an annotation with a hash of this. +*/ -}} +kind: Secret +apiVersion: v1 +metadata: + name: {{ include "binderhub-service.fullname" . }} + labels: + {{- include "binderhub-service.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- /* + To restart the binderhub-service pod only when relevant, we pick out the + chart configuration actually consumed in the mounted binderhub_config.py + file. + */}} + config.yaml: | + {{- pick .Values "config" "extraConfig" | toYaml | nindent 4 }} + + {{- /* Glob files to allow them to be mounted by the binderhub pod */}} + {{- /* key=filename: value=content */}} + {{- (.Files.Glob "files/*").AsConfig | nindent 2 }} diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 4175f9ff1..71390bb1c 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -41,6 +41,21 @@ properties: # Deployment resource # --------------------------------------------------------------------------- # + + config: + type: object + additionalProperties: false + patternProperties: + ".*": + type: [object, "null"] + additionalProperties: true + extraConfig: + type: object + additionalProperties: false + patternProperties: + ".*": + type: [string, "null"] + replicas: type: integer image: diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 902421ddc..d775130e7 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -8,6 +8,23 @@ global: {} # Deployment resource # ----------------------------------------------------------------------------- # + +# The "config" section below is rendered by a k8s Secret and mounted as a .yaml +# file together with binderhub_config.py for the binderhub Python software to +# parse. +# +# Together they make the configured values below translate so that +# config.BinderHub.base_url sets c.BinderHub.base_url, or more generally that +# config.X.y sets c.X.y where X is a class and y is a configurable traitlet on +# the class. +# +config: + BinderHub: + base_url: / + use_registry: true + KubernetesBuildExecutor: {} +extraConfig: {} + replicas: 1 image: repository: quay.io/2i2c/binderhub-service From a4d27e6204289786648f7f7fa79a354bb79ee303 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 24 Apr 2023 13:53:06 +0200 Subject: [PATCH 017/200] Refactor for improved comprehensibility of mounted config --- binderhub-service/mounted-files/binderhub_config.py | 2 +- binderhub-service/templates/deployment.yaml | 11 ++++++----- binderhub-service/templates/secret.yaml | 6 +++--- images/binderhub-service/Dockerfile | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/binderhub-service/mounted-files/binderhub_config.py b/binderhub-service/mounted-files/binderhub_config.py index 5e51d2f70..b757fc933 100644 --- a/binderhub-service/mounted-files/binderhub_config.py +++ b/binderhub-service/mounted-files/binderhub_config.py @@ -13,7 +13,7 @@ def _read_chart_config(): Read chart configuration, mounted via a k8s Secret rendered by the chart. """ yaml = YAML(typ="safe") - with open("/etc/binderhub/config/config.yaml") as f: + with open("/etc/binderhub/mounted-secret/chart-config.yaml") as f: return yaml.load(f) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index ebdfabedc..cdcc03e1c 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -11,15 +11,16 @@ spec: {{- include "binderhub-service.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/mounted-secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- . | toYaml | nindent 8 }} - {{- end }} + {{- end }} labels: {{- include "binderhub-service.selectorLabels" . | nindent 8 }} spec: volumes: - - name: config + - name: secret secret: secretName: {{ include "binderhub-service.fullname" . }} containers: @@ -32,8 +33,8 @@ spec: - name: http containerPort: {{ .Values.service.port }} volumeMounts: - - name: config - mountPath: /etc/binderhub/config/ + - name: secret + mountPath: /etc/binderhub/mounted-secret/ readOnly: true resources: {{- .Values.resources | toYaml | nindent 12 }} diff --git a/binderhub-service/templates/secret.yaml b/binderhub-service/templates/secret.yaml index a3f2bc7e8..86f1631a6 100644 --- a/binderhub-service/templates/secret.yaml +++ b/binderhub-service/templates/secret.yaml @@ -1,6 +1,6 @@ {{- /* Changes to this rendered manifest triggers a restart of the binderhub-service - pod as the pod specification includes an annotation with a hash of this. + pod as the pod specification includes an annotation with a checksum of this. */ -}} kind: Secret apiVersion: v1 @@ -15,9 +15,9 @@ stringData: chart configuration actually consumed in the mounted binderhub_config.py file. */}} - config.yaml: | + chart-config.yaml: | {{- pick .Values "config" "extraConfig" | toYaml | nindent 4 }} {{- /* Glob files to allow them to be mounted by the binderhub pod */}} {{- /* key=filename: value=content */}} - {{- (.Files.Glob "files/*").AsConfig | nindent 2 }} + {{- (.Files.Glob "mounted-files/*").AsConfig | nindent 2 }} diff --git a/images/binderhub-service/Dockerfile b/images/binderhub-service/Dockerfile index a3a989d7c..39f37269a 100644 --- a/images/binderhub-service/Dockerfile +++ b/images/binderhub-service/Dockerfile @@ -73,4 +73,4 @@ RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ ENV PYTHONUNBUFFERED=1 EXPOSE 8585 ENTRYPOINT ["tini", "--"] -CMD ["python", "-m", "binderhub", "--config", "/etc/binderhub/config/binderhub_config.py"] +CMD ["python", "-m", "binderhub", "--config", "/etc/binderhub/mounted-secret/binderhub_config.py"] From ce6187066a7b16995fb637454869e9f2dde99606 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Apr 2023 12:30:29 +0200 Subject: [PATCH 018/200] Fixes to get_chart_config - It couldn't return the full config before - It could run into a obscure bug if a config path's key was unexpectedly found in a string instead of a dictionary - It is tricky logic to read, so a comment was added and the variable names were adjusted --- .../mounted-files/binderhub_config.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/binderhub-service/mounted-files/binderhub_config.py b/binderhub-service/mounted-files/binderhub_config.py index b757fc933..03057ca1a 100644 --- a/binderhub-service/mounted-files/binderhub_config.py +++ b/binderhub-service/mounted-files/binderhub_config.py @@ -18,18 +18,23 @@ def _read_chart_config(): @lru_cache -def get_chart_config(path, default=None): +def get_chart_config(config_path=None, default=None): """ - Returns the full chart configuration or a part of it based on a path like - "config.BinderHub". + Returns the full chart configuration, or a section of it based on a config + section's path like "config.BinderHub". """ config = _read_chart_config() - for section in path.split("."): - if section not in config: - return default + if not config_path: + return config + + for key in config_path.split("."): if not isinstance(config, dict): + # can't resolve full path, + # parent section's config is is a scalar or null + return default + if key not in config: return default - config = config[section] + config = config[key] return config From 514cb70ab7e55a6aff8366de41bab307817b328c Mon Sep 17 00:00:00 2001 From: consideRatio Date: Thu, 27 Apr 2023 05:32:04 +0000 Subject: [PATCH 019/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 24f0ee8dc..a9cb2f98e 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,11 +4,11 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -alembic==1.10.2 +alembic==1.10.4 # via jupyterhub async-generator==1.10 # via jupyterhub -attrs==22.2.0 +attrs==23.1.0 # via jsonschema binderhub @ git+https://github.com/consideratio/binderhub@opt-out-of-launch # via -r requirements.in @@ -24,13 +24,13 @@ cffi==1.15.1 # via cryptography charset-normalizer==3.1.0 # via requests -cryptography==40.0.1 +cryptography==40.0.2 # via pyopenssl docker==6.0.1 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.17.1 +google-auth==2.17.3 # via kubernetes greenlet==2.0.2 # via sqlalchemy @@ -46,7 +46,7 @@ jsonschema==4.17.3 # jupyter-telemetry jupyter-telemetry==0.1.0 # via jupyterhub -jupyterhub==3.1.1 +jupyterhub==4.0.0 # via binderhub kubernetes==26.1.0 # via binderhub @@ -60,7 +60,7 @@ oauthlib==3.2.2 # via # jupyterhub # requests-oauthlib -packaging==23.0 +packaging==23.1 # via # docker # jupyterhub @@ -70,11 +70,11 @@ prometheus-client==0.16.0 # via # binderhub # jupyterhub -pyasn1==0.4.8 +pyasn1==0.5.0 # via # pyasn1-modules # rsa -pyasn1-modules==0.2.8 +pyasn1-modules==0.3.0 # via google-auth pycparser==2.21 # via cffi @@ -96,7 +96,7 @@ python-json-logger==2.0.7 # jupyter-telemetry pyyaml==6.0 # via kubernetes -requests==2.28.2 +requests==2.29.0 # via # docker # jupyterhub @@ -113,11 +113,11 @@ six==1.16.0 # google-auth # kubernetes # python-dateutil -sqlalchemy==2.0.8 +sqlalchemy==2.0.11 # via # alembic # jupyterhub -tornado==6.2 +tornado==6.3.1 # via # binderhub # jupyterhub From a10b9cc81c9f027391e1f79aaa763c743de8f79b Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Apr 2023 09:06:04 +0200 Subject: [PATCH 020/200] Add docker-api daemonset to prepare k8s nodes for build pods --- binderhub-service/templates/_helpers.tpl | 4 +- binderhub-service/templates/deployment.yaml | 2 + .../templates/docker-api/daemonset.yaml | 68 +++++++++++++++++++ binderhub-service/values.schema.yaml | 34 +++++++--- binderhub-service/values.yaml | 34 ++++++++-- tools/templates/lint-and-validate-values.yaml | 11 +++ 6 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 binderhub-service/templates/docker-api/daemonset.yaml diff --git a/binderhub-service/templates/_helpers.tpl b/binderhub-service/templates/_helpers.tpl index 26321f7cc..5a443a6d1 100644 --- a/binderhub-service/templates/_helpers.tpl +++ b/binderhub-service/templates/_helpers.tpl @@ -2,7 +2,7 @@ Expand the name of the chart. */}} {{- define "binderhub-service.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- .Values.nameOverride | default .Chart.Name | trunc 63 | trimSuffix "-" }} {{- end }} {{- /* @@ -14,7 +14,7 @@ {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} +{{- $name := .Values.nameOverride | default .Chart.Name }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index cdcc03e1c..8a05b1a11 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -9,6 +9,7 @@ spec: selector: matchLabels: {{- include "binderhub-service.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: binderhub template: metadata: annotations: @@ -18,6 +19,7 @@ spec: {{- end }} labels: {{- include "binderhub-service.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: binderhub spec: volumes: - name: secret diff --git a/binderhub-service/templates/docker-api/daemonset.yaml b/binderhub-service/templates/docker-api/daemonset.yaml new file mode 100644 index 000000000..f0c7f46bf --- /dev/null +++ b/binderhub-service/templates/docker-api/daemonset.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "binderhub-service.fullname" . }}-docker-api + labels: + {{- include "binderhub-service.labels" . | nindent 4 }} + app.kubernetes.io/component: docker-api +spec: + selector: + matchLabels: + {{- include "binderhub-service.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: docker-api + template: + metadata: + labels: + {{- include "binderhub-service.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: docker-api + {{- with .Values.podAnnotations }} + annotations: + {{- . | toYaml | nindent 8 }} + {{- end }} + spec: + containers: + - name: docker-api + image: {{ .Values.dockerApi.image.repository }}:{{ .Values.dockerApi.image.tag }} + args: + - dockerd + - --data-root=/var/lib/docker-api + - --exec-root=/var/run/docker-api + - --host=unix://var/run/docker-api/docker-api.sock + volumeMounts: + - name: data + mountPath: /var/lib/docker-api + - name: exec + mountPath: /var/run/docker-api + resources: + {{- .Values.dockerApi.resources | toYaml | nindent 12 }} + securityContext: + {{- .Values.dockerApi.securityContext | toYaml | nindent 12 }} + volumes: + - name: data + hostPath: + path: /var/lib/docker-api + type: DirectoryOrCreate + - name: exec + hostPath: + path: /var/run/docker-api + type: DirectoryOrCreate + {{- with .Values.dockerApi.image.pullSecrets }} + imagePullSecrets: + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.dockerApi.podSecurityContext }} + securityContext: + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.dockerApi.nodeSelector }} + nodeSelector: + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.dockerApi.affinity }} + affinity: + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.dockerApi.tolerations }} + tolerations: + {{- . | toYaml | nindent 8 }} + {{- end }} diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 71390bb1c..4656eb77e 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -41,7 +41,6 @@ properties: # Deployment resource # --------------------------------------------------------------------------- # - config: type: object additionalProperties: false @@ -58,7 +57,7 @@ properties: replicas: type: integer - image: + image: &image type: object additionalProperties: false required: [repository, tag] @@ -71,13 +70,13 @@ properties: enum: [null, "", IfNotPresent, Always, Never] pullSecrets: type: array - resources: + resources: &resources type: object additionalProperties: true - securityContext: + securityContext: &securityContext type: object additionalProperties: true - podSecurityContext: + podSecurityContext: &podSecurityContext type: object additionalProperties: true podAnnotations: &labels-and-annotations @@ -86,14 +85,14 @@ properties: patternProperties: ".*": type: string - nodeSelector: + nodeSelector: &nodeSelector type: object additionalProperties: true - tolerations: - type: array - affinity: + affinity: &affinity type: object additionalProperties: true + tolerations: &tolerations + type: array # RBAC resources # --------------------------------------------------------------------------- @@ -155,3 +154,20 @@ properties: tls: type: array annotations: *labels-and-annotations + + # DaemonSet resource - docker-api + # --------------------------------------------------------------------------- + # + dockerApi: + type: object + additionalProperties: false + required: [image] + properties: + image: *image + resources: *resources + securityContext: *securityContext + podSecurityContext: *podSecurityContext + podAnnotations: *labels-and-annotations + nodeSelector: *nodeSelector + affinity: *affinity + tolerations: *tolerations diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index d775130e7..8d36748ac 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -22,7 +22,8 @@ config: BinderHub: base_url: / use_registry: true - KubernetesBuildExecutor: {} + KubernetesBuildExecutor: + docker_host: /var/run/docker-api/docker-api.sock extraConfig: {} replicas: 1 @@ -31,6 +32,7 @@ image: tag: "set-by-chartpress" pullPolicy: "" pullSecrets: [] +resources: {} securityContext: capabilities: drop: @@ -38,12 +40,12 @@ securityContext: readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 1000 -resources: {} -podAnnotations: {} + podSecurityContext: {} +podAnnotations: {} nodeSelector: {} -tolerations: [] affinity: {} +tolerations: [] # RBAC resources # ----------------------------------------------------------------------------- @@ -79,3 +81,27 @@ ingress: - path: / pathType: ImplementationSpecific tls: [] + +# DaemonSet resource - docker-api +# ----------------------------------------------------------------------------- +# +# This DaemonSet starts a pod on each node to setup a Docker API that +# binderhub's spawned build pods can make use of, via a hostPath volume that +# exposes a unix socket. +# +dockerApi: + image: + repository: docker.io/library/docker + tag: "23.0.4-dind" # ref: https://hub.docker.com/_/docker/tags + pullPolicy: "" + pullSecrets: [] + resources: {} + securityContext: + privileged: true + runAsUser: 0 + + podSecurityContext: {} + podAnnotations: {} + nodeSelector: {} + affinity: {} + tolerations: [] diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml index f8ade6620..744cbaa25 100644 --- a/tools/templates/lint-and-validate-values.yaml +++ b/tools/templates/lint-and-validate-values.yaml @@ -37,3 +37,14 @@ service: # ingress: enabled: false + +# DaemonSet resource - docker-api +# ----------------------------------------------------------------------------- +# +dockerApi: + image: + repository: docker.io/library/docker + tag: "23.0.4-dind" + securityContext: + privileged: true + runAsUser: 0 From 111da9722c234953968dd1d809cc0eadc8649e45 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Apr 2023 09:06:23 +0200 Subject: [PATCH 021/200] ci: add automation to bump image dependencies --- .github/workflows/watch-dependencies.yaml | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/.github/workflows/watch-dependencies.yaml b/.github/workflows/watch-dependencies.yaml index 7bbab3bda..817afa478 100644 --- a/.github/workflows/watch-dependencies.yaml +++ b/.github/workflows/watch-dependencies.yaml @@ -4,11 +4,16 @@ # This Workflow watches dependencies and automatically creates PRs to update # them. # +# - Watch multiple images tags referenced in values.yaml to match the latest +# stable image tag (ignoring pre-releases). # - Refreeze images/*/requirements.txt based on images/*/requirements.in # name: Watch dependencies on: + pull_request: + paths: + - ".github/workflows/watch-dependencies.yaml" push: paths: - "images/*/requirements.in" @@ -20,6 +25,65 @@ on: workflow_dispatch: jobs: + update-image-dependencies: + if: github.repository == '2i2c-org/binderhub-service' + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + include: + - name: docker + registry: docker.io + repository: library/docker + values_path: dockerApi.image.tag + tag_prefix: "" + tag_suffix: -dind + + steps: + - uses: actions/checkout@v3 + + - name: Get values.yaml pinned tag of ${{ matrix.registry }}/${{ matrix.repository }} + id: local + run: | + local_tag=$(cat binderhub-service/values.yaml | yq e '.${{ matrix.values_path }}' -) + echo "tag=$local_tag" >> $GITHUB_OUTPUT + + - name: Get latest tag of ${{ matrix.registry }}/${{ matrix.repository }} + id: latest + # The skopeo image helps us list tags consistently from different docker + # registries. We identify the latest docker image tag based on the + # version numbers of format x.y.z included in a pattern with an optional + # prefix and suffix, like the tags "v4.5.0" (v prefix) and "23.0.4-dind" + # (-dind suffix). + run: | + latest_tag=$( + docker run --rm quay.io/skopeo/stable list-tags docker://${{ matrix.registry }}/${{ matrix.repository }} \ + | jq -r '[.Tags[] | select(. | match("^${{ matrix.tag_prefix }}\\d+\\.\\d+\\.\\d+${{ matrix.tag_suffix }}$") | .string)] | sort_by(split(".") | map(ltrimstr("${{ matrix.tag_prefix }}") | rtrimstr("${{ matrix.tag_suffix }}") | tonumber)) | last' + ) + echo "tag=$latest_tag" >> $GITHUB_OUTPUT + + - name: Update values.yaml pinned tag + if: steps.local.outputs.tag != steps.latest.outputs.tag + run: | + sed --in-place 's/tag: "${{ steps.local.outputs.tag }}"/tag: "${{ steps.latest.outputs.tag }}"/g' binderhub-service/values.yaml + + - name: git diff + if: steps.local.outputs.tag != steps.latest.outputs.tag + run: git --no-pager diff --color=always + + # ref: https://github.com/peter-evans/create-pull-request + - uses: peter-evans/create-pull-request@v4 + if: github.event_name != 'pull_request' + with: + branch: update-image-dependencies + labels: dependencies + commit-message: Update ${{ matrix.repository }} version from ${{ steps.local.outputs.tag }} to ${{ steps.latest.outputs.tag }} + title: Update ${{ matrix.repository }} version from ${{ steps.local.outputs.tag }} to ${{ steps.latest.outputs.tag }} + body: >- + A new ${{ matrix.repository }} image version has been detected, version + `${{ steps.latest.outputs.tag }}`. + refreeze-dockerfile-requirements-txt: if: github.repository == '2i2c-org/binderhub-service' runs-on: ubuntu-22.04 @@ -38,6 +102,7 @@ jobs: # ref: https://github.com/peter-evans/create-pull-request - uses: peter-evans/create-pull-request@v4 + if: github.event_name != 'pull_request' with: branch: update-image-requirements labels: dependencies From 40ab9860694a5a26118fe1a55a4817ec0ed834e4 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Apr 2023 09:13:25 +0200 Subject: [PATCH 022/200] ci: add new daemonset as important workload --- .github/workflows/test-chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index d16017139..554fc22e4 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -165,4 +165,4 @@ jobs: - uses: jupyterhub/action-k8s-namespace-report@v1 if: always() with: - important-workloads: deploy/binderhub-service + important-workloads: deploy/binderhub-service daemonset/binderhub-service-docker-api From 72f969f464627862468025d9064018222a221464 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Apr 2023 09:13:55 +0200 Subject: [PATCH 023/200] Update manually to use latest docker-api image --- .github/workflows/watch-dependencies.yaml | 2 +- binderhub-service/values.yaml | 2 +- tools/templates/lint-and-validate-values.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/watch-dependencies.yaml b/.github/workflows/watch-dependencies.yaml index 817afa478..d7b9b8cd1 100644 --- a/.github/workflows/watch-dependencies.yaml +++ b/.github/workflows/watch-dependencies.yaml @@ -54,7 +54,7 @@ jobs: # The skopeo image helps us list tags consistently from different docker # registries. We identify the latest docker image tag based on the # version numbers of format x.y.z included in a pattern with an optional - # prefix and suffix, like the tags "v4.5.0" (v prefix) and "23.0.4-dind" + # prefix and suffix, like the tags "v4.5.0" (v prefix) and "23.0.5-dind" # (-dind suffix). run: | latest_tag=$( diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 8d36748ac..f909ead53 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -92,7 +92,7 @@ ingress: dockerApi: image: repository: docker.io/library/docker - tag: "23.0.4-dind" # ref: https://hub.docker.com/_/docker/tags + tag: "23.0.5-dind" # ref: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml index 744cbaa25..5ee72538a 100644 --- a/tools/templates/lint-and-validate-values.yaml +++ b/tools/templates/lint-and-validate-values.yaml @@ -44,7 +44,7 @@ ingress: dockerApi: image: repository: docker.io/library/docker - tag: "23.0.4-dind" + tag: "23.0.5-dind" securityContext: privileged: true runAsUser: 0 From 11d0ffa24507d3f944dea83be17a2e062632a0b4 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Apr 2023 12:52:01 +0200 Subject: [PATCH 024/200] maint: don't use low-numbered container port for binderhub --- binderhub-service/templates/deployment.yaml | 2 +- binderhub-service/values.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index 8a05b1a11..af93367f3 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -33,7 +33,7 @@ spec: {{- end }} ports: - name: http - containerPort: {{ .Values.service.port }} + containerPort: {{ .Values.config.BinderHub.port }} volumeMounts: - name: secret mountPath: /etc/binderhub/mounted-secret/ diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index f909ead53..56b3f83c6 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -21,6 +21,7 @@ global: {} config: BinderHub: base_url: / + port: 8585 use_registry: true KubernetesBuildExecutor: docker_host: /var/run/docker-api/docker-api.sock From d5a0abe498388cdf072f7e335756ac2f95c21d38 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Apr 2023 12:52:32 +0200 Subject: [PATCH 025/200] maint: set docker-api path from file-system root --- binderhub-service/templates/docker-api/daemonset.yaml | 2 +- binderhub-service/values.yaml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/binderhub-service/templates/docker-api/daemonset.yaml b/binderhub-service/templates/docker-api/daemonset.yaml index f0c7f46bf..92811d88f 100644 --- a/binderhub-service/templates/docker-api/daemonset.yaml +++ b/binderhub-service/templates/docker-api/daemonset.yaml @@ -27,7 +27,7 @@ spec: - dockerd - --data-root=/var/lib/docker-api - --exec-root=/var/run/docker-api - - --host=unix://var/run/docker-api/docker-api.sock + - --host=unix:///var/run/docker-api/docker-api.sock volumeMounts: - name: data mountPath: /var/lib/docker-api diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 56b3f83c6..ef64fc7d6 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -24,6 +24,8 @@ config: port: 8585 use_registry: true KubernetesBuildExecutor: + # docker_host must not be updated, assumptions about it are hardcoded in + # docker-api/daemonset.yaml docker_host: /var/run/docker-api/docker-api.sock extraConfig: {} From ed8a7c7255ed8e6f615b43c9ab1814c2362e9d13 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Apr 2023 12:53:26 +0200 Subject: [PATCH 026/200] Add comment about config that both software and chart consumes --- binderhub-service/values.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index ef64fc7d6..94e5549d4 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -18,6 +18,12 @@ global: {} # config.X.y sets c.X.y where X is a class and y is a configurable traitlet on # the class. # +# Some config must be set here, and not via extraConfig, as its referenced by +# the chart's template directly. +# +# - BinderHub.base_url (readinessProbe) +# - BinderHub.port (containerPort) +# config: BinderHub: base_url: / From 34e010311a9fa9b293f90220d1fc09998432d796 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Apr 2023 12:54:12 +0200 Subject: [PATCH 027/200] Set BinderHub.require_build_only=true by default --- binderhub-service/values.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 94e5549d4..e650747ff 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -29,6 +29,7 @@ config: base_url: / port: 8585 use_registry: true + require_build_only: true KubernetesBuildExecutor: # docker_host must not be updated, assumptions about it are hardcoded in # docker-api/daemonset.yaml From 8b6f8fcc0876f5cb001b6954495082f84197c5c1 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 27 Apr 2023 13:09:38 +0200 Subject: [PATCH 028/200] Add startupProbe for binderhub-service pod --- binderhub-service/templates/deployment.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index af93367f3..15c7962b6 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -42,6 +42,12 @@ spec: {{- .Values.resources | toYaml | nindent 12 }} securityContext: {{- .Values.securityContext | toYaml | nindent 12 }} + startupProbe: + periodSeconds: 1 + failureThreshold: 60 + httpGet: + path: {{ .Values.config.BinderHub.base_url }}/versions + port: http {{- with .Values.image.pullSecrets }} imagePullSecrets: {{- . | toYaml | nindent 8 }} From 16156606db80c2e50115fcabcc2e9bf86606b5a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 05:59:10 +0000 Subject: [PATCH 029/200] build(deps): bump peter-evans/create-pull-request from 4 to 5 Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4 to 5. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v4...v5) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/watch-dependencies.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/watch-dependencies.yaml b/.github/workflows/watch-dependencies.yaml index d7b9b8cd1..ce303b61a 100644 --- a/.github/workflows/watch-dependencies.yaml +++ b/.github/workflows/watch-dependencies.yaml @@ -73,7 +73,7 @@ jobs: run: git --no-pager diff --color=always # ref: https://github.com/peter-evans/create-pull-request - - uses: peter-evans/create-pull-request@v4 + - uses: peter-evans/create-pull-request@v5 if: github.event_name != 'pull_request' with: branch: update-image-dependencies @@ -101,7 +101,7 @@ jobs: run: git --no-pager diff --color=always # ref: https://github.com/peter-evans/create-pull-request - - uses: peter-evans/create-pull-request@v4 + - uses: peter-evans/create-pull-request@v5 if: github.event_name != 'pull_request' with: branch: update-image-requirements From 78cf1774277dfd0aa537fd9b26deefa5b3344abe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 22:53:45 +0000 Subject: [PATCH 030/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2) - [github.com/PyCQA/autoflake: v2.0.2 → v2.1.1](https://github.com/PyCQA/autoflake/compare/v2.0.2...v2.1.1) - [github.com/pre-commit/mirrors-prettier: v3.0.0-alpha.6 → v3.0.0-alpha.9-for-vscode](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.0-alpha.6...v3.0.0-alpha.9-for-vscode) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ebd8ecad..529666600 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.3.2 hooks: - id: pyupgrade args: @@ -19,7 +19,7 @@ repos: # Autoformat: Python code - repo: https://github.com/PyCQA/autoflake - rev: v2.0.2 + rev: v2.1.1 hooks: - id: autoflake args: @@ -44,7 +44,7 @@ repos: # Autoformat: markdown, yaml (but not helm templates) - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.6 + rev: v3.0.0-alpha.9-for-vscode hooks: - id: prettier exclude: binderhub-service/templates/.* From b38d2592d54d05f7f0ac727abbe7ed24e42acb58 Mon Sep 17 00:00:00 2001 From: consideRatio Date: Mon, 22 May 2023 15:51:21 +0000 Subject: [PATCH 031/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index a9cb2f98e..a66abbee9 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,7 +4,7 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -alembic==1.10.4 +alembic==1.11.1 # via jupyterhub async-generator==1.10 # via jupyterhub @@ -14,7 +14,7 @@ binderhub @ git+https://github.com/consideratio/binderhub@opt-out-of-launch # via -r requirements.in cachetools==5.3.0 # via google-auth -certifi==2022.12.7 +certifi==2023.5.7 # via # kubernetes # requests @@ -26,11 +26,11 @@ charset-normalizer==3.1.0 # via requests cryptography==40.0.2 # via pyopenssl -docker==6.0.1 +docker==6.1.2 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.17.3 +google-auth==2.18.1 # via kubernetes greenlet==2.0.2 # via sqlalchemy @@ -80,7 +80,7 @@ pycparser==2.21 # via cffi pycurl==7.45.2 # via binderhub -pyjwt==2.6.0 +pyjwt==2.7.0 # via binderhub pyopenssl==23.1.1 # via certipy @@ -96,7 +96,7 @@ python-json-logger==2.0.7 # jupyter-telemetry pyyaml==6.0 # via kubernetes -requests==2.29.0 +requests==2.31.0 # via # docker # jupyterhub @@ -106,18 +106,20 @@ requests-oauthlib==1.3.1 # via kubernetes rsa==4.9 # via google-auth -ruamel-yaml==0.17.21 +ruamel-yaml==0.17.26 # via jupyter-telemetry +ruamel-yaml-clib==0.2.7 + # via ruamel-yaml six==1.16.0 # via # google-auth # kubernetes # python-dateutil -sqlalchemy==2.0.11 +sqlalchemy==2.0.15 # via # alembic # jupyterhub -tornado==6.3.1 +tornado==6.3.2 # via # binderhub # jupyterhub @@ -133,9 +135,10 @@ typing-extensions==4.5.0 urllib3==1.26.15 # via # docker + # google-auth # kubernetes # requests -websocket-client==1.5.1 +websocket-client==1.5.2 # via # docker # kubernetes From 4bbe92c1c148d285f66bbe59870d4ded525ab01b Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 1 Jun 2023 07:08:07 +0200 Subject: [PATCH 032/200] ci: add missing permissions for dependency bumper automation --- .github/workflows/watch-dependencies.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/watch-dependencies.yaml b/.github/workflows/watch-dependencies.yaml index ce303b61a..47edf8245 100644 --- a/.github/workflows/watch-dependencies.yaml +++ b/.github/workflows/watch-dependencies.yaml @@ -28,6 +28,9 @@ jobs: update-image-dependencies: if: github.repository == '2i2c-org/binderhub-service' runs-on: ubuntu-22.04 + permissions: + contents: write + pull-requests: write strategy: fail-fast: false From fe5d4bd29e1bf8f063ce9723432b7461a20f474a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 22:29:34 +0000 Subject: [PATCH 033/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.2 → v3.4.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.4.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 529666600..4ca3d9cf9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v3.3.2 + rev: v3.4.0 hooks: - id: pyupgrade args: From d2036ffafabbc766dd391d2c4046a3a530540a87 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Mon, 26 Jun 2023 15:52:06 +0300 Subject: [PATCH 034/200] Add enabling flag in schema --- binderhub-service/values.schema.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 4656eb77e..cedf26d2b 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -27,6 +27,14 @@ required: - service - ingress properties: + # Flag to conditionally install the chart + # --------------------------------------------------------------------------- + # + enabled: + type: boolean + description: | + Configuration flag for charts depending on binderhub-service to toggle installing it. + # General configuration # --------------------------------------------------------------------------- # From d11346c6ca0aceeff1a5f12110a5dfac874bed8b Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 26 Jun 2023 13:48:21 +0000 Subject: [PATCH 035/200] Update library/docker version from 23.0.5-dind to 24.0.2-dind --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index e650747ff..897c31a59 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -102,7 +102,7 @@ ingress: dockerApi: image: repository: docker.io/library/docker - tag: "23.0.5-dind" # ref: https://hub.docker.com/_/docker/tags + tag: "24.0.2-dind" # ref: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From 2cc22bd2b7259e588ec2bb31cb3ce6645a211f7f Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 26 Jun 2023 13:48:32 +0000 Subject: [PATCH 036/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index a66abbee9..4b8fcf91f 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -12,7 +12,7 @@ attrs==23.1.0 # via jsonschema binderhub @ git+https://github.com/consideratio/binderhub@opt-out-of-launch # via -r requirements.in -cachetools==5.3.0 +cachetools==5.3.1 # via google-auth certifi==2023.5.7 # via @@ -24,13 +24,13 @@ cffi==1.15.1 # via cryptography charset-normalizer==3.1.0 # via requests -cryptography==40.0.2 +cryptography==41.0.1 # via pyopenssl -docker==6.1.2 +docker==6.1.3 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.18.1 +google-auth==2.20.0 # via kubernetes greenlet==2.0.2 # via sqlalchemy @@ -46,13 +46,13 @@ jsonschema==4.17.3 # jupyter-telemetry jupyter-telemetry==0.1.0 # via jupyterhub -jupyterhub==4.0.0 +jupyterhub==4.0.1 # via binderhub kubernetes==26.1.0 # via binderhub mako==1.2.4 # via alembic -markupsafe==2.1.2 +markupsafe==2.1.3 # via # jinja2 # mako @@ -64,9 +64,9 @@ packaging==23.1 # via # docker # jupyterhub -pamela==1.0.0 +pamela==1.1.0 # via jupyterhub -prometheus-client==0.16.0 +prometheus-client==0.17.0 # via # binderhub # jupyterhub @@ -82,7 +82,7 @@ pycurl==7.45.2 # via binderhub pyjwt==2.7.0 # via binderhub -pyopenssl==23.1.1 +pyopenssl==23.2.0 # via certipy pyrsistent==0.19.3 # via jsonschema @@ -106,7 +106,7 @@ requests-oauthlib==1.3.1 # via kubernetes rsa==4.9 # via google-auth -ruamel-yaml==0.17.26 +ruamel-yaml==0.17.32 # via jupyter-telemetry ruamel-yaml-clib==0.2.7 # via ruamel-yaml @@ -115,7 +115,7 @@ six==1.16.0 # google-auth # kubernetes # python-dateutil -sqlalchemy==2.0.15 +sqlalchemy==2.0.17 # via # alembic # jupyterhub @@ -128,17 +128,17 @@ traitlets==5.9.0 # binderhub # jupyter-telemetry # jupyterhub -typing-extensions==4.5.0 +typing-extensions==4.6.3 # via # alembic # sqlalchemy -urllib3==1.26.15 +urllib3==1.26.16 # via # docker # google-auth # kubernetes # requests -websocket-client==1.5.2 +websocket-client==1.6.1 # via # docker # kubernetes From 35212657b1bae02056554adbf83abb7bb72d7bb4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jul 2023 23:32:18 +0000 Subject: [PATCH 037/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.4.0 → v3.8.0](https://github.com/asottile/pyupgrade/compare/v3.4.0...v3.8.0) - [github.com/PyCQA/autoflake: v2.1.1 → v2.2.0](https://github.com/PyCQA/autoflake/compare/v2.1.1...v2.2.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ca3d9cf9..5085ff08f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.8.0 hooks: - id: pyupgrade args: @@ -19,7 +19,7 @@ repos: # Autoformat: Python code - repo: https://github.com/PyCQA/autoflake - rev: v2.1.1 + rev: v2.2.0 hooks: - id: autoflake args: From aa7f779cb5cb84141eac990b8877b860b8f367c9 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Fri, 7 Jul 2023 07:45:32 +0000 Subject: [PATCH 038/200] Update library/docker version from 24.0.2-dind to 24.0.3-dind --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 897c31a59..9d479d91c 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -102,7 +102,7 @@ ingress: dockerApi: image: repository: docker.io/library/docker - tag: "24.0.2-dind" # ref: https://hub.docker.com/_/docker/tags + tag: "24.0.3-dind" # ref: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From f96343be2b3f9545ef6e838746e8faa929e3efea Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Fri, 7 Jul 2023 07:45:50 +0000 Subject: [PATCH 039/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 4b8fcf91f..897c89ac9 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -9,7 +9,9 @@ alembic==1.11.1 async-generator==1.10 # via jupyterhub attrs==23.1.0 - # via jsonschema + # via + # jsonschema + # referencing binderhub @ git+https://github.com/consideratio/binderhub@opt-out-of-launch # via -r requirements.in cachetools==5.3.1 @@ -30,7 +32,7 @@ docker==6.1.3 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.20.0 +google-auth==2.21.0 # via kubernetes greenlet==2.0.2 # via sqlalchemy @@ -40,10 +42,12 @@ jinja2==3.1.2 # via # binderhub # jupyterhub -jsonschema==4.17.3 +jsonschema==4.18.0 # via # binderhub # jupyter-telemetry +jsonschema-specifications==2023.6.1 + # via jsonschema jupyter-telemetry==0.1.0 # via jupyterhub jupyterhub==4.0.1 @@ -84,8 +88,6 @@ pyjwt==2.7.0 # via binderhub pyopenssl==23.2.0 # via certipy -pyrsistent==0.19.3 - # via jsonschema python-dateutil==2.8.2 # via # jupyterhub @@ -96,6 +98,10 @@ python-json-logger==2.0.7 # jupyter-telemetry pyyaml==6.0 # via kubernetes +referencing==0.29.1 + # via + # jsonschema + # jsonschema-specifications requests==2.31.0 # via # docker @@ -104,6 +110,10 @@ requests==2.31.0 # requests-oauthlib requests-oauthlib==1.3.1 # via kubernetes +rpds-py==0.8.8 + # via + # jsonschema + # referencing rsa==4.9 # via google-auth ruamel-yaml==0.17.32 @@ -115,7 +125,7 @@ six==1.16.0 # google-auth # kubernetes # python-dateutil -sqlalchemy==2.0.17 +sqlalchemy==2.0.18 # via # alembic # jupyterhub @@ -128,7 +138,7 @@ traitlets==5.9.0 # binderhub # jupyter-telemetry # jupyterhub -typing-extensions==4.6.3 +typing-extensions==4.7.1 # via # alembic # sqlalchemy From 27aef425147796f89241cc2781fce88a762c4f6e Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Fri, 7 Jul 2023 11:13:12 +0300 Subject: [PATCH 040/200] Trim suffix --- binderhub-service/templates/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index 15c7962b6..5dc5b9bbc 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -46,7 +46,7 @@ spec: periodSeconds: 1 failureThreshold: 60 httpGet: - path: {{ .Values.config.BinderHub.base_url }}/versions + path: {{ .Values.config.BinderHub.base_url | trimSuffix "/" }}/versions port: http {{- with .Values.image.pullSecrets }} imagePullSecrets: From 80906fd65a4ba96282541f4756f376d08da41dc3 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 28 Apr 2023 22:40:17 +0200 Subject: [PATCH 041/200] Add ability to pass registry credentials to build pods' docker clients --- .gitignore | 5 +- .../build-pods-docker-config/secret.yaml | 50 +++++++++++++++++++ binderhub-service/templates/deployment.yaml | 3 ++ binderhub-service/values.schema.yaml | 23 +++++++++ binderhub-service/values.yaml | 18 ++++++- dev-config.yaml | 11 ++++ tools/templates/lint-and-validate-values.yaml | 10 ++++ 7 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 binderhub-service/templates/build-pods-docker-config/secret.yaml diff --git a/.gitignore b/.gitignore index 04acaba4d..46e573bec 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,10 @@ binderhub-service/values.schema.json tools/templates/rendered-templates/ +# convenience for storing production config in the repo while developing +prod-config.yaml -### Other misc things - +# Other misc things .vscode *.DS_Store diff --git a/binderhub-service/templates/build-pods-docker-config/secret.yaml b/binderhub-service/templates/build-pods-docker-config/secret.yaml new file mode 100644 index 000000000..1a91b29e1 --- /dev/null +++ b/binderhub-service/templates/build-pods-docker-config/secret.yaml @@ -0,0 +1,50 @@ +# This Secret is mounted by BinderHub's managed build pods because +# c.KubernetesBuildExecutor.push_secret is configured with this Secret's name. +# +# IMPORTANT: This is _not_ a Kubernetes imagePullSecrets formatted Secret, it +# instead provides a config file for a docker client. +# +kind: Secret +apiVersion: v1 +metadata: + name: {{ include "binderhub-service.fullname" . }}-build-pods-docker-config + labels: + {{- include "binderhub-service.labels" . | nindent 4 }} +type: Opaque +stringData: + # config.json refers to docker config that should house credentials for the + # docker client in a build pod to use against the docker-api. + # + # Docker's config.json expects something like below, where the xx...xx= string + # is ":" base64 encoded. + # + # { + # "auths": { + # "https://index.docker.io/v1/": { + # "auth": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=" + # } + # } + # } + # + # ref: https://github.com/jupyterhub/binderhub/blob/79c5f61a992010f108637e3c434d9e606a3c8f72/binderhub/build.py#L397-L406 + # + {{- /* initialize a dict to represent a docker client config */}} + {{- $dockerConfig := dict }} + + {{- $server := .Values.buildPodsRegistryCredentials.server }} + {{- $username := .Values.buildPodsRegistryCredentials.username }} + {{- $password := .Values.buildPodsRegistryCredentials.password }} + {{- $blob := printf "%s:%s" $username $password | b64enc }} + {{- $credentials := dict "auths" (dict $server (dict "auth" $blob)) }} + + {{- /* merge docker client config with registry credentials */}} + {{- if .Values.config.BinderHub.use_registry }} + {{- $dockerConfig = merge $dockerConfig $credentials }} + {{- end }} + + {{- /* merge docker client config of any kind */}} + {{- if .Values.buildPodsDockerConfig }} + {{- $dockerConfig = merge $dockerConfig .Values.buildPodsDockerConfig }} + {{- end }} + config.json: | + {{- $dockerConfig | toPrettyJson | nindent 4 }} diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index 5dc5b9bbc..b0b80b095 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -38,6 +38,9 @@ spec: - name: secret mountPath: /etc/binderhub/mounted-secret/ readOnly: true + env: + - name: HELM_RELEASE_NAME + value: {{ .Release.Name }} resources: {{- .Values.resources | toYaml | nindent 12 }} securityContext: diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index cedf26d2b..83b0fe8eb 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -19,6 +19,8 @@ required: - nameOverride - fullnameOverride - global + # Resources for the BinderHub created build pods + - buildPodsRegistryCredentials # Deployment resource - image # Other resources @@ -46,6 +48,27 @@ properties: type: object additionalProperties: true + # Resources for the BinderHub created build pods + # --------------------------------------------------------------------------- + # + buildPodsDockerConfig: + type: object + additionalProperties: true + buildPodsRegistryCredentials: + type: object + additionalProperties: false + required: + - server + - username + - password + properties: + server: + type: string + username: + type: string + password: + type: string + # Deployment resource # --------------------------------------------------------------------------- # diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 9d479d91c..fb71701f4 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -5,6 +5,15 @@ nameOverride: "" fullnameOverride: "" global: {} +# Resources for the BinderHub created build pods +# ----------------------------------------------------------------------------- +# +buildPodsDockerConfig: {} +buildPodsRegistryCredentials: + server: "" + username: "" + password: "" + # Deployment resource # ----------------------------------------------------------------------------- # @@ -28,13 +37,18 @@ config: BinderHub: base_url: / port: 8585 - use_registry: true require_build_only: true KubernetesBuildExecutor: # docker_host must not be updated, assumptions about it are hardcoded in # docker-api/daemonset.yaml docker_host: /var/run/docker-api/docker-api.sock -extraConfig: {} +extraConfig: + # binderhub-service creates a k8s Secret with a docker config.json file + # including registry credentials. + binderhub_service_00_build_pods_docker_config: | + import os + helm_release_name = os.environ["HELM_RELEASE_NAME"] + c.KubernetesBuildExecutor.push_secret = f"{helm_release_name}-build-pods-docker-config" replicas: 1 image: diff --git a/dev-config.yaml b/dev-config.yaml index e69de29bb..ff96aca63 100644 --- a/dev-config.yaml +++ b/dev-config.yaml @@ -0,0 +1,11 @@ +# FIXME: When running tests, we will need a local container registry to test +# pushing images to that can be reached from the build pods. +# +config: + BinderHub: + use_registry: false + image_prefix: localhost/binderhub-service/ +buildPodsRegistryCredentials: + server: "localhost" + username: "dummy-username" + password: "dummy-password" diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml index 5ee72538a..90013655d 100644 --- a/tools/templates/lint-and-validate-values.yaml +++ b/tools/templates/lint-and-validate-values.yaml @@ -5,6 +5,16 @@ nameOverride: "" fullnameOverride: "" global: {} +# Resources for the BinderHub created build pods +# ----------------------------------------------------------------------------- +# +buildPodsDockerConfig: + dummy: dummy-value +buildPodsRegistryCredentials: + server: "quay.io" + username: "dummy-username" + password: "dummy-password" + # Deployment resource # ----------------------------------------------------------------------------- # From 5946aca13f9fbc1683543a87df95bbd0b2c4e9d7 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 5 Jun 2023 13:56:15 +0200 Subject: [PATCH 042/200] Set log_level to DEBUG --- dev-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-config.yaml b/dev-config.yaml index ff96aca63..4aa8260c3 100644 --- a/dev-config.yaml +++ b/dev-config.yaml @@ -3,6 +3,7 @@ # config: BinderHub: + log_level: DEBUG use_registry: false image_prefix: localhost/binderhub-service/ buildPodsRegistryCredentials: From 3e251fa69a09171eb7d94565e54012dcbde5d3d7 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Sat, 8 Jul 2023 18:34:31 +0200 Subject: [PATCH 043/200] ci: start and reference a local container registry --- .github/workflows/test-chart.yaml | 35 ++++++++++++++++++++++++++++--- dev-config.yaml | 14 ++++++------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index 554fc22e4..c94c1026f 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -94,6 +94,14 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 20 + # Start a local container registry that the binderhub build pods can push + # to, and the k8s cluster can pull images from for use by pods. + services: + registry: + image: docker.io/library/registry:latest + ports: + - 5000:5000 + strategy: fail-fast: false matrix: @@ -107,6 +115,7 @@ jobs: include: - k3s-channel: latest - k3s-channel: stable + - k3s-channel: v1.24 steps: - uses: actions/checkout@v3 @@ -114,6 +123,23 @@ jobs: # chartpress needs git history fetch-depth: 0 + - name: Determine how to reference local container registry + run: | + LOCAL_REGISTRY_HOST=$(hostname --all-ip-addresses | awk '{print $1}'):5000 + echo LOCAL_REGISTRY_HOST="$LOCAL_REGISTRY_HOST" >> $GITHUB_ENV + + - name: Configure k3s to pull from local container registry + run: | + # Allow k3s to pull from private registry + # https://docs.k3s.io/installation/private-registry + sudo mkdir -p /etc/rancher/k3s/ + cat << EOF | sudo tee /etc/rancher/k3s/registries.yaml + mirrors: + "$LOCAL_REGISTRY_HOST": + endpoint: + - "http://$LOCAL_REGISTRY_HOST" + EOF + # Starts a k8s cluster with NetworkPolicy enforcement and installs both # kubectl and helm # @@ -128,11 +154,12 @@ jobs: - uses: actions/setup-python@v4 with: python-version: "3.11" - - name: Install dependencies run: | pip install -r dev-requirements.txt - pip list + - name: List dependencies + run: | + pip freeze # Build our images if needed and update Chart.yaml and values.yaml with # version and tags @@ -153,7 +180,9 @@ jobs: - name: Install local chart run: | helm upgrade --install binderhub-service ./binderhub-service \ - --values dev-config.yaml + --values dev-config.yaml \ + --set config.BinderHub.image_prefix=$LOCAL_REGISTRY_HOST/binderhub-service/ \ + --set buildPodsRegistryCredentials.server=http://$LOCAL_REGISTRY_HOST # ref: https://github.com/jupyterhub/action-k8s-await-workloads - uses: jupyterhub/action-k8s-await-workloads@v2 diff --git a/dev-config.yaml b/dev-config.yaml index 4aa8260c3..6baa21f53 100644 --- a/dev-config.yaml +++ b/dev-config.yaml @@ -1,12 +1,12 @@ -# FIXME: When running tests, we will need a local container registry to test -# pushing images to that can be reached from the build pods. +# When running tests, we use a local container registry to test pushing images +# to that can be reached from the build pods. # config: BinderHub: log_level: DEBUG - use_registry: false - image_prefix: localhost/binderhub-service/ + use_registry: true + image_prefix: localhost:5000/binderhub-service/ buildPodsRegistryCredentials: - server: "localhost" - username: "dummy-username" - password: "dummy-password" + server: "http://localhost:5000" + username: "" + password: "" From 17ae581f17d8b9ae1fb9fc157bdfa4992508980c Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Sat, 8 Jul 2023 18:52:41 +0200 Subject: [PATCH 044/200] ci: test build and push of image using binderhub REST API and curl --- .github/workflows/test-chart.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index c94c1026f..cb67faca0 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -190,6 +190,11 @@ jobs: timeout: 150 max-restarts: 1 + - name: Test image build/push via binderhub REST API using curl + run: | + kubectl port-forward deploy/binderhub-service 8585 & + curl http://localhost:8585/build/gh/binderhub-ci-repos/cached-minimal-dockerfile/HEAD?build_only=true + # ref: https://github.com/jupyterhub/action-k8s-namespace-report - uses: jupyterhub/action-k8s-namespace-report@v1 if: always() From 6863faf34d4e4070cd99c00804ab999e4fe59aa1 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Sat, 8 Jul 2023 19:50:46 +0200 Subject: [PATCH 045/200] Define service.nodePort for use when testing --- .github/workflows/test-chart.yaml | 3 +-- binderhub-service/templates/service.yaml | 3 +++ binderhub-service/values.schema.yaml | 2 ++ dev-config.yaml | 4 ++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index cb67faca0..9e3108339 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -192,8 +192,7 @@ jobs: - name: Test image build/push via binderhub REST API using curl run: | - kubectl port-forward deploy/binderhub-service 8585 & - curl http://localhost:8585/build/gh/binderhub-ci-repos/cached-minimal-dockerfile/HEAD?build_only=true + curl http://localhost:30080/build/gh/binderhub-ci-repos/cached-minimal-dockerfile/HEAD?build_only=true # ref: https://github.com/jupyterhub/action-k8s-namespace-report - uses: jupyterhub/action-k8s-namespace-report@v1 diff --git a/binderhub-service/templates/service.yaml b/binderhub-service/templates/service.yaml index f2d37b6e7..5bd11cc42 100644 --- a/binderhub-service/templates/service.yaml +++ b/binderhub-service/templates/service.yaml @@ -13,4 +13,7 @@ spec: - name: http port: {{ .Values.service.port }} targetPort: http + {{- with .Values.service.nodePort }} + nodePort: {{ . }} + {{- end }} selector: {{- include "binderhub-service.selectorLabels" . | nindent 4 }} diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 83b0fe8eb..c7636807b 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -162,6 +162,8 @@ properties: type: string port: type: integer + nodePort: + type: integer annotations: *labels-and-annotations # Ingress resource diff --git a/dev-config.yaml b/dev-config.yaml index 6baa21f53..a1e0d2024 100644 --- a/dev-config.yaml +++ b/dev-config.yaml @@ -10,3 +10,7 @@ buildPodsRegistryCredentials: server: "http://localhost:5000" username: "" password: "" + +service: + type: NodePort + nodePort: 30080 From c17753c2be4e340b5f6649a22dd07af8826ae520 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Sat, 8 Jul 2023 21:26:53 +0200 Subject: [PATCH 046/200] Add dockerApi.[extraArgs|extraFiles] and configure insecure registry --- .github/workflows/test-chart.yaml | 4 +- .../templates/_helpers-extra-files.tpl | 72 +++++++++++++++++++ .../templates/docker-api/daemonset.yaml | 21 ++++++ .../templates/docker-api/secret.yaml | 17 +++++ binderhub-service/values.schema.yaml | 26 +++++++ binderhub-service/values.yaml | 3 + dev-config.yaml | 33 ++++++++- tools/templates/lint-and-validate-values.yaml | 6 ++ 8 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 binderhub-service/templates/_helpers-extra-files.tpl create mode 100644 binderhub-service/templates/docker-api/secret.yaml diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index 9e3108339..820d42652 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -182,7 +182,9 @@ jobs: helm upgrade --install binderhub-service ./binderhub-service \ --values dev-config.yaml \ --set config.BinderHub.image_prefix=$LOCAL_REGISTRY_HOST/binderhub-service/ \ - --set buildPodsRegistryCredentials.server=http://$LOCAL_REGISTRY_HOST + --set config.DockerRegistry.url=http://$LOCAL_REGISTRY_HOST \ + --set buildPodsRegistryCredentials.server=http://$LOCAL_REGISTRY_HOST \ + --set dockerApi.extraFiles.daemon-json.data.insecure-registries[0]=$LOCAL_REGISTRY_HOST # ref: https://github.com/jupyterhub/action-k8s-await-workloads - uses: jupyterhub/action-k8s-await-workloads@v2 diff --git a/binderhub-service/templates/_helpers-extra-files.tpl b/binderhub-service/templates/_helpers-extra-files.tpl new file mode 100644 index 000000000..09cdf607c --- /dev/null +++ b/binderhub-service/templates/_helpers-extra-files.tpl @@ -0,0 +1,72 @@ +{{- /* + binderhub-service.extraFiles.data: + Renders content for a k8s Secret's data field, coming from extraFiles with + binaryData entries. +*/}} +{{- define "binderhub-service.extraFiles.data.withNewLineSuffix" -}} + {{- range $file_key, $file_details := . }} + {{- include "binderhub-service.extraFiles.validate-file" (list $file_key $file_details) }} + {{- if $file_details.binaryData }} + {{- $file_key | quote }}: {{ $file_details.binaryData | nospace | quote }}{{ println }} + {{- end }} + {{- end }} +{{- end }} +{{- define "binderhub-service.extraFiles.data" -}} + {{- include "binderhub-service.extraFiles.data.withNewLineSuffix" . | trimSuffix "\n" }} +{{- end }} + +{{- /* + binderhub-service.extraFiles.stringData: + Renders content for a k8s Secret's stringData field, coming from extraFiles + with either data or stringData entries. +*/}} +{{- define "binderhub-service.extraFiles.stringData.withNewLineSuffix" -}} + {{- range $file_key, $file_details := . }} + {{- include "binderhub-service.extraFiles.validate-file" (list $file_key $file_details) }} + {{- $file_name := $file_details.mountPath | base }} + {{- if $file_details.stringData }} + {{- $file_key | quote }}: | + {{- $file_details.stringData | trimSuffix "\n" | nindent 2 }}{{ println }} + {{- end }} + {{- if $file_details.data }} + {{- $file_key | quote }}: | + {{- if or (eq (ext $file_name) ".yaml") (eq (ext $file_name) ".yml") }} + {{- $file_details.data | toYaml | nindent 2 }}{{ println }} + {{- else if eq (ext $file_name) ".json" }} + {{- $file_details.data | toJson | nindent 2 }}{{ println }} + {{- else if eq (ext $file_name) ".toml" }} + {{- $file_details.data | toToml | trimSuffix "\n" | nindent 2 }}{{ println }} + {{- else }} + {{- print "\n\nextraFiles entries with 'data' (" $file_key " > " $file_details.mountPath ") needs to have a filename extension of .yaml, .yml, .json, or .toml!" | fail }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- define "binderhub-service.extraFiles.stringData" -}} + {{- include "binderhub-service.extraFiles.stringData.withNewLineSuffix" . | trimSuffix "\n" }} +{{- end }} + +{{- define "binderhub-service.extraFiles.validate-file" -}} + {{- $file_key := index . 0 }} + {{- $file_details := index . 1 }} + + {{- /* Use of mountPath. */}} + {{- if not ($file_details.mountPath) }} + {{- print "\n\nextraFiles entries (" $file_key ") must contain the field 'mountPath'." | fail }} + {{- end }} + + {{- /* Use one of stringData, binaryData, data. */}} + {{- $field_count := 0 }} + {{- if $file_details.data }} + {{- $field_count = add1 $field_count }} + {{- end }} + {{- if $file_details.stringData }} + {{- $field_count = add1 $field_count }} + {{- end }} + {{- if $file_details.binaryData }} + {{- $field_count = add1 $field_count }} + {{- end }} + {{- if ne $field_count 1 }} + {{- print "\n\nextraFiles entries (" $file_key ") must only contain one of the fields: 'data', 'stringData', and 'binaryData'." | fail }} + {{- end }} +{{- end }} diff --git a/binderhub-service/templates/docker-api/daemonset.yaml b/binderhub-service/templates/docker-api/daemonset.yaml index 92811d88f..d33f0ee01 100644 --- a/binderhub-service/templates/docker-api/daemonset.yaml +++ b/binderhub-service/templates/docker-api/daemonset.yaml @@ -28,11 +28,19 @@ spec: - --data-root=/var/lib/docker-api - --exec-root=/var/run/docker-api - --host=unix:///var/run/docker-api/docker-api.sock + {{- with .Values.dockerApi.extraArgs }} + {{- . | toYaml | nindent 12 }} + {{- end }} volumeMounts: - name: data mountPath: /var/lib/docker-api - name: exec mountPath: /var/run/docker-api + {{- range $file_key, $file_details := .Values.dockerApi.extraFiles }} + - name: files + mountPath: {{ $file_details.mountPath }} + subPath: {{ $file_key | quote }} + {{- end }} resources: {{- .Values.dockerApi.resources | toYaml | nindent 12 }} securityContext: @@ -46,6 +54,19 @@ spec: hostPath: path: /var/run/docker-api type: DirectoryOrCreate + {{- if .Values.dockerApi.extraFiles }} + - name: files + secret: + secretName: {{ include "binderhub-service.fullname" . }}-docker-api + items: + {{- range $file_key, $file_details := .Values.dockerApi.extraFiles }} + - key: {{ $file_key | quote }} + path: {{ $file_key | quote }} + {{- with $file_details.mode }} + mode: {{ . }} + {{- end }} + {{- end }} + {{- end }} {{- with .Values.dockerApi.image.pullSecrets }} imagePullSecrets: {{- . | toYaml | nindent 8 }} diff --git a/binderhub-service/templates/docker-api/secret.yaml b/binderhub-service/templates/docker-api/secret.yaml new file mode 100644 index 000000000..719add616 --- /dev/null +++ b/binderhub-service/templates/docker-api/secret.yaml @@ -0,0 +1,17 @@ +{{- if .Values.dockerApi.extraFiles }} +kind: Secret +apiVersion: v1 +metadata: + name: {{ include "binderhub-service.fullname" . }}-docker-api + labels: + {{- include "binderhub-service.labels" . | nindent 4 }} +type: Opaque +{{- with include "binderhub-service.extraFiles.data" .Values.dockerApi.extraFiles }} +data: + {{- . | nindent 2 }} +{{- end }} +{{- with include "binderhub-service.extraFiles.stringData" .Values.dockerApi.extraFiles }} +stringData: + {{- . | nindent 2 }} +{{- end }} +{{- end }} diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index c7636807b..01d61000a 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -204,3 +204,29 @@ properties: nodeSelector: *nodeSelector affinity: *affinity tolerations: *tolerations + extraArgs: + type: array + extraFiles: + type: object + additionalProperties: false + patternProperties: + ".*": + type: object + additionalProperties: false + required: [mountPath] + oneOf: + - required: [data] + - required: [stringData] + - required: [binaryData] + properties: + mountPath: + type: string + data: + type: object + additionalProperties: true + stringData: + type: string + binaryData: + type: string + mode: + type: number diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index fb71701f4..954dd4cc0 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -129,3 +129,6 @@ dockerApi: nodeSelector: {} affinity: {} tolerations: [] + + extraArgs: [] + extraFiles: {} diff --git a/dev-config.yaml b/dev-config.yaml index a1e0d2024..63f1c8369 100644 --- a/dev-config.yaml +++ b/dev-config.yaml @@ -1,16 +1,43 @@ -# When running tests, we use a local container registry to test pushing images -# to that can be reached from the build pods. +# NOTE: localhost:5000 will be replaced from the test job! +# +# When running tests, we use a local container registry to test pushing +# images to that can be reached from the build pods. This local registry +# is run outside of k8s, so use of localhost won't work well. Due to that, +# all values with localhost:5000 below are set explicitly from the test +# job to an IP that is reachable instead. # config: + # DockerRegistry is a jupyterhub/binderhub class that is used to call + # get_image_manifest, which determines if an image needs to be built or is + # already available in a registry. It will not need credentials to the + # registry. + # + DockerRegistry: + url: http://localhost:5000 BinderHub: log_level: DEBUG use_registry: true image_prefix: localhost:5000/binderhub-service/ buildPodsRegistryCredentials: - server: "http://localhost:5000" + server: http://localhost:5000 username: "" password: "" service: type: NodePort nodePort: 30080 + +dockerApi: + # The docker-api daemonset running the docker daemon needs to be configured in + # a way allowing it to work against insecure HTTP (not HTTPS) registries. + # + # ref: https://docs.docker.com/registry/insecure/ + # + extraArgs: + - --config-file=/etc/docker-api/daemon.json + extraFiles: + daemon-json: + mountPath: /etc/docker-api/daemon.json + data: + debug: true + insecure-registries: [localhost:5000] diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml index 90013655d..4b337afcb 100644 --- a/tools/templates/lint-and-validate-values.yaml +++ b/tools/templates/lint-and-validate-values.yaml @@ -58,3 +58,9 @@ dockerApi: securityContext: privileged: true runAsUser: 0 + extraFiles: + daemon-json: + mountPath: /etc/docker/daemon.json + data: + debug: true + insecure-registries: [localhost:5000] From cf81e9ad25015749e0b3b33b3d76788c9481c9de Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Sat, 8 Jul 2023 21:56:12 +0200 Subject: [PATCH 047/200] ci: disable debug logging to avoid overwhelming reader --- dev-config.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dev-config.yaml b/dev-config.yaml index 63f1c8369..20b219a3e 100644 --- a/dev-config.yaml +++ b/dev-config.yaml @@ -15,7 +15,8 @@ config: DockerRegistry: url: http://localhost:5000 BinderHub: - log_level: DEBUG + # log_level isn't enabled to avoid overwhelming the reader with info + # log_level: DEBUG use_registry: true image_prefix: localhost:5000/binderhub-service/ buildPodsRegistryCredentials: @@ -39,5 +40,6 @@ dockerApi: daemon-json: mountPath: /etc/docker-api/daemon.json data: - debug: true + # debug isn't enabled to avoid overwhelming the reader with info + # debug: true insecure-registries: [localhost:5000] From 65c23bb82b046b46bf0718ddbfaa6f35dc8a91e7 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Tue, 11 Jul 2023 15:43:53 +0300 Subject: [PATCH 048/200] Add docker config secret to the deployment to be mounted --- binderhub-service/templates/deployment.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index b0b80b095..b8fd4746a 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -25,6 +25,9 @@ spec: - name: secret secret: secretName: {{ include "binderhub-service.fullname" . }} + - name: docker-config-secret + secret: + secretName: {{ include "binderhub-service.fullname" . }}-build-pods-docker-config containers: - name: binderhub image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" From aaf56876b9b1459343dec15dc97dc645eb2bed06 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Tue, 11 Jul 2023 16:47:47 +0300 Subject: [PATCH 049/200] Revert "bugfix: add docker config secret to the deployment to be mounted" --- binderhub-service/templates/deployment.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index b8fd4746a..b0b80b095 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -25,9 +25,6 @@ spec: - name: secret secret: secretName: {{ include "binderhub-service.fullname" . }} - - name: docker-config-secret - secret: - secretName: {{ include "binderhub-service.fullname" . }}-build-pods-docker-config containers: - name: binderhub image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" From 8fd02789e036e1a7018625d7371bebc92ac5cbe7 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Tue, 11 Jul 2023 16:54:57 +0300 Subject: [PATCH 050/200] Fix docker config secret's name used in config --- binderhub-service/templates/deployment.yaml | 4 ++-- binderhub-service/values.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index b0b80b095..cc0782856 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -39,8 +39,8 @@ spec: mountPath: /etc/binderhub/mounted-secret/ readOnly: true env: - - name: HELM_RELEASE_NAME - value: {{ .Release.Name }} + - name: HELM_DEPLOYMENT_NAME + value: {{ include "binderhub-service.fullname" . }} resources: {{- .Values.resources | toYaml | nindent 12 }} securityContext: diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 954dd4cc0..3232a092e 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -47,8 +47,8 @@ extraConfig: # including registry credentials. binderhub_service_00_build_pods_docker_config: | import os - helm_release_name = os.environ["HELM_RELEASE_NAME"] - c.KubernetesBuildExecutor.push_secret = f"{helm_release_name}-build-pods-docker-config" + helm_deployment_name = os.environ["HELM_DEPLOYMENT_NAME"] + c.KubernetesBuildExecutor.push_secret = f"{helm_deployment_name}-build-pods-docker-config" replicas: 1 image: From 358d32f01cbda6b7efcd92217566a2094cffb433 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Fri, 14 Jul 2023 14:51:34 +0300 Subject: [PATCH 051/200] Set the BUILD_NAMESPACE env var to be used by KubernetesBuildExecutor --- binderhub-service/templates/deployment.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index cc0782856..49a5aa1d0 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -41,6 +41,10 @@ spec: env: - name: HELM_DEPLOYMENT_NAME value: {{ include "binderhub-service.fullname" . }} + - name: BUILD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace resources: {{- .Values.resources | toYaml | nindent 12 }} securityContext: From 00ac5631e673569f3f419161b57a8106fe088f9e Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Fri, 14 Jul 2023 16:23:13 +0300 Subject: [PATCH 052/200] Add logo to fix docs linkcheck --- docs/source/_static/images/logo/logo.png | Bin 0 -> 15025 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/source/_static/images/logo/logo.png diff --git a/docs/source/_static/images/logo/logo.png b/docs/source/_static/images/logo/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..47ce3453420c6b338b4ab25673dd4ce0b0562bc2 GIT binary patch literal 15025 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 g|L+BBTVrz*xBvNtB=RyB-~}u>DX3)rTf Date: Fri, 14 Jul 2023 16:28:08 +0300 Subject: [PATCH 053/200] Fix linkcheck warnings --- docs/source/contributing.md | 2 +- docs/source/index.md | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/source/contributing.md b/docs/source/contributing.md index 340edab54..854139a31 100644 --- a/docs/source/contributing.md +++ b/docs/source/contributing.md @@ -1 +1 @@ -## Contributing +# Contributing diff --git a/docs/source/index.md b/docs/source/index.md index c8b5709d5..44b28a9fc 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -35,9 +35,15 @@ reference/changelog **TODO** -- Architecture -- Implementation - ```{toctree} :maxdepth: 2 +explanation/architecture +explanation/implementation ``` + +## Contributing + +```{toctree} +:maxdepth: 2 +contributing +``` \ No newline at end of file From da2fdceb94b85003283cc0d34a3ea686a07298a9 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Fri, 14 Jul 2023 16:28:57 +0300 Subject: [PATCH 054/200] Rm title --- docs/source/index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/source/index.md b/docs/source/index.md index 44b28a9fc..2e705d188 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -24,8 +24,6 @@ Write description here... **TODO** -- Changelog - ```{toctree} :maxdepth: 2 reference/changelog From b83aa161ed6f5dd366040c42c13bf5503311b64b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 14 Jul 2023 13:30:21 +0000 Subject: [PATCH 055/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.md b/docs/source/index.md index 2e705d188..341fbd161 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -44,4 +44,4 @@ explanation/implementation ```{toctree} :maxdepth: 2 contributing -``` \ No newline at end of file +``` From 5717663759a0a167c99b92d8e21d9c88b62b2c0d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 00:43:13 +0000 Subject: [PATCH 056/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.8.0 → v3.10.1](https://github.com/asottile/pyupgrade/compare/v3.8.0...v3.10.1) - [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0) - [github.com/pre-commit/mirrors-prettier: v3.0.0-alpha.9-for-vscode → v3.0.0](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.0-alpha.9-for-vscode...v3.0.0) - [github.com/PyCQA/flake8: 6.0.0 → 6.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...6.1.0) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5085ff08f..9e4cb3971 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v3.8.0 + rev: v3.10.1 hooks: - id: pyupgrade args: @@ -27,7 +27,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black args: @@ -44,14 +44,14 @@ repos: # Autoformat: markdown, yaml (but not helm templates) - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.9-for-vscode + rev: v3.0.0 hooks: - id: prettier exclude: binderhub-service/templates/.* # Linting: Python code (see the file .flake8) - repo: https://github.com/PyCQA/flake8 - rev: "6.0.0" + rev: "6.1.0" hooks: - id: flake8 # Ignore style and complexity From 7b1bd254b1c85bc0be5453e7774f67646d2bbf89 Mon Sep 17 00:00:00 2001 From: consideRatio Date: Fri, 1 Sep 2023 05:02:13 +0000 Subject: [PATCH 057/200] Update library/docker version from 24.0.3-dind to 24.0.5-dind --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 3232a092e..e316591ad 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -116,7 +116,7 @@ ingress: dockerApi: image: repository: docker.io/library/docker - tag: "24.0.3-dind" # ref: https://hub.docker.com/_/docker/tags + tag: "24.0.5-dind" # ref: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From 3059e861456fb90b9eaa05e375df2b48d9bf6cfc Mon Sep 17 00:00:00 2001 From: consideRatio Date: Fri, 1 Sep 2023 05:02:20 +0000 Subject: [PATCH 058/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 38 +++++++++++------------ 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 897c89ac9..8b8da448b 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,7 +4,7 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -alembic==1.11.1 +alembic==1.12.0 # via jupyterhub async-generator==1.10 # via jupyterhub @@ -16,7 +16,7 @@ binderhub @ git+https://github.com/consideratio/binderhub@opt-out-of-launch # via -r requirements.in cachetools==5.3.1 # via google-auth -certifi==2023.5.7 +certifi==2023.7.22 # via # kubernetes # requests @@ -24,15 +24,15 @@ certipy==0.1.3 # via jupyterhub cffi==1.15.1 # via cryptography -charset-normalizer==3.1.0 +charset-normalizer==3.2.0 # via requests -cryptography==41.0.1 +cryptography==41.0.3 # via pyopenssl docker==6.1.3 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.21.0 +google-auth==2.22.0 # via kubernetes greenlet==2.0.2 # via sqlalchemy @@ -42,17 +42,17 @@ jinja2==3.1.2 # via # binderhub # jupyterhub -jsonschema==4.18.0 +jsonschema==4.19.0 # via # binderhub # jupyter-telemetry -jsonschema-specifications==2023.6.1 +jsonschema-specifications==2023.7.1 # via jsonschema jupyter-telemetry==0.1.0 # via jupyterhub -jupyterhub==4.0.1 +jupyterhub==4.0.2 # via binderhub -kubernetes==26.1.0 +kubernetes==27.2.0 # via binderhub mako==1.2.4 # via alembic @@ -63,6 +63,7 @@ markupsafe==2.1.3 oauthlib==3.2.2 # via # jupyterhub + # kubernetes # requests-oauthlib packaging==23.1 # via @@ -70,7 +71,7 @@ packaging==23.1 # jupyterhub pamela==1.1.0 # via jupyterhub -prometheus-client==0.17.0 +prometheus-client==0.17.1 # via # binderhub # jupyterhub @@ -84,7 +85,7 @@ pycparser==2.21 # via cffi pycurl==7.45.2 # via binderhub -pyjwt==2.7.0 +pyjwt==2.8.0 # via binderhub pyopenssl==23.2.0 # via certipy @@ -96,9 +97,9 @@ python-json-logger==2.0.7 # via # binderhub # jupyter-telemetry -pyyaml==6.0 +pyyaml==6.0.1 # via kubernetes -referencing==0.29.1 +referencing==0.30.2 # via # jsonschema # jsonschema-specifications @@ -110,7 +111,7 @@ requests==2.31.0 # requests-oauthlib requests-oauthlib==1.3.1 # via kubernetes -rpds-py==0.8.8 +rpds-py==0.10.0 # via # jsonschema # referencing @@ -125,11 +126,11 @@ six==1.16.0 # google-auth # kubernetes # python-dateutil -sqlalchemy==2.0.18 +sqlalchemy==2.0.20 # via # alembic # jupyterhub -tornado==6.3.2 +tornado==6.3.3 # via # binderhub # jupyterhub @@ -148,10 +149,7 @@ urllib3==1.26.16 # google-auth # kubernetes # requests -websocket-client==1.6.1 +websocket-client==1.6.2 # via # docker # kubernetes - -# The following packages are considered to be unsafe in a requirements file: -# setuptools From a5536411b9dd82b14a1cc6b0816b5750f55b5b00 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 00:34:28 +0000 Subject: [PATCH 059/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/autoflake: v2.2.0 → v2.2.1](https://github.com/PyCQA/autoflake/compare/v2.2.0...v2.2.1) - [github.com/pre-commit/mirrors-prettier: v3.0.0 → v3.0.3](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.0...v3.0.3) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e4cb3971..3a34e8744 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: # Autoformat: Python code - repo: https://github.com/PyCQA/autoflake - rev: v2.2.0 + rev: v2.2.1 hooks: - id: autoflake args: @@ -44,7 +44,7 @@ repos: # Autoformat: markdown, yaml (but not helm templates) - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0 + rev: v3.0.3 hooks: - id: prettier exclude: binderhub-service/templates/.* From a9483953d7850015040c596f711114e9d0ff8454 Mon Sep 17 00:00:00 2001 From: consideRatio Date: Sun, 1 Oct 2023 05:02:03 +0000 Subject: [PATCH 060/200] Update library/docker version from 24.0.5-dind to 24.0.6-dind --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index e316591ad..0b37621fe 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -116,7 +116,7 @@ ingress: dockerApi: image: repository: docker.io/library/docker - tag: "24.0.5-dind" # ref: https://hub.docker.com/_/docker/tags + tag: "24.0.6-dind" # ref: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From 567f06ccd38b06e9a3cab7dd5be1a9056c761516 Mon Sep 17 00:00:00 2001 From: consideRatio Date: Sun, 1 Oct 2023 05:02:06 +0000 Subject: [PATCH 061/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 26 +++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 8b8da448b..322331ed1 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -22,17 +22,17 @@ certifi==2023.7.22 # requests certipy==0.1.3 # via jupyterhub -cffi==1.15.1 +cffi==1.16.0 # via cryptography -charset-normalizer==3.2.0 +charset-normalizer==3.3.0 # via requests -cryptography==41.0.3 +cryptography==41.0.4 # via pyopenssl docker==6.1.3 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.22.0 +google-auth==2.23.2 # via kubernetes greenlet==2.0.2 # via sqlalchemy @@ -42,7 +42,7 @@ jinja2==3.1.2 # via # binderhub # jupyterhub -jsonschema==4.19.0 +jsonschema==4.19.1 # via # binderhub # jupyter-telemetry @@ -52,7 +52,7 @@ jupyter-telemetry==0.1.0 # via jupyterhub jupyterhub==4.0.2 # via binderhub -kubernetes==27.2.0 +kubernetes==28.1.0 # via binderhub mako==1.2.4 # via alembic @@ -111,22 +111,21 @@ requests==2.31.0 # requests-oauthlib requests-oauthlib==1.3.1 # via kubernetes -rpds-py==0.10.0 +rpds-py==0.10.3 # via # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.17.32 +ruamel-yaml==0.17.33 # via jupyter-telemetry ruamel-yaml-clib==0.2.7 # via ruamel-yaml six==1.16.0 # via - # google-auth # kubernetes # python-dateutil -sqlalchemy==2.0.20 +sqlalchemy==2.0.21 # via # alembic # jupyterhub @@ -134,22 +133,21 @@ tornado==6.3.3 # via # binderhub # jupyterhub -traitlets==5.9.0 +traitlets==5.10.1 # via # binderhub # jupyter-telemetry # jupyterhub -typing-extensions==4.7.1 +typing-extensions==4.8.0 # via # alembic # sqlalchemy urllib3==1.26.16 # via # docker - # google-auth # kubernetes # requests -websocket-client==1.6.2 +websocket-client==1.6.3 # via # docker # kubernetes From 4ab8a72d9bff8e1fa2e09ff4877c1de59fe7bd22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Oct 2023 05:53:00 +0000 Subject: [PATCH 062/200] build(deps): bump docker/setup-buildx-action from 2 to 3 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 70507b4c3..ff7b03130 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -69,7 +69,7 @@ jobs: uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx (for chartpress multi-arch builds) - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Install chart publishing dependencies (chartpress, pyyaml, helm) run: | From f63c9cb9aec9425d9170b35789a8b43f56a68b0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Oct 2023 05:53:04 +0000 Subject: [PATCH 063/200] build(deps): bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yaml | 2 +- .github/workflows/test-chart.yaml | 6 +++--- .github/workflows/test-docs.yaml | 2 +- .github/workflows/watch-dependencies.yaml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 70507b4c3..b8971dc06 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # chartpress needs git history fetch-depth: 0 diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index 820d42652..010cdbea4 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -26,7 +26,7 @@ jobs: lint_and_validate_rendered_templates: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.11" @@ -54,7 +54,7 @@ jobs: - helm-version: v3.8.0 # minimal required version steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.11" @@ -118,7 +118,7 @@ jobs: - k3s-channel: v1.24 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # chartpress needs git history fetch-depth: 0 diff --git a/.github/workflows/test-docs.yaml b/.github/workflows/test-docs.yaml index 0a4f5d072..b839c59b2 100644 --- a/.github/workflows/test-docs.yaml +++ b/.github/workflows/test-docs.yaml @@ -24,7 +24,7 @@ jobs: linkcheck: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.11" diff --git a/.github/workflows/watch-dependencies.yaml b/.github/workflows/watch-dependencies.yaml index 47edf8245..11788deff 100644 --- a/.github/workflows/watch-dependencies.yaml +++ b/.github/workflows/watch-dependencies.yaml @@ -44,7 +44,7 @@ jobs: tag_suffix: -dind steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get values.yaml pinned tag of ${{ matrix.registry }}/${{ matrix.repository }} id: local @@ -95,7 +95,7 @@ jobs: pull-requests: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Refreeze requirements.txt based on requirements.in run: ci/refreeze From abc6bf593204d99c697f2c739d1fbba719ec848d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Oct 2023 09:14:37 +0000 Subject: [PATCH 064/200] build(deps): bump docker/setup-qemu-action from 2 to 3 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e1ed34374..f5f5db784 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -66,7 +66,7 @@ jobs: f.write(f"publishing={publishing}\n") - name: Set up QEMU (for docker buildx) - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx (for chartpress multi-arch builds) uses: docker/setup-buildx-action@v3 From 73eb377ad391bbf07c01d0f98de5fa01c0a709ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 01:36:23 +0000 Subject: [PATCH 065/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.10.1 → v3.14.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.14.0) - [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3a34e8744..b7b908af4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.14.0 hooks: - id: pyupgrade args: @@ -27,7 +27,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black args: From 17aaa0700b8fccb623b80cdf0bdbbb7019098c8d Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Tue, 10 Oct 2023 15:27:04 +0300 Subject: [PATCH 066/200] Use the binderhub repo directly and pin to the commit that adds the build only mode --- images/binderhub-service/requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/binderhub-service/requirements.in b/images/binderhub-service/requirements.in index a16784d01..010c446a5 100644 --- a/images/binderhub-service/requirements.in +++ b/images/binderhub-service/requirements.in @@ -2,4 +2,4 @@ # To update requirements.txt, use the "Run workflow" button at # https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -binderhub[pycurl] @ git+https://github.com/consideratio/binderhub@opt-out-of-launch +binderhub[pycurl] @ git+https://github.com/jupyterhub/binderhub@0dde0c044e70b89ee7bceac9eac73928d215290c From d3483e6ba3259e56977428b2ed7140b13dd0c4d5 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Tue, 10 Oct 2023 15:27:22 +0300 Subject: [PATCH 067/200] Update the name of the config flag --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 0b37621fe..d03a0ac03 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -37,7 +37,7 @@ config: BinderHub: base_url: / port: 8585 - require_build_only: true + enable_api_only_mode: true KubernetesBuildExecutor: # docker_host must not be updated, assumptions about it are hardcoded in # docker-api/daemonset.yaml From d833d0892299b36ff196045c074a492fc13cf2f2 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 17 Oct 2023 08:53:02 +0000 Subject: [PATCH 068/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 322331ed1..c796f7b11 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -12,7 +12,7 @@ attrs==23.1.0 # via # jsonschema # referencing -binderhub @ git+https://github.com/consideratio/binderhub@opt-out-of-launch +binderhub @ git+https://github.com/jupyterhub/binderhub@0dde0c044e70b89ee7bceac9eac73928d215290c # via -r requirements.in cachetools==5.3.1 # via google-auth @@ -32,9 +32,9 @@ docker==6.1.3 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.23.2 +google-auth==2.23.3 # via kubernetes -greenlet==2.0.2 +greenlet==3.0.0 # via sqlalchemy idna==3.4 # via requests @@ -65,7 +65,7 @@ oauthlib==3.2.2 # jupyterhub # kubernetes # requests-oauthlib -packaging==23.1 +packaging==23.2 # via # docker # jupyterhub @@ -111,21 +111,21 @@ requests==2.31.0 # requests-oauthlib requests-oauthlib==1.3.1 # via kubernetes -rpds-py==0.10.3 +rpds-py==0.10.6 # via # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.17.33 +ruamel-yaml==0.17.35 # via jupyter-telemetry -ruamel-yaml-clib==0.2.7 +ruamel-yaml-clib==0.2.8 # via ruamel-yaml six==1.16.0 # via # kubernetes # python-dateutil -sqlalchemy==2.0.21 +sqlalchemy==2.0.22 # via # alembic # jupyterhub @@ -133,7 +133,7 @@ tornado==6.3.3 # via # binderhub # jupyterhub -traitlets==5.10.1 +traitlets==5.11.2 # via # binderhub # jupyter-telemetry @@ -142,12 +142,12 @@ typing-extensions==4.8.0 # via # alembic # sqlalchemy -urllib3==1.26.16 +urllib3==1.26.17 # via # docker # kubernetes # requests -websocket-client==1.6.3 +websocket-client==1.6.4 # via # docker # kubernetes From 0b3ac4a2dd67e54a53bb5909e0a8c05a2c74dc6f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:36:25 +0000 Subject: [PATCH 069/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.14.0 → v3.15.0](https://github.com/asottile/pyupgrade/compare/v3.14.0...v3.15.0) - [github.com/psf/black: 23.9.1 → 23.10.1](https://github.com/psf/black/compare/23.9.1...23.10.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7b908af4..b9e6a4329 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v3.14.0 + rev: v3.15.0 hooks: - id: pyupgrade args: @@ -27,7 +27,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black args: From d67845e03bb2e7e78709f25c9d808eb1e661183a Mon Sep 17 00:00:00 2001 From: consideRatio Date: Fri, 1 Dec 2023 05:02:05 +0000 Subject: [PATCH 070/200] Update library/docker version from 24.0.6-dind to 24.0.7-dind --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index d03a0ac03..a047d6127 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -116,7 +116,7 @@ ingress: dockerApi: image: repository: docker.io/library/docker - tag: "24.0.6-dind" # ref: https://hub.docker.com/_/docker/tags + tag: "24.0.7-dind" # ref: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From 8ea5104e9feeed964ecb4e36e8fa364b57a78dee Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 19 Dec 2023 13:19:26 -0800 Subject: [PATCH 071/200] Bump base debian image & don't install node from nodesource The node from debian bookworm is more than recent enough --- images/binderhub-service/Dockerfile | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/images/binderhub-service/Dockerfile b/images/binderhub-service/Dockerfile index 39f37269a..328901c9f 100644 --- a/images/binderhub-service/Dockerfile +++ b/images/binderhub-service/Dockerfile @@ -8,14 +8,11 @@ # # NOTE: If the image version is updated, also update it in ci/refreeze! # -FROM python:3.11-bullseye as build-stage +FROM python:3.11-bookworm as build-stage -# install node as required to build binderhub from source -RUN echo "deb https://deb.nodesource.com/node_16.x bullseye main" > /etc/apt/sources.list.d/nodesource.list \ - && curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - RUN apt-get update \ && apt-get install --yes \ - nodejs \ + nodejs npm \ && rm -rf /var/lib/apt/lists/* # Build wheels @@ -37,7 +34,7 @@ RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ # --------------- # This stage is built and published as quay.io/2i2c/binderhub-service. # -FROM python:3.11-slim-bullseye +FROM python:3.11-slim-bookworm ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update \ From f2ebaaa7ac6ff00c37040a9d5145ed18308b1093 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 19 Dec 2023 18:27:04 -0800 Subject: [PATCH 072/200] Add funding note --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 93affe332..2ece99539 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,9 @@ The documentation should help configure the BinderHub service to: [jupyterhub chart]: https://github.com/jupyterhub/zero-to-jupyterhub-k8s [persistent binderhub chart]: https://github.com/gesiscss/persistent_binderhub [was added]: https://github.com/jupyterhub/binderhub/pull/666 + +## Funding + +Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with +NFDI4DS [460234259](https://gepris.dfg.de/gepris/projekt/460234259?context=projekt&task=showDetail&id=460234259&) +and [CESSDA](https://www.cessda.eu). From ef6e2eecf6db1ef2570bbd5a882d65c43d0a08df Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 19 Dec 2023 13:58:23 -0800 Subject: [PATCH 073/200] Provide setup instructions for installing binderhub-service --- README.md | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ece99539..64968e4da 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,115 @@ The documentation should help configure the BinderHub service to: [persistent binderhub chart]: https://github.com/gesiscss/persistent_binderhub [was added]: https://github.com/jupyterhub/binderhub/pull/666 +## Installation + +1. Add the `binderhub-service` chart repository to helm: + + ```bash + helm repo add binderhub-service https://2i2c.org/binderhub-service + helm repo update + ``` + + Note this URL will change eventually, as binderhub-service is designed + to be a generic service, not something for use only by 2i2c. + +2. Install the latest development version of `binderhub-service` into a + namespace. + + ```bash + helm upgrade \ + --install \ + --create-namespace \ + --dev \ + --wait \ + --namespace + \ + binderhub-service/binderhub-service + ``` + + This sets up a binderhub service, but not in a publicly visible way. + +3. Test that it's running by port-forwarding to the correct pod: + + ```bash + kubectl -n port-forward $(kubectl -n get pod -l app.kubernetes.io/component=binderhub -o name) 8585:8585 + ``` + + This should forward requests on port 8585 on your localhost, to the binder service running inside the pod. So if you go + to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, *success!*. + +4. Create a docker registry for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, + but you can use anything else as well. + +5. In your GCP project, [enable Google Artifact Registry](https://cloud.google.com/artifact-registry/docs/enable-service) if + you have not done so before. + +6. Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo) - make sure you are in + the correct project!). Give it a name (ideally same name you are using for + helm chart), select 'Docker' as the format, 'Standard' as the mode, 'Region' + as the location and select the same region your kubernetes cluster is in. Hit "Create". + +7. Find the full path of the registry you just created, by opening it in the list + and looking for the small 'copy' icon next to the name of the registry. If you + hit it, it should copy something like `-docker.pkg.dev//`. + Save this. + +8. Create a Google Cloud Service Account that has permissions to push to this + registry ([via this URL] + (https://console.cloud.google.com/iam-admin/serviceaccounts/create) - make + sure you are in the correct project). Give it a name (same as the name you used + for the helm chart, but with a '-pusher' suffix) and click 'Create and Continue'. + In the next step, select 'Artifact Registry Writer' as a role. If you are in + an environment with multiple artifact registries, you may want to add a condition + here to restrict this service account's permissions. Click "Next". In the final + step, just click "Done". + +9. Now that the service account is created, find it in the list and open it. You will + find a tab named 'Keys' once the informational display opens - select that. Click + 'Add Key' -> 'Create New Key'. In the dialog box that pops up, select 'JSON' as the + key type and click 'Create'. This should download a key file. **Keep this file safe**! + +10. Now that we have the appropriate permissions, let's set up our configuration! Create a + new file named `binderhub-service-config.yaml` with the following contents: + + ```yaml + config: + BinderHub: + use_registry: true + image_prefix: /binder + buildPodsRegistryCredentials: + server: "https://-docker.pkg.dev" + username: "_json_key" + password: | + + ``` + + where: + 1. `` is what you copied from step 7 + 2. `` is the JSON file you downloaded in step 9. This is + a multi-line file - either indent it correctly to match up (the `|` allows multiline strings), + or simply edit the contents to be a single line. Since it is JSON, it does not matter. + 3. `` is the region your artifact registry was created in. + +11. Run a `helm upgrade` to use the new configuration you just created: + + ```bash + helm upgrade \ + --install \ + --create-namespace \ + --dev \ + --wait \ + --namespace + \ + binderhub-service/binderhub-service \ + -f binderhub-service-config.yaml + ``` + + This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, + you will see that the binderhub pod has restarted - this confirms that the config has been set up! + ## Funding Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with NFDI4DS [460234259](https://gepris.dfg.de/gepris/projekt/460234259?context=projekt&task=showDetail&id=460234259&) -and [CESSDA](https://www.cessda.eu). +and [CESSDA](https://www.cessda.eu). \ No newline at end of file From df34156973f350b8c529550666a0723de514e815 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 19 Dec 2023 18:10:20 -0800 Subject: [PATCH 074/200] Add testing instructions --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 64968e4da..3e4aa6954 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,8 @@ The documentation should help configure the BinderHub service to: BinderHub: use_registry: true image_prefix: /binder + # Temporarily enable the binderhub UI so we can test image building and pushing + enable_api_only_mode: false buildPodsRegistryCredentials: server: "https://-docker.pkg.dev" username: "_json_key" @@ -150,6 +152,31 @@ The documentation should help configure the BinderHub service to: This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, you will see that the binderhub pod has restarted - this confirms that the config has been set up! +12. Let's verify that *image building and pushing* works. Access the binderhub pod by following the + same instructions as step 3. But this time, you should see a binderhub page very similar to that + on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying + out `binder-examples/requirements`. It might take a while to build, but you should be able to see + logs in the UI. It should succeed at *pushing* the github image, but will fail to launch. The last + lines in the log in the UI should look like: + + ``` + Successfully pushed europe-west10-docker.pkg.dev/binderhub-service-development/bh-service-test/binderbinder-2dexamples-2drequirements-55ab5c:50533eb470ee6c24e872043d30b2fee463d6943fBuilt image, launching... + Launching server... + Launch attempt 1 failed, retrying... + Launch attempt 2 failed, retrying... + ``` + + You can also go back to the Google Artifact Registry you created earlier to verify that the built + image is indeed there. + +13. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. + Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy + using the command from step 11. + +You now have a working binderhub-service! It's now time to deploy a [z2jh](https://z2jh.jupyter.org) JupyterHub +with [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) installed. Instructions +for that are coming soon. + ## Funding Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with From 539d663f78fd0375370ac40fdbf088ee9f0c0542 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 02:12:30 +0000 Subject: [PATCH 075/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 3e4aa6954..852120d38 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ The documentation should help configure the BinderHub service to: ``` This should forward requests on port 8585 on your localhost, to the binder service running inside the pod. So if you go - to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, *success!*. + to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success!_. 4. Create a docker registry for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, but you can use anything else as well. @@ -114,26 +114,23 @@ The documentation should help configure the BinderHub service to: 10. Now that we have the appropriate permissions, let's set up our configuration! Create a new file named `binderhub-service-config.yaml` with the following contents: - ```yaml - config: - BinderHub: - use_registry: true - image_prefix: /binder - # Temporarily enable the binderhub UI so we can test image building and pushing - enable_api_only_mode: false - buildPodsRegistryCredentials: - server: "https://-docker.pkg.dev" - username: "_json_key" - password: | - - ``` - - where: - 1. `` is what you copied from step 7 - 2. `` is the JSON file you downloaded in step 9. This is - a multi-line file - either indent it correctly to match up (the `|` allows multiline strings), - or simply edit the contents to be a single line. Since it is JSON, it does not matter. - 3. `` is the region your artifact registry was created in. +```yaml +config: + BinderHub: + use_registry: true + image_prefix: /binder + # Temporarily enable the binderhub UI so we can test image building and pushing + enable_api_only_mode: false +buildPodsRegistryCredentials: + server: "https://-docker.pkg.dev" + username: "_json_key" + password: | + +``` + +where: 1. `` is what you copied from step 7 2. `` is the JSON file you downloaded in step 9. This is +a multi-line file - either indent it correctly to match up (the `|` allows multiline strings), +or simply edit the contents to be a single line. Since it is JSON, it does not matter. 3. `` is the region your artifact registry was created in. 11. Run a `helm upgrade` to use the new configuration you just created: @@ -152,11 +149,11 @@ The documentation should help configure the BinderHub service to: This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, you will see that the binderhub pod has restarted - this confirms that the config has been set up! -12. Let's verify that *image building and pushing* works. Access the binderhub pod by following the +12. Let's verify that _image building and pushing_ works. Access the binderhub pod by following the same instructions as step 3. But this time, you should see a binderhub page very similar to that on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying out `binder-examples/requirements`. It might take a while to build, but you should be able to see - logs in the UI. It should succeed at *pushing* the github image, but will fail to launch. The last + logs in the UI. It should succeed at _pushing_ the github image, but will fail to launch. The last lines in the log in the UI should look like: ``` @@ -181,4 +178,4 @@ for that are coming soon. Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with NFDI4DS [460234259](https://gepris.dfg.de/gepris/projekt/460234259?context=projekt&task=showDetail&id=460234259&) -and [CESSDA](https://www.cessda.eu). \ No newline at end of file +and [CESSDA](https://www.cessda.eu). From dc22ee078502707c5d6323b9bb954ba78d177a4b Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 20 Dec 2023 09:11:10 -0800 Subject: [PATCH 076/200] Cleanup registry / repository confusion --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 852120d38..e4fb07f86 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ The documentation should help configure the BinderHub service to: This should forward requests on port 8585 on your localhost, to the binder service running inside the pod. So if you go to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success!_. -4. Create a docker registry for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, +4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, but you can use anything else as well. 5. In your GCP project, [enable Google Artifact Registry](https://cloud.google.com/artifact-registry/docs/enable-service) if @@ -91,13 +91,13 @@ The documentation should help configure the BinderHub service to: helm chart), select 'Docker' as the format, 'Standard' as the mode, 'Region' as the location and select the same region your kubernetes cluster is in. Hit "Create". -7. Find the full path of the registry you just created, by opening it in the list - and looking for the small 'copy' icon next to the name of the registry. If you - hit it, it should copy something like `-docker.pkg.dev//`. +7. Find the full path of the repository you just created, by opening it in the list + and looking for the small 'copy' icon next to the name of the repository. If you + hit it, it should copy something like `-docker.pkg.dev//`. Save this. 8. Create a Google Cloud Service Account that has permissions to push to this - registry ([via this URL] + repository ([via this URL] (https://console.cloud.google.com/iam-admin/serviceaccounts/create) - make sure you are in the correct project). Give it a name (same as the name you used for the helm chart, but with a '-pusher' suffix) and click 'Create and Continue'. @@ -118,7 +118,7 @@ The documentation should help configure the BinderHub service to: config: BinderHub: use_registry: true - image_prefix: /binder + image_prefix: /binder # Temporarily enable the binderhub UI so we can test image building and pushing enable_api_only_mode: false buildPodsRegistryCredentials: @@ -128,9 +128,9 @@ buildPodsRegistryCredentials: ``` -where: 1. `` is what you copied from step 7 2. `` is the JSON file you downloaded in step 9. This is +where: 1. `` is what you copied from step 7 2. `` is the JSON file you downloaded in step 9. This is a multi-line file - either indent it correctly to match up (the `|` allows multiline strings), -or simply edit the contents to be a single line. Since it is JSON, it does not matter. 3. `` is the region your artifact registry was created in. +or simply edit the contents to be a single line. Since it is JSON, it does not matter. 3. `` is the region your artifact repository was created in. 11. Run a `helm upgrade` to use the new configuration you just created: @@ -163,7 +163,7 @@ or simply edit the contents to be a single line. Since it is JSON, it does not m Launch attempt 2 failed, retrying... ``` - You can also go back to the Google Artifact Registry you created earlier to verify that the built + You can also go back to the Google Artifact Registry repository you created earlier to verify that the built image is indeed there. 13. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. From e7ebc9acb23c49bf37e4bdf3bbc37a732075064a Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 20 Dec 2023 12:46:25 -0800 Subject: [PATCH 077/200] Clarifications to the installation document Worked through these with @simaattar2003 and made these changes. Co-authored-by: simaattar2003 --- README.md | 152 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 79 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index e4fb07f86..e2ce6d004 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ The documentation should help configure the BinderHub service to: helm upgrade \ --install \ --create-namespace \ - --dev \ + --devel \ --wait \ --namespace \ @@ -81,94 +81,100 @@ The documentation should help configure the BinderHub service to: to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success!_. 4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, - but you can use anything else as well. + but binderhub supports using other registries. -5. In your GCP project, [enable Google Artifact Registry](https://cloud.google.com/artifact-registry/docs/enable-service) if - you have not done so before. + Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo). Make sure you're in the correct project (look at the drop + down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. -6. Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo) - make sure you are in - the correct project!). Give it a name (ideally same name you are using for - helm chart), select 'Docker' as the format, 'Standard' as the mode, 'Region' - as the location and select the same region your kubernetes cluster is in. Hit "Create". + In the repository creation page, give it a name (ideally same name you are using for + helm chart), select 'Docker' as the format, 'Standard' as the mode, 'Region' + as the location type and select the same region your kubernetes cluster is in. The + settings about encryption and other options can be left in their default. Hit "Create". -7. Find the full path of the repository you just created, by opening it in the list +5. Find the full path of the repository you just created, by opening it in the list and looking for the small 'copy' icon next to the name of the repository. If you hit it, it should copy something like `-docker.pkg.dev//`. Save this. -8. Create a Google Cloud Service Account that has permissions to push to this +6. Create a Google Cloud Service Account that has permissions to push to this repository ([via this URL] (https://console.cloud.google.com/iam-admin/serviceaccounts/create) - make - sure you are in the correct project). Give it a name (same as the name you used + sure you are in the correct project again). You may also need appropriate permissions to set this up. Give it a name (same as the name you used for the helm chart, but with a '-pusher' suffix) and click 'Create and Continue'. - In the next step, select 'Artifact Registry Writer' as a role. If you are in - an environment with multiple artifact registries, you may want to add a condition - here to restrict this service account's permissions. Click "Next". In the final - step, just click "Done". + In the next step, select 'Artifact Registry Writer' as a role. Click "Next". In the final step, just click "Done". -9. Now that the service account is created, find it in the list and open it. You will +7. Now that the service account is created, find it in the list and open it. You will find a tab named 'Keys' once the informational display opens - select that. Click 'Add Key' -> 'Create New Key'. In the dialog box that pops up, select 'JSON' as the key type and click 'Create'. This should download a key file. **Keep this file safe**! -10. Now that we have the appropriate permissions, let's set up our configuration! Create a - new file named `binderhub-service-config.yaml` with the following contents: - -```yaml -config: - BinderHub: - use_registry: true - image_prefix: /binder - # Temporarily enable the binderhub UI so we can test image building and pushing - enable_api_only_mode: false -buildPodsRegistryCredentials: - server: "https://-docker.pkg.dev" - username: "_json_key" - password: | - -``` - -where: 1. `` is what you copied from step 7 2. `` is the JSON file you downloaded in step 9. This is -a multi-line file - either indent it correctly to match up (the `|` allows multiline strings), -or simply edit the contents to be a single line. Since it is JSON, it does not matter. 3. `` is the region your artifact repository was created in. - -11. Run a `helm upgrade` to use the new configuration you just created: - - ```bash - helm upgrade \ - --install \ - --create-namespace \ - --dev \ - --wait \ - --namespace - \ - binderhub-service/binderhub-service \ - -f binderhub-service-config.yaml - ``` - - This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, - you will see that the binderhub pod has restarted - this confirms that the config has been set up! - -12. Let's verify that _image building and pushing_ works. Access the binderhub pod by following the - same instructions as step 3. But this time, you should see a binderhub page very similar to that - on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying - out `binder-examples/requirements`. It might take a while to build, but you should be able to see - logs in the UI. It should succeed at _pushing_ the github image, but will fail to launch. The last - lines in the log in the UI should look like: - - ``` - Successfully pushed europe-west10-docker.pkg.dev/binderhub-service-development/bh-service-test/binderbinder-2dexamples-2drequirements-55ab5c:50533eb470ee6c24e872043d30b2fee463d6943fBuilt image, launching... - Launching server... - Launch attempt 1 failed, retrying... - Launch attempt 2 failed, retrying... - ``` - - You can also go back to the Google Artifact Registry repository you created earlier to verify that the built - image is indeed there. - -13. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. +8. Now that we have the appropriate permissions, let's set up our configuration! Create a + new file named `binderhub-service-config.yaml` with the following contents: + + ```yaml + config: + BinderHub: + use_registry: true + image_prefix: /binder + # Temporarily enable the binderhub UI so we can test image building and pushing + enable_api_only_mode: false + buildPodsRegistryCredentials: + server: "https://-docker.pkg.dev" + username: "_json_key" + password: | + + ``` + + where: + + 1. `` is what you copied from step 5. + + 2. `` is the JSON file you downloaded in step 7. + This is a multi-line file - either indent it correctly to match up (the `|` + allows multiline strings), + or simply edit the contents to be a single line. Since it is JSON, + it does not matter. + + 3. `` is the region your artifact repository was created in. You can see + this in the first part of `` as well. + +9. Run a `helm upgrade` to use the new configuration you just created: + + ```bash + helm upgrade \ + --install \ + --create-namespace \ + --devel \ + --wait \ + --namespace + \ + binderhub-service/binderhub-service \ + -f binderhub-service-config.yaml + ``` + + This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, + you will see that the binderhub pod has restarted - this confirms that the config has been set up! + +10. Let's verify that _image building and pushing_ works. Access the binderhub pod by following the + same instructions as step 3. But this time, you should see a binderhub page very similar to that + on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying + out `binder-examples/requirements`. It might take a while to build, but you should be able to see + logs in the UI. It should succeed at _pushing_ the github image, but will fail to launch. The last + lines in the log in the UI should look like: + + ``` + Successfully pushed europe-west10-docker.pkg.dev/binderhub-service-development/bh-service-test/binderbinder-2dexamples-2drequirements-55ab5c:50533eb470ee6c24e872043d30b2fee463d6943fBuilt image, launching... + Launching server... + Launch attempt 1 failed, retrying... + Launch attempt 2 failed, retrying... + ``` + + You can also go back to the Google Artifact Registry repository you created earlier to verify that the built + image is indeed there. + +12. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy - using the command from step 11. + using the command from step 9. You now have a working binderhub-service! It's now time to deploy a [z2jh](https://z2jh.jupyter.org) JupyterHub with [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) installed. Instructions From 51915c85f8ac58f2869a9ffd94881c6a1ccd48f5 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 20 Dec 2023 12:51:07 -0800 Subject: [PATCH 078/200] Fix wrong indent --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2ce6d004..ca6028dfe 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ The documentation should help configure the BinderHub service to: username: "_json_key" password: | - ``` + ``` where: From 0afa27d38324f70432aaca8d6ec3db9e8f6c4ef0 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 20 Dec 2023 12:54:43 -0800 Subject: [PATCH 079/200] Fix more indents --- README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ca6028dfe..92381a360 100644 --- a/README.md +++ b/README.md @@ -83,13 +83,13 @@ The documentation should help configure the BinderHub service to: 4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, but binderhub supports using other registries. - Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo). Make sure you're in the correct project (look at the drop - down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. + Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo). Make sure you're in the correct project (look at the drop + down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. - In the repository creation page, give it a name (ideally same name you are using for - helm chart), select 'Docker' as the format, 'Standard' as the mode, 'Region' - as the location type and select the same region your kubernetes cluster is in. The - settings about encryption and other options can be left in their default. Hit "Create". + In the repository creation page, give it a name (ideally same name you are using for + helm chart), select 'Docker' as the format, 'Standard' as the mode, 'Region' + as the location type and select the same region your kubernetes cluster is in. The + settings about encryption and other options can be left in their default. Hit "Create". 5. Find the full path of the repository you just created, by opening it in the list and looking for the small 'copy' icon next to the name of the repository. If you @@ -156,21 +156,21 @@ The documentation should help configure the BinderHub service to: you will see that the binderhub pod has restarted - this confirms that the config has been set up! 10. Let's verify that _image building and pushing_ works. Access the binderhub pod by following the - same instructions as step 3. But this time, you should see a binderhub page very similar to that - on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying - out `binder-examples/requirements`. It might take a while to build, but you should be able to see - logs in the UI. It should succeed at _pushing_ the github image, but will fail to launch. The last - lines in the log in the UI should look like: - - ``` - Successfully pushed europe-west10-docker.pkg.dev/binderhub-service-development/bh-service-test/binderbinder-2dexamples-2drequirements-55ab5c:50533eb470ee6c24e872043d30b2fee463d6943fBuilt image, launching... - Launching server... - Launch attempt 1 failed, retrying... - Launch attempt 2 failed, retrying... - ``` - - You can also go back to the Google Artifact Registry repository you created earlier to verify that the built - image is indeed there. + same instructions as step 3. But this time, you should see a binderhub page very similar to that + on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying + out `binder-examples/requirements`. It might take a while to build, but you should be able to see + logs in the UI. It should succeed at _pushing_ the github image, but will fail to launch. The last + lines in the log in the UI should look like: + + ``` + Successfully pushed europe-west10-docker.pkg.dev/binderhub-service-development/bh-service-test/binderbinder-2dexamples-2drequirements-55ab5c:50533eb470ee6c24e872043d30b2fee463d6943fBuilt image, launching... + Launching server... + Launch attempt 1 failed, retrying... + Launch attempt 2 failed, retrying... + ``` + + You can also go back to the Google Artifact Registry repository you created earlier to verify that the built + image is indeed there. 12. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy From 1b269d5d49b750c6762c6c4cfe35781ff87844eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 20:55:27 +0000 Subject: [PATCH 080/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 92381a360..617677931 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ The documentation should help configure the BinderHub service to: but binderhub supports using other registries. Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo). Make sure you're in the correct project (look at the drop - down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. + down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. In the repository creation page, give it a name (ideally same name you are using for helm chart), select 'Docker' as the format, 'Standard' as the mode, 'Region' @@ -130,10 +130,10 @@ The documentation should help configure the BinderHub service to: 1. `` is what you copied from step 5. 2. `` is the JSON file you downloaded in step 7. - This is a multi-line file - either indent it correctly to match up (the `|` - allows multiline strings), - or simply edit the contents to be a single line. Since it is JSON, - it does not matter. + This is a multi-line file - either indent it correctly to match up (the `|` + allows multiline strings), + or simply edit the contents to be a single line. Since it is JSON, + it does not matter. 3. `` is the region your artifact repository was created in. You can see this in the first part of `` as well. @@ -172,7 +172,7 @@ The documentation should help configure the BinderHub service to: You can also go back to the Google Artifact Registry repository you created earlier to verify that the built image is indeed there. -12. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. +11. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy using the command from step 9. From 990abf762164f775c9a743dc88d9b62a0bec598f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 05:05:57 +0000 Subject: [PATCH 081/200] build(deps): bump actions/upload-artifact from 3 to 4 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f5f5db784..2f7df5689 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -124,7 +124,7 @@ jobs: run: helm package binderhub-service # ref: https://github.com/actions/upload-artifact - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: steps.publishing.outputs.publishing == '' with: name: binderhub-service-${{ github.sha }} From ffd0dfe233a34358d6f9757c5b634384d2b7baaa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 05:06:00 +0000 Subject: [PATCH 082/200] build(deps): bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yaml | 2 +- .github/workflows/test-chart.yaml | 6 +++--- .github/workflows/test-docs.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f5f5db784..94356f1b8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -39,7 +39,7 @@ jobs: # chartpress needs git history fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index 010cdbea4..26d27eace 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" @@ -55,7 +55,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" @@ -151,7 +151,7 @@ jobs: traefik-enabled: false docker-enabled: true - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies diff --git a/.github/workflows/test-docs.yaml b/.github/workflows/test-docs.yaml index b839c59b2..955727a9b 100644 --- a/.github/workflows/test-docs.yaml +++ b/.github/workflows/test-docs.yaml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" From 49bff18b85fe80886c91d9fcf81ee39fe9a73b5a Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 16 Jan 2024 11:09:40 -0800 Subject: [PATCH 083/200] Temporarily pin dind image tag See https://github.com/2i2c-org/infrastructure/issues/3588 for tracking issue --- binderhub-service/values.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index a047d6127..7ffc9e8b0 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -116,7 +116,8 @@ ingress: dockerApi: image: repository: docker.io/library/docker - tag: "24.0.7-dind" # ref: https://hub.docker.com/_/docker/tags + # Temporarily pinned, until https://github.com/2i2c-org/infrastructure/issues/3588 is fixed + tag: "24.0.6-dind" # source: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From ae6fdab1dd33b3611351d7a29edecfcfd6c1c9f3 Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Tue, 16 Jan 2024 13:17:01 -0800 Subject: [PATCH 084/200] Apply suggestions from code review Co-authored-by: Erik Sundell --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 617677931..ef3d8e399 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ The documentation should help configure the BinderHub service to: down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. In the repository creation page, give it a name (ideally same name you are using for - helm chart), select 'Docker' as the format, 'Standard' as the mode, 'Region' + dedicated to the chart installation), select 'Docker' as the format, 'Standard' as the mode, 'Region' as the location type and select the same region your kubernetes cluster is in. The settings about encryption and other options can be left in their default. Hit "Create". @@ -100,7 +100,7 @@ The documentation should help configure the BinderHub service to: repository ([via this URL] (https://console.cloud.google.com/iam-admin/serviceaccounts/create) - make sure you are in the correct project again). You may also need appropriate permissions to set this up. Give it a name (same as the name you used - for the helm chart, but with a '-pusher' suffix) and click 'Create and Continue'. + for the chart installation, but with a '-pusher' suffix) and click 'Create and Continue'. In the next step, select 'Artifact Registry Writer' as a role. Click "Next". In the final step, just click "Done". 7. Now that the service account is created, find it in the list and open it. You will @@ -149,7 +149,7 @@ The documentation should help configure the BinderHub service to: --namespace \ binderhub-service/binderhub-service \ - -f binderhub-service-config.yaml + --values binderhub-service-config.yaml ``` This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, From 981989c0a056689974936e1f1a55001703e84a41 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 16 Jan 2024 18:13:54 -0800 Subject: [PATCH 085/200] Support running multiple instances on same cluster Without this parameterization, all installations try to use the same hostPath paths and everything except 1 fails --- binderhub-service/templates/deployment.yaml | 6 ++++++ .../templates/docker-api/daemonset.yaml | 14 +++++++------- binderhub-service/values.yaml | 13 ++++++++----- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index 49a5aa1d0..3a40848f8 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -41,10 +41,16 @@ spec: env: - name: HELM_DEPLOYMENT_NAME value: {{ include "binderhub-service.fullname" . }} + # Namespace build pods should be placed in - name: BUILD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace + # Namespace the binderhub pod is running in + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace resources: {{- .Values.resources | toYaml | nindent 12 }} securityContext: diff --git a/binderhub-service/templates/docker-api/daemonset.yaml b/binderhub-service/templates/docker-api/daemonset.yaml index d33f0ee01..2801646df 100644 --- a/binderhub-service/templates/docker-api/daemonset.yaml +++ b/binderhub-service/templates/docker-api/daemonset.yaml @@ -25,17 +25,17 @@ spec: image: {{ .Values.dockerApi.image.repository }}:{{ .Values.dockerApi.image.tag }} args: - dockerd - - --data-root=/var/lib/docker-api - - --exec-root=/var/run/docker-api - - --host=unix:///var/run/docker-api/docker-api.sock + - --data-root=/var/lib/{{ .Release.Namespace }}-{{ .Release.Name }}/docker-api + - --exec-root=/var/run/{{ .Release.Namespace }}-{{ .Release.Name }}/docker-api + - --host=unix:///var/run/{{ .Release.Namespace}}-{{ .Release.Name }}/docker-api/docker-api.sock {{- with .Values.dockerApi.extraArgs }} {{- . | toYaml | nindent 12 }} {{- end }} volumeMounts: - name: data - mountPath: /var/lib/docker-api + mountPath: /var/lib/{{ .Release.Namespace }}-{{ .Release.Name }} /docker-api - name: exec - mountPath: /var/run/docker-api + mountPath: /var/run/{{ .Release.Namespace }}-{{ .Release.Name }}/docker-api {{- range $file_key, $file_details := .Values.dockerApi.extraFiles }} - name: files mountPath: {{ $file_details.mountPath }} @@ -48,11 +48,11 @@ spec: volumes: - name: data hostPath: - path: /var/lib/docker-api + path: /var/lib/{{ .Release.Namespace }}-{{ .Release.Name }}/docker-api type: DirectoryOrCreate - name: exec hostPath: - path: /var/run/docker-api + path: /var/run/{{ .Release.Namespace}}-{{ .Release.Name }}/docker-api type: DirectoryOrCreate {{- if .Values.dockerApi.extraFiles }} - name: files diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index a047d6127..8f57e61fa 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -38,18 +38,21 @@ config: base_url: / port: 8585 enable_api_only_mode: true - KubernetesBuildExecutor: - # docker_host must not be updated, assumptions about it are hardcoded in - # docker-api/daemonset.yaml - docker_host: /var/run/docker-api/docker-api.sock extraConfig: # binderhub-service creates a k8s Secret with a docker config.json file # including registry credentials. - binderhub_service_00_build_pods_docker_config: | + binderhub-service-01-build-pods-docker-config: | import os helm_deployment_name = os.environ["HELM_DEPLOYMENT_NAME"] c.KubernetesBuildExecutor.push_secret = f"{helm_deployment_name}-build-pods-docker-config" + binderhub-service-02-set-docker-api: | + import os + helm_deployment_name = os.environ["HELM_DEPLOYMENT_NAME"] + namespace = os.environ["NAMESPACE"] + c.KubernetesBuildExecutor.docker_host = f"/var/lib/{ namespace }/{ helm_deployment_name }/docker-api/docker-api.sock" + + replicas: 1 image: repository: quay.io/2i2c/binderhub-service From 30b3e5808685f2504bf75b0694fe374f909e10c3 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 17 Jan 2024 09:43:10 -0800 Subject: [PATCH 086/200] Fix docker socket path determination --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 8f57e61fa..21f922e03 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -50,7 +50,7 @@ extraConfig: import os helm_deployment_name = os.environ["HELM_DEPLOYMENT_NAME"] namespace = os.environ["NAMESPACE"] - c.KubernetesBuildExecutor.docker_host = f"/var/lib/{ namespace }/{ helm_deployment_name }/docker-api/docker-api.sock" + c.KubernetesBuildExecutor.docker_host = f"/var/lib/{ namespace }-{ helm_deployment_name }/docker-api/docker-api.sock" replicas: 1 From 7d6a025f972293f2c60875d9f49104b01454b09d Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 17 Jan 2024 09:48:04 -0800 Subject: [PATCH 087/200] Point to *correct* path for docker socket --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 21f922e03..c2591101b 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -50,7 +50,7 @@ extraConfig: import os helm_deployment_name = os.environ["HELM_DEPLOYMENT_NAME"] namespace = os.environ["NAMESPACE"] - c.KubernetesBuildExecutor.docker_host = f"/var/lib/{ namespace }-{ helm_deployment_name }/docker-api/docker-api.sock" + c.KubernetesBuildExecutor.docker_host = f"/var/run/{ namespace }-{ helm_deployment_name }/docker-api/docker-api.sock" replicas: 1 From 7b1d7adec69d8f795dcc5b86491c4c954a6e9fbc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:48:33 +0000 Subject: [PATCH 088/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- binderhub-service/values.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index c2591101b..8f1ff7f75 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -52,7 +52,6 @@ extraConfig: namespace = os.environ["NAMESPACE"] c.KubernetesBuildExecutor.docker_host = f"/var/run/{ namespace }-{ helm_deployment_name }/docker-api/docker-api.sock" - replicas: 1 image: repository: quay.io/2i2c/binderhub-service From 17efd7b2ad2baa0203ada22c87ce213dcac5dec3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 05:44:27 +0000 Subject: [PATCH 089/200] build(deps): bump peter-evans/create-pull-request from 5 to 6 Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 5 to 6. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v5...v6) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/watch-dependencies.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/watch-dependencies.yaml b/.github/workflows/watch-dependencies.yaml index 11788deff..8531d681f 100644 --- a/.github/workflows/watch-dependencies.yaml +++ b/.github/workflows/watch-dependencies.yaml @@ -76,7 +76,7 @@ jobs: run: git --no-pager diff --color=always # ref: https://github.com/peter-evans/create-pull-request - - uses: peter-evans/create-pull-request@v5 + - uses: peter-evans/create-pull-request@v6 if: github.event_name != 'pull_request' with: branch: update-image-dependencies @@ -104,7 +104,7 @@ jobs: run: git --no-pager diff --color=always # ref: https://github.com/peter-evans/create-pull-request - - uses: peter-evans/create-pull-request@v5 + - uses: peter-evans/create-pull-request@v6 if: github.event_name != 'pull_request' with: branch: update-image-requirements From 7ea8b711af844d90aa25b71a9134707efaad9dc1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 05:44:33 +0000 Subject: [PATCH 090/200] build(deps): bump jupyterhub/action-k3s-helm from 3 to 4 Bumps [jupyterhub/action-k3s-helm](https://github.com/jupyterhub/action-k3s-helm) from 3 to 4. - [Release notes](https://github.com/jupyterhub/action-k3s-helm/releases) - [Changelog](https://github.com/jupyterhub/action-k3s-helm/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyterhub/action-k3s-helm/compare/v3...v4) --- updated-dependencies: - dependency-name: jupyterhub/action-k3s-helm dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test-chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index 010cdbea4..2bd7932c3 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -144,7 +144,7 @@ jobs: # kubectl and helm # # ref: https://github.com/jupyterhub/action-k3s-helm/ - - uses: jupyterhub/action-k3s-helm@v3 + - uses: jupyterhub/action-k3s-helm@v4 with: k3s-channel: ${{ matrix.k3s-channel }} metrics-enabled: false From 138975d7c8a81000149d857d5f4859acadbbbca9 Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Thu, 1 Feb 2024 10:02:09 +0000 Subject: [PATCH 091/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 60 +++++++++++------------ 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index c796f7b11..b7b369635 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,19 +4,19 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -alembic==1.12.0 +alembic==1.13.1 # via jupyterhub async-generator==1.10 # via jupyterhub -attrs==23.1.0 +attrs==23.2.0 # via # jsonschema # referencing binderhub @ git+https://github.com/jupyterhub/binderhub@0dde0c044e70b89ee7bceac9eac73928d215290c # via -r requirements.in -cachetools==5.3.1 +cachetools==5.3.2 # via google-auth -certifi==2023.7.22 +certifi==2023.11.17 # via # kubernetes # requests @@ -24,39 +24,39 @@ certipy==0.1.3 # via jupyterhub cffi==1.16.0 # via cryptography -charset-normalizer==3.3.0 +charset-normalizer==3.3.2 # via requests -cryptography==41.0.4 +cryptography==42.0.2 # via pyopenssl -docker==6.1.3 +docker==7.0.0 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.23.3 +google-auth==2.27.0 # via kubernetes -greenlet==3.0.0 +greenlet==3.0.3 # via sqlalchemy -idna==3.4 +idna==3.6 # via requests -jinja2==3.1.2 +jinja2==3.1.3 # via # binderhub # jupyterhub -jsonschema==4.19.1 +jsonschema==4.21.1 # via # binderhub # jupyter-telemetry -jsonschema-specifications==2023.7.1 +jsonschema-specifications==2023.12.1 # via jsonschema jupyter-telemetry==0.1.0 # via jupyterhub jupyterhub==4.0.2 # via binderhub -kubernetes==28.1.0 +kubernetes==29.0.0 # via binderhub -mako==1.2.4 +mako==1.3.2 # via alembic -markupsafe==2.1.3 +markupsafe==2.1.4 # via # jinja2 # mako @@ -71,11 +71,11 @@ packaging==23.2 # jupyterhub pamela==1.1.0 # via jupyterhub -prometheus-client==0.17.1 +prometheus-client==0.19.0 # via # binderhub # jupyterhub -pyasn1==0.5.0 +pyasn1==0.5.1 # via # pyasn1-modules # rsa @@ -87,7 +87,7 @@ pycurl==7.45.2 # via binderhub pyjwt==2.8.0 # via binderhub -pyopenssl==23.2.0 +pyopenssl==24.0.0 # via certipy python-dateutil==2.8.2 # via @@ -99,7 +99,7 @@ python-json-logger==2.0.7 # jupyter-telemetry pyyaml==6.0.1 # via kubernetes -referencing==0.30.2 +referencing==0.33.0 # via # jsonschema # jsonschema-specifications @@ -111,13 +111,13 @@ requests==2.31.0 # requests-oauthlib requests-oauthlib==1.3.1 # via kubernetes -rpds-py==0.10.6 +rpds-py==0.17.1 # via # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.17.35 +ruamel-yaml==0.18.5 # via jupyter-telemetry ruamel-yaml-clib==0.2.8 # via ruamel-yaml @@ -125,29 +125,27 @@ six==1.16.0 # via # kubernetes # python-dateutil -sqlalchemy==2.0.22 +sqlalchemy==2.0.25 # via # alembic # jupyterhub -tornado==6.3.3 +tornado==6.4 # via # binderhub # jupyterhub -traitlets==5.11.2 +traitlets==5.14.1 # via # binderhub # jupyter-telemetry # jupyterhub -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # alembic # sqlalchemy -urllib3==1.26.17 +urllib3==2.2.0 # via # docker # kubernetes # requests -websocket-client==1.6.4 - # via - # docker - # kubernetes +websocket-client==1.7.0 + # via kubernetes From 958315db0ef91afc996cefe2aace625462d3e459 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:34:27 +0000 Subject: [PATCH 092/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.1 → 24.1.1](https://github.com/psf/black/compare/23.10.1...24.1.1) - [github.com/pycqa/isort: 5.12.0 → 5.13.2](https://github.com/pycqa/isort/compare/5.12.0...5.13.2) - [github.com/pre-commit/mirrors-prettier: v3.0.3 → v4.0.0-alpha.8](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.3...v4.0.0-alpha.8) - [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b9e6a4329..08bc79dd0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 24.1.1 hooks: - id: black args: @@ -36,7 +36,7 @@ repos: # Autoformat: Python code - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort args: @@ -44,14 +44,14 @@ repos: # Autoformat: markdown, yaml (but not helm templates) - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v4.0.0-alpha.8 hooks: - id: prettier exclude: binderhub-service/templates/.* # Linting: Python code (see the file .flake8) - repo: https://github.com/PyCQA/flake8 - rev: "6.1.0" + rev: "7.0.0" hooks: - id: flake8 # Ignore style and complexity From 61f732ca93b44d4db4093196696689dae9cb86af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:34:35 +0000 Subject: [PATCH 093/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- binderhub-service/mounted-files/binderhub_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/binderhub-service/mounted-files/binderhub_config.py b/binderhub-service/mounted-files/binderhub_config.py index 03057ca1a..7cf92a980 100644 --- a/binderhub-service/mounted-files/binderhub_config.py +++ b/binderhub-service/mounted-files/binderhub_config.py @@ -2,6 +2,7 @@ This configuration file is mounted to be read by binderhub with the sole purpose of loading chart configuration passed via "config" and "extraConfig". """ + from functools import lru_cache from ruamel.yaml import YAML From c58292eb94fa9953d2232295d108734a0dd34f85 Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Wed, 7 Feb 2024 19:22:18 -0800 Subject: [PATCH 094/200] Fix extra space in docker socket mount path --- binderhub-service/templates/docker-api/daemonset.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/templates/docker-api/daemonset.yaml b/binderhub-service/templates/docker-api/daemonset.yaml index 2801646df..5385795ca 100644 --- a/binderhub-service/templates/docker-api/daemonset.yaml +++ b/binderhub-service/templates/docker-api/daemonset.yaml @@ -33,7 +33,7 @@ spec: {{- end }} volumeMounts: - name: data - mountPath: /var/lib/{{ .Release.Namespace }}-{{ .Release.Name }} /docker-api + mountPath: /var/lib/{{ .Release.Namespace }}-{{ .Release.Name }}/docker-api - name: exec mountPath: /var/run/{{ .Release.Namespace }}-{{ .Release.Name }}/docker-api {{- range $file_key, $file_details := .Values.dockerApi.extraFiles }} From 4c67f25a02de1d7548a07dc8b8e2c0e62186ef05 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 6 Feb 2024 19:29:57 -0800 Subject: [PATCH 095/200] Write a broad outline of how to connect this to a z2jh --- README.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ef3d8e399..dedc06156 100644 --- a/README.md +++ b/README.md @@ -176,9 +176,97 @@ The documentation should help configure the BinderHub service to: Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy using the command from step 9. -You now have a working binderhub-service! It's now time to deploy a [z2jh](https://z2jh.jupyter.org) JupyterHub -with [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) installed. Instructions -for that are coming soon. +## Connect with a JupyterHub installation + +Next, let's connect this to a JupyterHub set up via [z2jh](https://z2jh.jupyter.org/). While any JupyterHub +that can run containers will work with this, the *most common* setup is to use this with z2jh. The first +few steps are lifted directly from the [install JupyterHub](https://z2jh.jupyter.org/en/stable/jupyterhub/installation.html) +section of z2jh. + +1. Add the z2jh chart repository to helm: + + ``` + helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ + helm repo update + ``` + +2. Figure out the name of the binderhub service. + +2. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. + + ```yaml + hub: + loadRoles: + user: + scopes: + - self + - "access:services" + services: + binder: + # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 + # for something more readable and requiring less copy-pasting + url: http://binderhub-service-test:80 + ``` + +4. Install the JupyterHub + +5. Test access to service. Note that it looks broken. + +5. Change binder config + +```yaml +config: + BinderHub: + base_url: /services/binder +``` + +6. Test, see that it works! + +7. Connect with `jupyterhub-fancy-profiles` + +```yaml +singleuser: + profileList: + - display_name: "Only Profile Available, this info is not shown in the UI" + slug: only-choice + profile_options: + image: + display_name: Image + unlisted_choice: &profile_list_unlisted_choice + enabled: True + display_name: "Custom image" + validation_regex: "^.+:.+$" + validation_message: "Must be a publicly available docker image, of form :" + display_name_in_choices: "Specify an existing docker image" + description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" + kubespawner_override: + image: "{value}" + choices: + pangeo: + display_name: Pangeo Notebook Image + description: "Python image with scientific, dask and geospatial tools" + kubespawner_override: + image: pangeo/pangeo-notebook:2023.09.11 + scipy: + display_name: Jupyter SciPy Notebook + slug: scipy + kubespawner_override: + image: jupyter/scipy-notebook:2023-06-26 +hub: + image: + # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags + name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles + tag: z2jh-v3.2.1-fancy-profiles-sha-5874628 + + extraConfig: + enable-fancy-profiles: | + from jupyterhub_fancy_profiles import setup_ui + setup_ui(c) +``` + +8. Deploy + +9. Test! ## Funding From 0e5c27014355ef1ba4974a75a53bfab85dd96d2e Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 7 Feb 2024 20:18:39 -0800 Subject: [PATCH 096/200] Use separate variable names for push secret & helm release name Follow-up to https://github.com/2i2c-org/binderhub-service/pull/77 (which made wrong assumptions about HELM_DEPLOYMENT_NAME). --- .../templates/build-pods-docker-config/secret.yaml | 2 ++ binderhub-service/templates/deployment.yaml | 6 ++++-- binderhub-service/values.yaml | 7 +++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/binderhub-service/templates/build-pods-docker-config/secret.yaml b/binderhub-service/templates/build-pods-docker-config/secret.yaml index 1a91b29e1..9d3a3f176 100644 --- a/binderhub-service/templates/build-pods-docker-config/secret.yaml +++ b/binderhub-service/templates/build-pods-docker-config/secret.yaml @@ -7,6 +7,8 @@ kind: Secret apiVersion: v1 metadata: + # If this is changed, update the value of the PUSH_SECRET_NAME environment + # variable in the binderhub deployment (in deployment.yaml) name: {{ include "binderhub-service.fullname" . }}-build-pods-docker-config labels: {{- include "binderhub-service.labels" . | nindent 4 }} diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index 3a40848f8..d432a438a 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -39,8 +39,10 @@ spec: mountPath: /etc/binderhub/mounted-secret/ readOnly: true env: - - name: HELM_DEPLOYMENT_NAME - value: {{ include "binderhub-service.fullname" . }} + - name: PUSH_SECRET_NAME + value: {{ include "binderhub-service.fullname" . }}-build-pods-docker-config + - name: HELM_RELEASE_NAME + value: {{ .Release.Name }} # Namespace build pods should be placed in - name: BUILD_NAMESPACE valueFrom: diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index e2e41232d..54dfd76e6 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -43,14 +43,13 @@ extraConfig: # including registry credentials. binderhub-service-01-build-pods-docker-config: | import os - helm_deployment_name = os.environ["HELM_DEPLOYMENT_NAME"] - c.KubernetesBuildExecutor.push_secret = f"{helm_deployment_name}-build-pods-docker-config" + c.KubernetesBuildExecutor.push_secret = os.environ["PUSH_SECRET_NAME"] binderhub-service-02-set-docker-api: | import os - helm_deployment_name = os.environ["HELM_DEPLOYMENT_NAME"] + helm_release_name = os.environ["HELM_RELEASE_NAME"] namespace = os.environ["NAMESPACE"] - c.KubernetesBuildExecutor.docker_host = f"/var/run/{ namespace }-{ helm_deployment_name }/docker-api/docker-api.sock" + c.KubernetesBuildExecutor.docker_host = f"/var/run/{ namespace }-{ helm_release_name }/docker-api/docker-api.sock" replicas: 1 image: From fa75d983f9d7098b70343ef62582a11d4fe426dd Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 7 Feb 2024 20:21:39 -0800 Subject: [PATCH 097/200] Flesh out the binderhub service & fancy-profiles instructions --- README.md | 190 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 128 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index dedc06156..694398b0c 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ The documentation should help configure the BinderHub service to: --create-namespace \ --devel \ --wait \ - --namespace + --namespace \ \ binderhub-service/binderhub-service ``` @@ -190,83 +190,149 @@ section of z2jh. helm repo update ``` -2. Figure out the name of the binderhub service. +2. We want the binderhub to be available under `http://{{hub url}}/services/binder`, because + that is what `jupyterhub-fancy-profiles` expects. Eventually we would also want authentication + to work correctly. For that, we must set up binderhub as a [JupyterHub Service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html). + This provides two things: + + a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to + expose the service to the external world without needing its own loadbalancer or ingress. + b. (Eventually) Appropriate credentials for authenticated network calls between these two services. + + To make this connection, we need to tell JupyterHub where to find BinderHub. Eventually + this can be automatic (once [this issue](https://github.com/2i2c-org/binderhub-service/issues/57) + gets resolved). In the meantime, you can get the name of the BinderHub service by executing + the following command: -2. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. + ```bash + kubectl -n get svc -l app.kubernetes.io/name=binderhub-service -o name + ``` + + Make a note of this, we will use it in the next step. + +3. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. ```yaml - hub: - loadRoles: - user: - scopes: - - self - - "access:services" services: binder: # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 # for something more readable and requiring less copy-pasting - url: http://binderhub-service-test:80 + url: http://{{ service name from step 2}} + ``` + +4. Install the JupyterHub helm chart with the following command: + + ```bash + helm upgrade --cleanup-on-fail \ + --install jupyterhub/jupyterhub \ + --namespace \ + --version= \ + --values z2jh-config.yaml \ + --wait ``` -4. Install the JupyterHub + where: -5. Test access to service. Note that it looks broken. + - `` is any name you can use to refer to this imag + (like `jupyterhub`) -5. Change binder config + - `` is the *same* namespace used for the BinderHub install -```yaml -config: - BinderHub: - base_url: /services/binder -``` + - `` is the latest stable version of the JupyterHub + helm chart, available from [the chart repository](https://hub.jupyter.org/helm-chart/). + +5. Find the external IP on which the JupyterHub is accessible: + + ```bash + kubectl -n get svc proxy-public + ``` -6. Test, see that it works! - -7. Connect with `jupyterhub-fancy-profiles` - -```yaml -singleuser: - profileList: - - display_name: "Only Profile Available, this info is not shown in the UI" - slug: only-choice - profile_options: - image: - display_name: Image - unlisted_choice: &profile_list_unlisted_choice - enabled: True - display_name: "Custom image" - validation_regex: "^.+:.+$" - validation_message: "Must be a publicly available docker image, of form :" - display_name_in_choices: "Specify an existing docker image" - description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" - kubespawner_override: - image: "{value}" - choices: - pangeo: - display_name: Pangeo Notebook Image - description: "Python image with scientific, dask and geospatial tools" - kubespawner_override: - image: pangeo/pangeo-notebook:2023.09.11 - scipy: - display_name: Jupyter SciPy Notebook - slug: scipy - kubespawner_override: - image: jupyter/scipy-notebook:2023-06-26 -hub: - image: - # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags - name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles - tag: z2jh-v3.2.1-fancy-profiles-sha-5874628 - - extraConfig: - enable-fancy-profiles: | - from jupyterhub_fancy_profiles import setup_ui - setup_ui(c) +6. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder`. + You should see an unstyled, somewhat broken 404 page. This is great and expected. Let's fix + that. + +7. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now + be available under `/services/binder`. + + ```yaml + config: + BinderHub: + base_url: /services/binder + ``` + + Deploy this using the `helm upgrade` command from step 9 in the previous section. + +6. Test by going to `http://{{ external ip from step 5}}/services/binder` again, and you + should see a *styled* 404 page! Success - this means BinderHub is now connected to + JupyterHub, even if the end users can't see it yet. Let's connect them! + +## Connect with `jupyterhub-fancy-profiles` + +The [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) +project provides a user facing frontend for connecting the JupyterHub to BinderHub, +allowing them to build their own images the same way they would on `mybinder.org`! + +1. First, we need to install the `jupyterhub-fancy-profiles` package in the container + that is running the JupyterHub process itself (not the user containers). The + easiest way to do this is to use one of the pre-built images provided by + the `jupyterhub-fancy-profiles` project. In the [list of tags](https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags), + select the latest tag that also includes the version of the z2jh chart you are + using (the `version` specified in step 4 of the previous step). Once you + select the tag, add it to the `z2jh-config.yaml` file: + + ```yaml + hub: + image: + # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags + name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles + tag: # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" + + extraConfig: + enable-fancy-profiles: | + from jupyterhub_fancy_profiles import setup_ui + setup_ui(c) + ``` + +2. Since `jupyterhub-fancy-profiles` adds on to the [profileList](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) + feature of KubeSpawner, we need to configure a profile list here as well. + Add this to the `z2jh-config.yaml` file: + + ```yaml + singleuser: + profileList: + - display_name: "Only Profile Available, this info is not shown in the UI" + slug: only-choice + profile_options: + image: + display_name: Image + unlisted_choice: &profile_list_unlisted_choice + enabled: True + display_name: "Custom image" + validation_regex: "^.+:.+$" + validation_message: "Must be a publicly available docker image, of form :" + display_name_in_choices: "Specify an existing docker image" + description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" + kubespawner_override: + image: "{value}" + choices: + pangeo: + display_name: Pangeo Notebook Image + description: "Python image with scientific, dask and geospatial tools" + kubespawner_override: + image: pangeo/pangeo-notebook:2023.09.11 + scipy: + display_name: Jupyter SciPy Notebook + slug: scipy + kubespawner_override: + image: jupyter/scipy-notebook:2023-06-26 ``` -8. Deploy +3. Deploy, using the command from step 3 of the section above. -9. Test! +4. Access the JupyterHub itself, using the external IP you got from step 5 of the section + above. You should see a UI that allows you to choose two pre-existing images (pangeo and scipy), + specify your own image, or 'build' your own image. The last option lets you access the binder functionality! + Test it out :) ## Funding From 5725c4b071d370ef7b5f7c5f761dcbc26cb84d1c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 04:25:04 +0000 Subject: [PATCH 098/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 77 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 694398b0c..feb510c64 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ The documentation should help configure the BinderHub service to: ## Connect with a JupyterHub installation Next, let's connect this to a JupyterHub set up via [z2jh](https://z2jh.jupyter.org/). While any JupyterHub -that can run containers will work with this, the *most common* setup is to use this with z2jh. The first +that can run containers will work with this, the _most common_ setup is to use this with z2jh. The first few steps are lifted directly from the [install JupyterHub](https://z2jh.jupyter.org/en/stable/jupyterhub/installation.html) section of z2jh. @@ -196,7 +196,7 @@ section of z2jh. This provides two things: a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to - expose the service to the external world without needing its own loadbalancer or ingress. + expose the service to the external world without needing its own loadbalancer or ingress. b. (Eventually) Appropriate credentials for authenticated network calls between these two services. To make this connection, we need to tell JupyterHub where to find BinderHub. Eventually @@ -214,10 +214,10 @@ section of z2jh. ```yaml services: - binder: - # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 - # for something more readable and requiring less copy-pasting - url: http://{{ service name from step 2}} + binder: + # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 + # for something more readable and requiring less copy-pasting + url: http://{{ service name from step 2}} ``` 4. Install the JupyterHub helm chart with the following command: @@ -236,7 +236,7 @@ section of z2jh. - `` is any name you can use to refer to this imag (like `jupyterhub`) - - `` is the *same* namespace used for the BinderHub install + - `` is the _same_ namespace used for the BinderHub install - `` is the latest stable version of the JupyterHub helm chart, available from [the chart repository](https://hub.jupyter.org/helm-chart/). @@ -256,14 +256,14 @@ section of z2jh. ```yaml config: - BinderHub: - base_url: /services/binder + BinderHub: + base_url: /services/binder ``` Deploy this using the `helm upgrade` command from step 9 in the previous section. -6. Test by going to `http://{{ external ip from step 5}}/services/binder` again, and you - should see a *styled* 404 page! Success - this means BinderHub is now connected to +8. Test by going to `http://{{ external ip from step 5}}/services/binder` again, and you + should see a _styled_ 404 page! Success - this means BinderHub is now connected to JupyterHub, even if the end users can't see it yet. Let's connect them! ## Connect with `jupyterhub-fancy-profiles` @@ -299,32 +299,34 @@ allowing them to build their own images the same way they would on `mybinder.org ```yaml singleuser: - profileList: - - display_name: "Only Profile Available, this info is not shown in the UI" - slug: only-choice - profile_options: - image: - display_name: Image - unlisted_choice: &profile_list_unlisted_choice - enabled: True - display_name: "Custom image" - validation_regex: "^.+:.+$" - validation_message: "Must be a publicly available docker image, of form :" - display_name_in_choices: "Specify an existing docker image" - description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" - kubespawner_override: - image: "{value}" - choices: - pangeo: - display_name: Pangeo Notebook Image - description: "Python image with scientific, dask and geospatial tools" - kubespawner_override: - image: pangeo/pangeo-notebook:2023.09.11 - scipy: - display_name: Jupyter SciPy Notebook - slug: scipy - kubespawner_override: - image: jupyter/scipy-notebook:2023-06-26 + profileList: + - display_name: "Only Profile Available, this info is not shown in the UI" + slug: only-choice + profile_options: + image: + display_name: Image + unlisted_choice: &profile_list_unlisted_choice + enabled: True + display_name: "Custom image" + validation_regex: "^.+:.+$" + validation_message: "Must be a publicly available docker image, of form :" + display_name_in_choices: "Specify an existing docker image" + description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" + kubespawner_override: + image: "{value}" + choices: + pangeo: + display_name: Pangeo Notebook Image + description: "Python image with scientific, dask and geospatial tools" + kubespawner_override: + image: pangeo/pangeo-notebook:2023.09.11 + scipy: + display_name: Jupyter SciPy Notebook + slug: scipy + kubespawner_override: + image: jupyter/scipy-notebook:2023-06-26 + ``` + ``` 3. Deploy, using the command from step 3 of the section above. @@ -339,3 +341,4 @@ allowing them to build their own images the same way they would on `mybinder.org Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with NFDI4DS [460234259](https://gepris.dfg.de/gepris/projekt/460234259?context=projekt&task=showDetail&id=460234259&) and [CESSDA](https://www.cessda.eu). +``` From 9ffb10617c32ff17e4dd3716d06b4c44f0724d7e Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Thu, 8 Feb 2024 16:32:37 -0800 Subject: [PATCH 099/200] Clarifications & changes from walkthrough with Sima --- README.md | 99 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index feb510c64..cc828c633 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ The documentation should help configure the BinderHub service to: (https://console.cloud.google.com/iam-admin/serviceaccounts/create) - make sure you are in the correct project again). You may also need appropriate permissions to set this up. Give it a name (same as the name you used for the chart installation, but with a '-pusher' suffix) and click 'Create and Continue'. - In the next step, select 'Artifact Registry Writer' as a role. Click "Next". In the final step, just click "Done". + In the next step, select 'Artifact Registry Writer' as a role. Click "Continue". In the final step, just click "Done". 7. Now that the service account is created, find it in the list and open it. You will find a tab named 'Keys' once the informational display opens - select that. Click @@ -130,10 +130,9 @@ The documentation should help configure the BinderHub service to: 1. `` is what you copied from step 5. 2. `` is the JSON file you downloaded in step 7. - This is a multi-line file - either indent it correctly to match up (the `|` - allows multiline strings), - or simply edit the contents to be a single line. Since it is JSON, - it does not matter. + This is a multi-line file and will need to be indented correctly. The `|` after + `password` allows the value to be multi-line, and each line should be indented at least + 2 spaces from `password`. 3. `` is the region your artifact repository was created in. You can see this in the first part of `` as well. @@ -196,7 +195,9 @@ section of z2jh. This provides two things: a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to - expose the service to the external world without needing its own loadbalancer or ingress. + expose the service to the external world using JupyterHub's ingress / loadbalancer, without + needint a dedicated ingress / loadbalancer for BinderHub. + b. (Eventually) Appropriate credentials for authenticated network calls between these two services. To make this connection, we need to tell JupyterHub where to find BinderHub. Eventually @@ -205,22 +206,34 @@ section of z2jh. the following command: ```bash - kubectl -n get svc -l app.kubernetes.io/name=binderhub-service -o name + kubectl -n get svc -l app.kubernetes.io/name=binderhub-service ``` - Make a note of this, we will use it in the next step. + Make a note of the name under the `NAME` column, we will use it in the next step. 3. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. ```yaml - services: - binder: - # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 - # for something more readable and requiring less copy-pasting - url: http://{{ service name from step 2}} + hub: + services: + binder: + # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 + # for something more readable and requiring less copy-pasting + url: http://{{ service name from step 2}} + ``` + +4. Find the latest version of the z2jh helm chart. The easiest way is to run the + following command: + + ```bash + helm search repo jupyterhub ``` -4. Install the JupyterHub helm chart with the following command: + This should output a few columns. Look for the version under **CHART VERSION** (not *APP VERSION*) + for `jupyterhub/jupyterhub`. That's the latest z2jh chart version, and that is what + we will be using. + +5. Install the JupyterHub helm chart with the following command: ```bash helm upgrade --cleanup-on-fail \ @@ -239,19 +252,19 @@ section of z2jh. - `` is the _same_ namespace used for the BinderHub install - `` is the latest stable version of the JupyterHub - helm chart, available from [the chart repository](https://hub.jupyter.org/helm-chart/). + helm chart, determined in the previous step. -5. Find the external IP on which the JupyterHub is accessible: +6. Find the external IP on which the JupyterHub is accessible: ```bash kubectl -n get svc proxy-public ``` -6. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder`. - You should see an unstyled, somewhat broken 404 page. This is great and expected. Let's fix - that. +7. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder/` (the + trailing slash is *important*). You should see an unstyled, somewhat broken + 404 page. This is great and expected. Let's fix that. -7. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now +8. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now be available under `/services/binder`. ```yaml @@ -262,9 +275,10 @@ section of z2jh. Deploy this using the `helm upgrade` command from step 9 in the previous section. -8. Test by going to `http://{{ external ip from step 5}}/services/binder` again, and you - should see a _styled_ 404 page! Success - this means BinderHub is now connected to - JupyterHub, even if the end users can't see it yet. Let's connect them! +9. Test by going to `http://{{ external ip from step 5}}/services/binder/` (the trailing slash + is *important*!) again, and you should see a _styled_ 404 page! Success - + this means BinderHub is now connected to JupyterHub, even if the end users + can't see it yet. Let's connect them! ## Connect with `jupyterhub-fancy-profiles` @@ -277,20 +291,26 @@ allowing them to build their own images the same way they would on `mybinder.org easiest way to do this is to use one of the pre-built images provided by the `jupyterhub-fancy-profiles` project. In the [list of tags](https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags), select the latest tag that also includes the version of the z2jh chart you are - using (the `version` specified in step 4 of the previous step). Once you - select the tag, add it to the `z2jh-config.yaml` file: + using (the `version` specified in step 4 of the previous step). This is *most likely* + the tag on the top of the page, and looks something like `z2jh-v{{ z2jh version }}-fancy-profiles-sha-{{ some string}}`. + + Once you find the tag, *modify* the `z2jh-config.yaml` file to enable `jupyterhub-fancy-profiles`. + While it is hidden here for clarity, make sure to preserve the `hub.services` section that + you added in step 3 of the previous section while editing this file. ```yaml hub: + services: + ... image: # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles - tag: # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" + tag: "" # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" - extraConfig: - enable-fancy-profiles: | - from jupyterhub_fancy_profiles import setup_ui - setup_ui(c) + extraConfig: + enable-fancy-profiles: | + from jupyterhub_fancy_profiles import setup_ui + setup_ui(c) ``` 2. Since `jupyterhub-fancy-profiles` adds on to the [profileList](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) @@ -305,7 +325,7 @@ allowing them to build their own images the same way they would on `mybinder.org profile_options: image: display_name: Image - unlisted_choice: &profile_list_unlisted_choice + unlisted_choice: enabled: True display_name: "Custom image" validation_regex: "^.+:.+$" @@ -327,14 +347,19 @@ allowing them to build their own images the same way they would on `mybinder.org image: jupyter/scipy-notebook:2023-06-26 ``` -``` - -3. Deploy, using the command from step 3 of the section above. +3. Deploy, using the command from step 5 of the section above. 4. Access the JupyterHub itself, using the external IP you got from step 5 of the section - above. You should see a UI that allows you to choose two pre-existing images (pangeo and scipy), - specify your own image, or 'build' your own image. The last option lets you access the binder functionality! - Test it out :) + above (not `{{ hub IP }}/services/binder/`). Once you log in (you can use *any* username + and password), you should see a UI that allows you to choose two pre-existing + images (pangeo and scipy), specify your own image, or 'build' your own image. + The last option lets you access the binder functionality! Test it out :) + +From now on, you can customize this JupyterHub as you would any other JupyterHub set up +using z2jh. The [customization guide](https://z2jh.jupyter.org/en/stable/jupyterhub/customization.html) +contains many helpful. In particular, you probably want to set up more restrictive +[authentication](https://z2jh.jupyter.org/en/stable/administrator/authentication.html) so +not everyone can log in to your hub! ## Funding From 4e3941f2bb89b18859b947f56f7b268692802453 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 00:33:00 +0000 Subject: [PATCH 100/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index cc828c633..44b0297c9 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,8 @@ section of z2jh. This provides two things: a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to - expose the service to the external world using JupyterHub's ingress / loadbalancer, without - needint a dedicated ingress / loadbalancer for BinderHub. + expose the service to the external world using JupyterHub's ingress / loadbalancer, without + needint a dedicated ingress / loadbalancer for BinderHub. b. (Eventually) Appropriate credentials for authenticated network calls between these two services. @@ -215,11 +215,11 @@ section of z2jh. ```yaml hub: - services: - binder: - # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 - # for something more readable and requiring less copy-pasting - url: http://{{ service name from step 2}} + services: + binder: + # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 + # for something more readable and requiring less copy-pasting + url: http://{{ service name from step 2}} ``` 4. Find the latest version of the z2jh helm chart. The easiest way is to run the @@ -229,7 +229,7 @@ section of z2jh. helm search repo jupyterhub ``` - This should output a few columns. Look for the version under **CHART VERSION** (not *APP VERSION*) + This should output a few columns. Look for the version under **CHART VERSION** (not _APP VERSION_) for `jupyterhub/jupyterhub`. That's the latest z2jh chart version, and that is what we will be using. @@ -261,7 +261,7 @@ section of z2jh. ``` 7. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder/` (the - trailing slash is *important*). You should see an unstyled, somewhat broken + trailing slash is _important_). You should see an unstyled, somewhat broken 404 page. This is great and expected. Let's fix that. 8. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now @@ -276,7 +276,7 @@ section of z2jh. Deploy this using the `helm upgrade` command from step 9 in the previous section. 9. Test by going to `http://{{ external ip from step 5}}/services/binder/` (the trailing slash - is *important*!) again, and you should see a _styled_ 404 page! Success - + is _important_!) again, and you should see a _styled_ 404 page! Success - this means BinderHub is now connected to JupyterHub, even if the end users can't see it yet. Let's connect them! @@ -291,26 +291,25 @@ allowing them to build their own images the same way they would on `mybinder.org easiest way to do this is to use one of the pre-built images provided by the `jupyterhub-fancy-profiles` project. In the [list of tags](https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags), select the latest tag that also includes the version of the z2jh chart you are - using (the `version` specified in step 4 of the previous step). This is *most likely* + using (the `version` specified in step 4 of the previous step). This is _most likely_ the tag on the top of the page, and looks something like `z2jh-v{{ z2jh version }}-fancy-profiles-sha-{{ some string}}`. - Once you find the tag, *modify* the `z2jh-config.yaml` file to enable `jupyterhub-fancy-profiles`. + Once you find the tag, _modify_ the `z2jh-config.yaml` file to enable `jupyterhub-fancy-profiles`. While it is hidden here for clarity, make sure to preserve the `hub.services` section that you added in step 3 of the previous section while editing this file. ```yaml hub: - services: - ... + services: ... image: - # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags - name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles - tag: "" # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" + # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags + name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles + tag: "" # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" extraConfig: - enable-fancy-profiles: | - from jupyterhub_fancy_profiles import setup_ui - setup_ui(c) + enable-fancy-profiles: | + from jupyterhub_fancy_profiles import setup_ui + setup_ui(c) ``` 2. Since `jupyterhub-fancy-profiles` adds on to the [profileList](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) @@ -350,10 +349,10 @@ allowing them to build their own images the same way they would on `mybinder.org 3. Deploy, using the command from step 5 of the section above. 4. Access the JupyterHub itself, using the external IP you got from step 5 of the section - above (not `{{ hub IP }}/services/binder/`). Once you log in (you can use *any* username + above (not `{{ hub IP }}/services/binder/`). Once you log in (you can use _any_ username and password), you should see a UI that allows you to choose two pre-existing images (pangeo and scipy), specify your own image, or 'build' your own image. - The last option lets you access the binder functionality! Test it out :) + The last option lets you access the binder functionality! Test it out :) From now on, you can customize this JupyterHub as you would any other JupyterHub set up using z2jh. The [customization guide](https://z2jh.jupyter.org/en/stable/jupyterhub/customization.html) @@ -366,4 +365,7 @@ not everyone can log in to your hub! Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with NFDI4DS [460234259](https://gepris.dfg.de/gepris/projekt/460234259?context=projekt&task=showDetail&id=460234259&) and [CESSDA](https://www.cessda.eu). + +``` + ``` From 3fd9ca8f2f40cbbbebff2189f475b357da37f78f Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Thu, 8 Feb 2024 16:37:31 -0800 Subject: [PATCH 101/200] Remove stray backticks --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 44b0297c9..c0d529869 100644 --- a/README.md +++ b/README.md @@ -364,8 +364,4 @@ not everyone can log in to your hub! Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with NFDI4DS [460234259](https://gepris.dfg.de/gepris/projekt/460234259?context=projekt&task=showDetail&id=460234259&) -and [CESSDA](https://www.cessda.eu). - -``` - -``` +and [CESSDA](https://www.cessda.eu). \ No newline at end of file From d05956f9b58005bbd5cab96389c9adeabf073ec9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 00:37:46 +0000 Subject: [PATCH 102/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c0d529869..21ab41616 100644 --- a/README.md +++ b/README.md @@ -364,4 +364,4 @@ not everyone can log in to your hub! Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with NFDI4DS [460234259](https://gepris.dfg.de/gepris/projekt/460234259?context=projekt&task=showDetail&id=460234259&) -and [CESSDA](https://www.cessda.eu). \ No newline at end of file +and [CESSDA](https://www.cessda.eu). From a300b3a10d83e0247a940337e492068e9641382b Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Mon, 12 Feb 2024 13:25:54 -0800 Subject: [PATCH 103/200] Fix typo Co-authored-by: Sarah Gibson <44771837+sgibson91@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21ab41616..5d05951cf 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ section of z2jh. a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to expose the service to the external world using JupyterHub's ingress / loadbalancer, without - needint a dedicated ingress / loadbalancer for BinderHub. + needing a dedicated ingress / loadbalancer for BinderHub. b. (Eventually) Appropriate credentials for authenticated network calls between these two services. From f55932afb405ecef0f0c9390d822e67e54ad6405 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Mon, 12 Feb 2024 13:27:33 -0800 Subject: [PATCH 104/200] Complete sentence --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5d05951cf..3422a3698 100644 --- a/README.md +++ b/README.md @@ -356,9 +356,10 @@ allowing them to build their own images the same way they would on `mybinder.org From now on, you can customize this JupyterHub as you would any other JupyterHub set up using z2jh. The [customization guide](https://z2jh.jupyter.org/en/stable/jupyterhub/customization.html) -contains many helpful. In particular, you probably want to set up more restrictive -[authentication](https://z2jh.jupyter.org/en/stable/administrator/authentication.html) so -not everyone can log in to your hub! +contains many helpful examples of how you can customize your hub. In particular, +you probably want to set up more restrictive +[authentication](https://z2jh.jupyter.org/en/stable/administrator/authentication.html) +so not everyone can log in to your hub! ## Funding From f9e898b754da354b5f01147f5a181a48319549aa Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Fri, 1 Mar 2024 05:03:11 +0000 Subject: [PATCH 105/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 30 ++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index b7b369635..7a8138379 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -13,10 +13,12 @@ attrs==23.2.0 # jsonschema # referencing binderhub @ git+https://github.com/jupyterhub/binderhub@0dde0c044e70b89ee7bceac9eac73928d215290c - # via -r requirements.in -cachetools==5.3.2 + # via + # -r requirements.in + # binderhub +cachetools==5.3.3 # via google-auth -certifi==2023.11.17 +certifi==2024.2.2 # via # kubernetes # requests @@ -26,13 +28,13 @@ cffi==1.16.0 # via cryptography charset-normalizer==3.3.2 # via requests -cryptography==42.0.2 +cryptography==42.0.5 # via pyopenssl docker==7.0.0 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.27.0 +google-auth==2.28.1 # via kubernetes greenlet==3.0.3 # via sqlalchemy @@ -56,7 +58,7 @@ kubernetes==29.0.0 # via binderhub mako==1.3.2 # via alembic -markupsafe==2.1.4 +markupsafe==2.1.5 # via # jinja2 # mako @@ -71,7 +73,7 @@ packaging==23.2 # jupyterhub pamela==1.1.0 # via jupyterhub -prometheus-client==0.19.0 +prometheus-client==0.20.0 # via # binderhub # jupyterhub @@ -83,13 +85,13 @@ pyasn1-modules==0.3.0 # via google-auth pycparser==2.21 # via cffi -pycurl==7.45.2 +pycurl==7.45.3 # via binderhub pyjwt==2.8.0 # via binderhub pyopenssl==24.0.0 # via certipy -python-dateutil==2.8.2 +python-dateutil==2.9.0 # via # jupyterhub # kubernetes @@ -111,13 +113,13 @@ requests==2.31.0 # requests-oauthlib requests-oauthlib==1.3.1 # via kubernetes -rpds-py==0.17.1 +rpds-py==0.18.0 # via # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.18.5 +ruamel-yaml==0.18.6 # via jupyter-telemetry ruamel-yaml-clib==0.2.8 # via ruamel-yaml @@ -125,7 +127,7 @@ six==1.16.0 # via # kubernetes # python-dateutil -sqlalchemy==2.0.25 +sqlalchemy==2.0.27 # via # alembic # jupyterhub @@ -138,11 +140,11 @@ traitlets==5.14.1 # binderhub # jupyter-telemetry # jupyterhub -typing-extensions==4.9.0 +typing-extensions==4.10.0 # via # alembic # sqlalchemy -urllib3==2.2.0 +urllib3==2.2.1 # via # docker # kubernetes From 5d91c02c47a9f4aca5678ac107782a2f307b7b64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 05:40:54 +0000 Subject: [PATCH 106/200] build(deps): bump jupyterhub/action-k8s-await-workloads from 2 to 3 Bumps [jupyterhub/action-k8s-await-workloads](https://github.com/jupyterhub/action-k8s-await-workloads) from 2 to 3. - [Release notes](https://github.com/jupyterhub/action-k8s-await-workloads/releases) - [Changelog](https://github.com/jupyterhub/action-k8s-await-workloads/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyterhub/action-k8s-await-workloads/compare/v2...v3) --- updated-dependencies: - dependency-name: jupyterhub/action-k8s-await-workloads dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test-chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index 0d303be00..b13d72ae6 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -187,7 +187,7 @@ jobs: --set dockerApi.extraFiles.daemon-json.data.insecure-registries[0]=$LOCAL_REGISTRY_HOST # ref: https://github.com/jupyterhub/action-k8s-await-workloads - - uses: jupyterhub/action-k8s-await-workloads@v2 + - uses: jupyterhub/action-k8s-await-workloads@v3 with: timeout: 150 max-restarts: 1 From 976515a826fbc582c57b9e1efed4e73edb313c9a Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Tue, 5 Mar 2024 15:54:44 +0200 Subject: [PATCH 107/200] Move installation from readme in rtd --- README.md | 318 --------------------------------- docs/source/howto/index.md | 18 ++ docs/source/howto/install.md | 317 ++++++++++++++++++++++++++++++++ docs/source/tutorials/index.md | 18 ++ 4 files changed, 353 insertions(+), 318 deletions(-) create mode 100644 docs/source/howto/index.md create mode 100644 docs/source/howto/install.md create mode 100644 docs/source/tutorials/index.md diff --git a/README.md b/README.md index 3422a3698..2ece99539 100644 --- a/README.md +++ b/README.md @@ -43,324 +43,6 @@ The documentation should help configure the BinderHub service to: [persistent binderhub chart]: https://github.com/gesiscss/persistent_binderhub [was added]: https://github.com/jupyterhub/binderhub/pull/666 -## Installation - -1. Add the `binderhub-service` chart repository to helm: - - ```bash - helm repo add binderhub-service https://2i2c.org/binderhub-service - helm repo update - ``` - - Note this URL will change eventually, as binderhub-service is designed - to be a generic service, not something for use only by 2i2c. - -2. Install the latest development version of `binderhub-service` into a - namespace. - - ```bash - helm upgrade \ - --install \ - --create-namespace \ - --devel \ - --wait \ - --namespace \ - \ - binderhub-service/binderhub-service - ``` - - This sets up a binderhub service, but not in a publicly visible way. - -3. Test that it's running by port-forwarding to the correct pod: - - ```bash - kubectl -n port-forward $(kubectl -n get pod -l app.kubernetes.io/component=binderhub -o name) 8585:8585 - ``` - - This should forward requests on port 8585 on your localhost, to the binder service running inside the pod. So if you go - to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success!_. - -4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, - but binderhub supports using other registries. - - Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo). Make sure you're in the correct project (look at the drop - down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. - - In the repository creation page, give it a name (ideally same name you are using for - dedicated to the chart installation), select 'Docker' as the format, 'Standard' as the mode, 'Region' - as the location type and select the same region your kubernetes cluster is in. The - settings about encryption and other options can be left in their default. Hit "Create". - -5. Find the full path of the repository you just created, by opening it in the list - and looking for the small 'copy' icon next to the name of the repository. If you - hit it, it should copy something like `-docker.pkg.dev//`. - Save this. - -6. Create a Google Cloud Service Account that has permissions to push to this - repository ([via this URL] - (https://console.cloud.google.com/iam-admin/serviceaccounts/create) - make - sure you are in the correct project again). You may also need appropriate permissions to set this up. Give it a name (same as the name you used - for the chart installation, but with a '-pusher' suffix) and click 'Create and Continue'. - In the next step, select 'Artifact Registry Writer' as a role. Click "Continue". In the final step, just click "Done". - -7. Now that the service account is created, find it in the list and open it. You will - find a tab named 'Keys' once the informational display opens - select that. Click - 'Add Key' -> 'Create New Key'. In the dialog box that pops up, select 'JSON' as the - key type and click 'Create'. This should download a key file. **Keep this file safe**! - -8. Now that we have the appropriate permissions, let's set up our configuration! Create a - new file named `binderhub-service-config.yaml` with the following contents: - - ```yaml - config: - BinderHub: - use_registry: true - image_prefix: /binder - # Temporarily enable the binderhub UI so we can test image building and pushing - enable_api_only_mode: false - buildPodsRegistryCredentials: - server: "https://-docker.pkg.dev" - username: "_json_key" - password: | - - ``` - - where: - - 1. `` is what you copied from step 5. - - 2. `` is the JSON file you downloaded in step 7. - This is a multi-line file and will need to be indented correctly. The `|` after - `password` allows the value to be multi-line, and each line should be indented at least - 2 spaces from `password`. - - 3. `` is the region your artifact repository was created in. You can see - this in the first part of `` as well. - -9. Run a `helm upgrade` to use the new configuration you just created: - - ```bash - helm upgrade \ - --install \ - --create-namespace \ - --devel \ - --wait \ - --namespace - \ - binderhub-service/binderhub-service \ - --values binderhub-service-config.yaml - ``` - - This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, - you will see that the binderhub pod has restarted - this confirms that the config has been set up! - -10. Let's verify that _image building and pushing_ works. Access the binderhub pod by following the - same instructions as step 3. But this time, you should see a binderhub page very similar to that - on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying - out `binder-examples/requirements`. It might take a while to build, but you should be able to see - logs in the UI. It should succeed at _pushing_ the github image, but will fail to launch. The last - lines in the log in the UI should look like: - - ``` - Successfully pushed europe-west10-docker.pkg.dev/binderhub-service-development/bh-service-test/binderbinder-2dexamples-2drequirements-55ab5c:50533eb470ee6c24e872043d30b2fee463d6943fBuilt image, launching... - Launching server... - Launch attempt 1 failed, retrying... - Launch attempt 2 failed, retrying... - ``` - - You can also go back to the Google Artifact Registry repository you created earlier to verify that the built - image is indeed there. - -11. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. - Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy - using the command from step 9. - -## Connect with a JupyterHub installation - -Next, let's connect this to a JupyterHub set up via [z2jh](https://z2jh.jupyter.org/). While any JupyterHub -that can run containers will work with this, the _most common_ setup is to use this with z2jh. The first -few steps are lifted directly from the [install JupyterHub](https://z2jh.jupyter.org/en/stable/jupyterhub/installation.html) -section of z2jh. - -1. Add the z2jh chart repository to helm: - - ``` - helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ - helm repo update - ``` - -2. We want the binderhub to be available under `http://{{hub url}}/services/binder`, because - that is what `jupyterhub-fancy-profiles` expects. Eventually we would also want authentication - to work correctly. For that, we must set up binderhub as a [JupyterHub Service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html). - This provides two things: - - a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to - expose the service to the external world using JupyterHub's ingress / loadbalancer, without - needing a dedicated ingress / loadbalancer for BinderHub. - - b. (Eventually) Appropriate credentials for authenticated network calls between these two services. - - To make this connection, we need to tell JupyterHub where to find BinderHub. Eventually - this can be automatic (once [this issue](https://github.com/2i2c-org/binderhub-service/issues/57) - gets resolved). In the meantime, you can get the name of the BinderHub service by executing - the following command: - - ```bash - kubectl -n get svc -l app.kubernetes.io/name=binderhub-service - ``` - - Make a note of the name under the `NAME` column, we will use it in the next step. - -3. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. - - ```yaml - hub: - services: - binder: - # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 - # for something more readable and requiring less copy-pasting - url: http://{{ service name from step 2}} - ``` - -4. Find the latest version of the z2jh helm chart. The easiest way is to run the - following command: - - ```bash - helm search repo jupyterhub - ``` - - This should output a few columns. Look for the version under **CHART VERSION** (not _APP VERSION_) - for `jupyterhub/jupyterhub`. That's the latest z2jh chart version, and that is what - we will be using. - -5. Install the JupyterHub helm chart with the following command: - - ```bash - helm upgrade --cleanup-on-fail \ - --install jupyterhub/jupyterhub \ - --namespace \ - --version= \ - --values z2jh-config.yaml \ - --wait - ``` - - where: - - - `` is any name you can use to refer to this imag - (like `jupyterhub`) - - - `` is the _same_ namespace used for the BinderHub install - - - `` is the latest stable version of the JupyterHub - helm chart, determined in the previous step. - -6. Find the external IP on which the JupyterHub is accessible: - - ```bash - kubectl -n get svc proxy-public - ``` - -7. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder/` (the - trailing slash is _important_). You should see an unstyled, somewhat broken - 404 page. This is great and expected. Let's fix that. - -8. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now - be available under `/services/binder`. - - ```yaml - config: - BinderHub: - base_url: /services/binder - ``` - - Deploy this using the `helm upgrade` command from step 9 in the previous section. - -9. Test by going to `http://{{ external ip from step 5}}/services/binder/` (the trailing slash - is _important_!) again, and you should see a _styled_ 404 page! Success - - this means BinderHub is now connected to JupyterHub, even if the end users - can't see it yet. Let's connect them! - -## Connect with `jupyterhub-fancy-profiles` - -The [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) -project provides a user facing frontend for connecting the JupyterHub to BinderHub, -allowing them to build their own images the same way they would on `mybinder.org`! - -1. First, we need to install the `jupyterhub-fancy-profiles` package in the container - that is running the JupyterHub process itself (not the user containers). The - easiest way to do this is to use one of the pre-built images provided by - the `jupyterhub-fancy-profiles` project. In the [list of tags](https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags), - select the latest tag that also includes the version of the z2jh chart you are - using (the `version` specified in step 4 of the previous step). This is _most likely_ - the tag on the top of the page, and looks something like `z2jh-v{{ z2jh version }}-fancy-profiles-sha-{{ some string}}`. - - Once you find the tag, _modify_ the `z2jh-config.yaml` file to enable `jupyterhub-fancy-profiles`. - While it is hidden here for clarity, make sure to preserve the `hub.services` section that - you added in step 3 of the previous section while editing this file. - - ```yaml - hub: - services: ... - image: - # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags - name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles - tag: "" # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" - - extraConfig: - enable-fancy-profiles: | - from jupyterhub_fancy_profiles import setup_ui - setup_ui(c) - ``` - -2. Since `jupyterhub-fancy-profiles` adds on to the [profileList](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) - feature of KubeSpawner, we need to configure a profile list here as well. - Add this to the `z2jh-config.yaml` file: - - ```yaml - singleuser: - profileList: - - display_name: "Only Profile Available, this info is not shown in the UI" - slug: only-choice - profile_options: - image: - display_name: Image - unlisted_choice: - enabled: True - display_name: "Custom image" - validation_regex: "^.+:.+$" - validation_message: "Must be a publicly available docker image, of form :" - display_name_in_choices: "Specify an existing docker image" - description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" - kubespawner_override: - image: "{value}" - choices: - pangeo: - display_name: Pangeo Notebook Image - description: "Python image with scientific, dask and geospatial tools" - kubespawner_override: - image: pangeo/pangeo-notebook:2023.09.11 - scipy: - display_name: Jupyter SciPy Notebook - slug: scipy - kubespawner_override: - image: jupyter/scipy-notebook:2023-06-26 - ``` - -3. Deploy, using the command from step 5 of the section above. - -4. Access the JupyterHub itself, using the external IP you got from step 5 of the section - above (not `{{ hub IP }}/services/binder/`). Once you log in (you can use _any_ username - and password), you should see a UI that allows you to choose two pre-existing - images (pangeo and scipy), specify your own image, or 'build' your own image. - The last option lets you access the binder functionality! Test it out :) - -From now on, you can customize this JupyterHub as you would any other JupyterHub set up -using z2jh. The [customization guide](https://z2jh.jupyter.org/en/stable/jupyterhub/customization.html) -contains many helpful examples of how you can customize your hub. In particular, -you probably want to set up more restrictive -[authentication](https://z2jh.jupyter.org/en/stable/administrator/authentication.html) -so not everyone can log in to your hub! - ## Funding Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with diff --git a/docs/source/howto/index.md b/docs/source/howto/index.md new file mode 100644 index 000000000..f43370b40 --- /dev/null +++ b/docs/source/howto/index.md @@ -0,0 +1,18 @@ +(howto)= +# How-To Guides + +The documentation in this section is _goal-oriented_ and designed to guide the +reader through specific steps to reach that goal. + +```{note} +[Read more about the How-To quadrant of the diataxis framework.](https://diataxis.fr/how-to-guides/) +``` + +```{attention} +This documentation is a Work in Progress! +Please see our [contributing guide](contributing) if you'd like to add to it. +``` + +```{toctree} +install.md +``` diff --git a/docs/source/howto/install.md b/docs/source/howto/install.md new file mode 100644 index 000000000..8a33e3bbb --- /dev/null +++ b/docs/source/howto/install.md @@ -0,0 +1,317 @@ +# Installation + +1. Add the `binderhub-service` chart repository to helm: + + ```bash + helm repo add binderhub-service https://2i2c.org/binderhub-service + helm repo update + ``` + + Note this URL will change eventually, as binderhub-service is designed + to be a generic service, not something for use only by 2i2c. + +2. Install the latest development version of `binderhub-service` into a + namespace. + + ```bash + helm upgrade \ + --install \ + --create-namespace \ + --devel \ + --wait \ + --namespace \ + \ + binderhub-service/binderhub-service + ``` + + This sets up a binderhub service, but not in a publicly visible way. + +3. Test that it's running by port-forwarding to the correct pod: + + ```bash + kubectl -n port-forward $(kubectl -n get pod -l app.kubernetes.io/component=binderhub -o name) 8585:8585 + ``` + + This should forward requests on port 8585 on your localhost, to the binder service running inside the pod. So if you go + to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success!_. + +4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, + but binderhub supports using other registries. + + Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo). Make sure you're in the correct project (look at the drop + down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. + + In the repository creation page, give it a name (ideally same name you are using for + dedicated to the chart installation), select 'Docker' as the format, 'Standard' as the mode, 'Region' + as the location type and select the same region your kubernetes cluster is in. The + settings about encryption and other options can be left in their default. Hit "Create". + +5. Find the full path of the repository you just created, by opening it in the list + and looking for the small 'copy' icon next to the name of the repository. If you + hit it, it should copy something like `-docker.pkg.dev//`. + Save this. + +6. Create a Google Cloud Service Account that has permissions to push to this + repository ([via this URL] + (https://console.cloud.google.com/iam-admin/serviceaccounts/create) - make + sure you are in the correct project again). You may also need appropriate permissions to set this up. Give it a name (same as the name you used + for the chart installation, but with a '-pusher' suffix) and click 'Create and Continue'. + In the next step, select 'Artifact Registry Writer' as a role. Click "Continue". In the final step, just click "Done". + +7. Now that the service account is created, find it in the list and open it. You will + find a tab named 'Keys' once the informational display opens - select that. Click + 'Add Key' -> 'Create New Key'. In the dialog box that pops up, select 'JSON' as the + key type and click 'Create'. This should download a key file. **Keep this file safe**! + +8. Now that we have the appropriate permissions, let's set up our configuration! Create a + new file named `binderhub-service-config.yaml` with the following contents: + + ```yaml + config: + BinderHub: + use_registry: true + image_prefix: /binder + # Temporarily enable the binderhub UI so we can test image building and pushing + enable_api_only_mode: false + buildPodsRegistryCredentials: + server: "https://-docker.pkg.dev" + username: "_json_key" + password: | + + ``` + + where: + + 1. `` is what you copied from step 5. + + 2. `` is the JSON file you downloaded in step 7. + This is a multi-line file and will need to be indented correctly. The `|` after + `password` allows the value to be multi-line, and each line should be indented at least + 2 spaces from `password`. + + 3. `` is the region your artifact repository was created in. You can see + this in the first part of `` as well. + +9. Run a `helm upgrade` to use the new configuration you just created: + + ```bash + helm upgrade \ + --install \ + --create-namespace \ + --devel \ + --wait \ + --namespace + \ + binderhub-service/binderhub-service \ + --values binderhub-service-config.yaml + ``` + + This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, + you will see that the binderhub pod has restarted - this confirms that the config has been set up! + +10. Let's verify that _image building and pushing_ works. Access the binderhub pod by following the + same instructions as step 3. But this time, you should see a binderhub page very similar to that + on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying + out `binder-examples/requirements`. It might take a while to build, but you should be able to see + logs in the UI. It should succeed at _pushing_ the github image, but will fail to launch. The last + lines in the log in the UI should look like: + + ``` + Successfully pushed europe-west10-docker.pkg.dev/binderhub-service-development/bh-service-test/binderbinder-2dexamples-2drequirements-55ab5c:50533eb470ee6c24e872043d30b2fee463d6943fBuilt image, launching... + Launching server... + Launch attempt 1 failed, retrying... + Launch attempt 2 failed, retrying... + ``` + + You can also go back to the Google Artifact Registry repository you created earlier to verify that the built + image is indeed there. + +11. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. + Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy + using the command from step 9. + +## Connect with a JupyterHub installation + +Next, let's connect this to a JupyterHub set up via [z2jh](https://z2jh.jupyter.org/). While any JupyterHub +that can run containers will work with this, the _most common_ setup is to use this with z2jh. The first +few steps are lifted directly from the [install JupyterHub](https://z2jh.jupyter.org/en/stable/jupyterhub/installation.html) +section of z2jh. + +1. Add the z2jh chart repository to helm: + + ``` + helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ + helm repo update + ``` + +2. We want the binderhub to be available under `http://{{hub url}}/services/binder`, because + that is what `jupyterhub-fancy-profiles` expects. Eventually we would also want authentication + to work correctly. For that, we must set up binderhub as a [JupyterHub Service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html). + This provides two things: + + a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to + expose the service to the external world using JupyterHub's ingress / loadbalancer, without + needing a dedicated ingress / loadbalancer for BinderHub. + + b. (Eventually) Appropriate credentials for authenticated network calls between these two services. + + To make this connection, we need to tell JupyterHub where to find BinderHub. Eventually + this can be automatic (once [this issue](https://github.com/2i2c-org/binderhub-service/issues/57) + gets resolved). In the meantime, you can get the name of the BinderHub service by executing + the following command: + + ```bash + kubectl -n get svc -l app.kubernetes.io/name=binderhub-service + ``` + + Make a note of the name under the `NAME` column, we will use it in the next step. + +3. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. + + ```yaml + hub: + services: + binder: + # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 + # for something more readable and requiring less copy-pasting + url: http://{{ service name from step 2}} + ``` + +4. Find the latest version of the z2jh helm chart. The easiest way is to run the + following command: + + ```bash + helm search repo jupyterhub + ``` + + This should output a few columns. Look for the version under **CHART VERSION** (not _APP VERSION_) + for `jupyterhub/jupyterhub`. That's the latest z2jh chart version, and that is what + we will be using. + +5. Install the JupyterHub helm chart with the following command: + + ```bash + helm upgrade --cleanup-on-fail \ + --install jupyterhub/jupyterhub \ + --namespace \ + --version= \ + --values z2jh-config.yaml \ + --wait + ``` + + where: + + - `` is any name you can use to refer to this imag + (like `jupyterhub`) + + - `` is the _same_ namespace used for the BinderHub install + + - `` is the latest stable version of the JupyterHub + helm chart, determined in the previous step. + +6. Find the external IP on which the JupyterHub is accessible: + + ```bash + kubectl -n get svc proxy-public + ``` + +7. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder/` (the + trailing slash is _important_). You should see an unstyled, somewhat broken + 404 page. This is great and expected. Let's fix that. + +8. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now + be available under `/services/binder`. + + ```yaml + config: + BinderHub: + base_url: /services/binder + ``` + + Deploy this using the `helm upgrade` command from step 9 in the previous section. + +9. Test by going to `http://{{ external ip from step 5}}/services/binder/` (the trailing slash + is _important_!) again, and you should see a _styled_ 404 page! Success - + this means BinderHub is now connected to JupyterHub, even if the end users + can't see it yet. Let's connect them! + +## Connect with `jupyterhub-fancy-profiles` + +The [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) +project provides a user facing frontend for connecting the JupyterHub to BinderHub, +allowing them to build their own images the same way they would on `mybinder.org`! + +1. First, we need to install the `jupyterhub-fancy-profiles` package in the container + that is running the JupyterHub process itself (not the user containers). The + easiest way to do this is to use one of the pre-built images provided by + the `jupyterhub-fancy-profiles` project. In the [list of tags](https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags), + select the latest tag that also includes the version of the z2jh chart you are + using (the `version` specified in step 4 of the previous step). This is _most likely_ + the tag on the top of the page, and looks something like `z2jh-v{{ z2jh version }}-fancy-profiles-sha-{{ some string}}`. + + Once you find the tag, _modify_ the `z2jh-config.yaml` file to enable `jupyterhub-fancy-profiles`. + While it is hidden here for clarity, make sure to preserve the `hub.services` section that + you added in step 3 of the previous section while editing this file. + + ```yaml + hub: + services: ... + image: + # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags + name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles + tag: "" # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" + + extraConfig: + enable-fancy-profiles: | + from jupyterhub_fancy_profiles import setup_ui + setup_ui(c) + ``` + +2. Since `jupyterhub-fancy-profiles` adds on to the [profileList](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) + feature of KubeSpawner, we need to configure a profile list here as well. + Add this to the `z2jh-config.yaml` file: + + ```yaml + singleuser: + profileList: + - display_name: "Only Profile Available, this info is not shown in the UI" + slug: only-choice + profile_options: + image: + display_name: Image + unlisted_choice: + enabled: True + display_name: "Custom image" + validation_regex: "^.+:.+$" + validation_message: "Must be a publicly available docker image, of form :" + display_name_in_choices: "Specify an existing docker image" + description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" + kubespawner_override: + image: "{value}" + choices: + pangeo: + display_name: Pangeo Notebook Image + description: "Python image with scientific, dask and geospatial tools" + kubespawner_override: + image: pangeo/pangeo-notebook:2023.09.11 + scipy: + display_name: Jupyter SciPy Notebook + slug: scipy + kubespawner_override: + image: jupyter/scipy-notebook:2023-06-26 + ``` + +3. Deploy, using the command from step 5 of the section above. + +4. Access the JupyterHub itself, using the external IP you got from step 5 of the section + above (not `{{ hub IP }}/services/binder/`). Once you log in (you can use _any_ username + and password), you should see a UI that allows you to choose two pre-existing + images (pangeo and scipy), specify your own image, or 'build' your own image. + The last option lets you access the binder functionality! Test it out :) + +From now on, you can customize this JupyterHub as you would any other JupyterHub set up +using z2jh. The [customization guide](https://z2jh.jupyter.org/en/stable/jupyterhub/customization.html) +contains many helpful examples of how you can customize your hub. In particular, +you probably want to set up more restrictive +[authentication](https://z2jh.jupyter.org/en/stable/administrator/authentication.html) +so not everyone can log in to your hub! diff --git a/docs/source/tutorials/index.md b/docs/source/tutorials/index.md new file mode 100644 index 000000000..aeb8101eb --- /dev/null +++ b/docs/source/tutorials/index.md @@ -0,0 +1,18 @@ +(tutorials)= +# Tutorials + +The documentation in this section are step-by-step guides that lead the reader +through completing a specific task. + +```{note} +[Read more about the Tutorials quadrant of the diataxis framework.](https://diataxis.fr/tutorials/) +``` + +```{attention} +This documentation is a Work in Progress! +Please see our [contributing guide](contributing) if you'd like to add to it. +``` + +```{toctree} +:maxdepth: 2 +``` From 974337b6306caddee3ff4d42bfdaba963ba43a84 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Tue, 5 Mar 2024 15:55:48 +0200 Subject: [PATCH 108/200] Add implementation details --- docs/source/explanation/implementation.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/source/explanation/implementation.md b/docs/source/explanation/implementation.md index cb77e3c65..6f9bbd7f3 100644 --- a/docs/source/explanation/implementation.md +++ b/docs/source/explanation/implementation.md @@ -1,5 +1,13 @@ # Implementation +The [binderhub-service](https://github.com/2i2c-org/binderhub-service/) Helm chart runs BinderHub, the Python software, as a standalone service to build and push images with [repo2docker](https://github.com/jupyterhub/repo2docker), next to JupyterHub. + +The `binderhub-service` installation starts a `docker-api` pod on each of the user nodes via the following [DaemonSet definition](https://github.com/2i2c-org/binderhub-service/blob/main/binderhub-service/templates/docker-api/daemonset.yaml). + +The `docker-api` pod setups and starts the [dockerd](https://docs.docker.com/engine/reference/commandline/dockerd/) daemon, that will then be accessible via a mounted unix socket on the node, by the `build pods`. + +The `build pods` are created as a result of an image build request, and they must run on the same node as the builder pods to make use of the docker daemon. These pods mount a k8s Secret with the docker config file holding the necessary registry credentials so they can push to the container registry. + ## Technical stack [jupyterhub]: https://jupyterhub.readthedocs.io/en/stable/ From 7ff824ea31bcc14b7b4ac81885021c6de0341331 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Thu, 7 Mar 2024 10:22:03 +0200 Subject: [PATCH 109/200] Move the instalation guide into the tutorials section --- docs/source/howto/index.md | 1 - docs/source/howto/install.md | 317 --------------------------------- docs/source/index.md | 1 + docs/source/tutorials/index.md | 2 + 4 files changed, 3 insertions(+), 318 deletions(-) delete mode 100644 docs/source/howto/install.md diff --git a/docs/source/howto/index.md b/docs/source/howto/index.md index f43370b40..0f314c739 100644 --- a/docs/source/howto/index.md +++ b/docs/source/howto/index.md @@ -14,5 +14,4 @@ Please see our [contributing guide](contributing) if you'd like to add to it. ``` ```{toctree} -install.md ``` diff --git a/docs/source/howto/install.md b/docs/source/howto/install.md deleted file mode 100644 index 8a33e3bbb..000000000 --- a/docs/source/howto/install.md +++ /dev/null @@ -1,317 +0,0 @@ -# Installation - -1. Add the `binderhub-service` chart repository to helm: - - ```bash - helm repo add binderhub-service https://2i2c.org/binderhub-service - helm repo update - ``` - - Note this URL will change eventually, as binderhub-service is designed - to be a generic service, not something for use only by 2i2c. - -2. Install the latest development version of `binderhub-service` into a - namespace. - - ```bash - helm upgrade \ - --install \ - --create-namespace \ - --devel \ - --wait \ - --namespace \ - \ - binderhub-service/binderhub-service - ``` - - This sets up a binderhub service, but not in a publicly visible way. - -3. Test that it's running by port-forwarding to the correct pod: - - ```bash - kubectl -n port-forward $(kubectl -n get pod -l app.kubernetes.io/component=binderhub -o name) 8585:8585 - ``` - - This should forward requests on port 8585 on your localhost, to the binder service running inside the pod. So if you go - to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success!_. - -4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, - but binderhub supports using other registries. - - Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo). Make sure you're in the correct project (look at the drop - down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. - - In the repository creation page, give it a name (ideally same name you are using for - dedicated to the chart installation), select 'Docker' as the format, 'Standard' as the mode, 'Region' - as the location type and select the same region your kubernetes cluster is in. The - settings about encryption and other options can be left in their default. Hit "Create". - -5. Find the full path of the repository you just created, by opening it in the list - and looking for the small 'copy' icon next to the name of the repository. If you - hit it, it should copy something like `-docker.pkg.dev//`. - Save this. - -6. Create a Google Cloud Service Account that has permissions to push to this - repository ([via this URL] - (https://console.cloud.google.com/iam-admin/serviceaccounts/create) - make - sure you are in the correct project again). You may also need appropriate permissions to set this up. Give it a name (same as the name you used - for the chart installation, but with a '-pusher' suffix) and click 'Create and Continue'. - In the next step, select 'Artifact Registry Writer' as a role. Click "Continue". In the final step, just click "Done". - -7. Now that the service account is created, find it in the list and open it. You will - find a tab named 'Keys' once the informational display opens - select that. Click - 'Add Key' -> 'Create New Key'. In the dialog box that pops up, select 'JSON' as the - key type and click 'Create'. This should download a key file. **Keep this file safe**! - -8. Now that we have the appropriate permissions, let's set up our configuration! Create a - new file named `binderhub-service-config.yaml` with the following contents: - - ```yaml - config: - BinderHub: - use_registry: true - image_prefix: /binder - # Temporarily enable the binderhub UI so we can test image building and pushing - enable_api_only_mode: false - buildPodsRegistryCredentials: - server: "https://-docker.pkg.dev" - username: "_json_key" - password: | - - ``` - - where: - - 1. `` is what you copied from step 5. - - 2. `` is the JSON file you downloaded in step 7. - This is a multi-line file and will need to be indented correctly. The `|` after - `password` allows the value to be multi-line, and each line should be indented at least - 2 spaces from `password`. - - 3. `` is the region your artifact repository was created in. You can see - this in the first part of `` as well. - -9. Run a `helm upgrade` to use the new configuration you just created: - - ```bash - helm upgrade \ - --install \ - --create-namespace \ - --devel \ - --wait \ - --namespace - \ - binderhub-service/binderhub-service \ - --values binderhub-service-config.yaml - ``` - - This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, - you will see that the binderhub pod has restarted - this confirms that the config has been set up! - -10. Let's verify that _image building and pushing_ works. Access the binderhub pod by following the - same instructions as step 3. But this time, you should see a binderhub page very similar to that - on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying - out `binder-examples/requirements`. It might take a while to build, but you should be able to see - logs in the UI. It should succeed at _pushing_ the github image, but will fail to launch. The last - lines in the log in the UI should look like: - - ``` - Successfully pushed europe-west10-docker.pkg.dev/binderhub-service-development/bh-service-test/binderbinder-2dexamples-2drequirements-55ab5c:50533eb470ee6c24e872043d30b2fee463d6943fBuilt image, launching... - Launching server... - Launch attempt 1 failed, retrying... - Launch attempt 2 failed, retrying... - ``` - - You can also go back to the Google Artifact Registry repository you created earlier to verify that the built - image is indeed there. - -11. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. - Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy - using the command from step 9. - -## Connect with a JupyterHub installation - -Next, let's connect this to a JupyterHub set up via [z2jh](https://z2jh.jupyter.org/). While any JupyterHub -that can run containers will work with this, the _most common_ setup is to use this with z2jh. The first -few steps are lifted directly from the [install JupyterHub](https://z2jh.jupyter.org/en/stable/jupyterhub/installation.html) -section of z2jh. - -1. Add the z2jh chart repository to helm: - - ``` - helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ - helm repo update - ``` - -2. We want the binderhub to be available under `http://{{hub url}}/services/binder`, because - that is what `jupyterhub-fancy-profiles` expects. Eventually we would also want authentication - to work correctly. For that, we must set up binderhub as a [JupyterHub Service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html). - This provides two things: - - a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to - expose the service to the external world using JupyterHub's ingress / loadbalancer, without - needing a dedicated ingress / loadbalancer for BinderHub. - - b. (Eventually) Appropriate credentials for authenticated network calls between these two services. - - To make this connection, we need to tell JupyterHub where to find BinderHub. Eventually - this can be automatic (once [this issue](https://github.com/2i2c-org/binderhub-service/issues/57) - gets resolved). In the meantime, you can get the name of the BinderHub service by executing - the following command: - - ```bash - kubectl -n get svc -l app.kubernetes.io/name=binderhub-service - ``` - - Make a note of the name under the `NAME` column, we will use it in the next step. - -3. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. - - ```yaml - hub: - services: - binder: - # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 - # for something more readable and requiring less copy-pasting - url: http://{{ service name from step 2}} - ``` - -4. Find the latest version of the z2jh helm chart. The easiest way is to run the - following command: - - ```bash - helm search repo jupyterhub - ``` - - This should output a few columns. Look for the version under **CHART VERSION** (not _APP VERSION_) - for `jupyterhub/jupyterhub`. That's the latest z2jh chart version, and that is what - we will be using. - -5. Install the JupyterHub helm chart with the following command: - - ```bash - helm upgrade --cleanup-on-fail \ - --install jupyterhub/jupyterhub \ - --namespace \ - --version= \ - --values z2jh-config.yaml \ - --wait - ``` - - where: - - - `` is any name you can use to refer to this imag - (like `jupyterhub`) - - - `` is the _same_ namespace used for the BinderHub install - - - `` is the latest stable version of the JupyterHub - helm chart, determined in the previous step. - -6. Find the external IP on which the JupyterHub is accessible: - - ```bash - kubectl -n get svc proxy-public - ``` - -7. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder/` (the - trailing slash is _important_). You should see an unstyled, somewhat broken - 404 page. This is great and expected. Let's fix that. - -8. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now - be available under `/services/binder`. - - ```yaml - config: - BinderHub: - base_url: /services/binder - ``` - - Deploy this using the `helm upgrade` command from step 9 in the previous section. - -9. Test by going to `http://{{ external ip from step 5}}/services/binder/` (the trailing slash - is _important_!) again, and you should see a _styled_ 404 page! Success - - this means BinderHub is now connected to JupyterHub, even if the end users - can't see it yet. Let's connect them! - -## Connect with `jupyterhub-fancy-profiles` - -The [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) -project provides a user facing frontend for connecting the JupyterHub to BinderHub, -allowing them to build their own images the same way they would on `mybinder.org`! - -1. First, we need to install the `jupyterhub-fancy-profiles` package in the container - that is running the JupyterHub process itself (not the user containers). The - easiest way to do this is to use one of the pre-built images provided by - the `jupyterhub-fancy-profiles` project. In the [list of tags](https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags), - select the latest tag that also includes the version of the z2jh chart you are - using (the `version` specified in step 4 of the previous step). This is _most likely_ - the tag on the top of the page, and looks something like `z2jh-v{{ z2jh version }}-fancy-profiles-sha-{{ some string}}`. - - Once you find the tag, _modify_ the `z2jh-config.yaml` file to enable `jupyterhub-fancy-profiles`. - While it is hidden here for clarity, make sure to preserve the `hub.services` section that - you added in step 3 of the previous section while editing this file. - - ```yaml - hub: - services: ... - image: - # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags - name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles - tag: "" # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" - - extraConfig: - enable-fancy-profiles: | - from jupyterhub_fancy_profiles import setup_ui - setup_ui(c) - ``` - -2. Since `jupyterhub-fancy-profiles` adds on to the [profileList](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) - feature of KubeSpawner, we need to configure a profile list here as well. - Add this to the `z2jh-config.yaml` file: - - ```yaml - singleuser: - profileList: - - display_name: "Only Profile Available, this info is not shown in the UI" - slug: only-choice - profile_options: - image: - display_name: Image - unlisted_choice: - enabled: True - display_name: "Custom image" - validation_regex: "^.+:.+$" - validation_message: "Must be a publicly available docker image, of form :" - display_name_in_choices: "Specify an existing docker image" - description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" - kubespawner_override: - image: "{value}" - choices: - pangeo: - display_name: Pangeo Notebook Image - description: "Python image with scientific, dask and geospatial tools" - kubespawner_override: - image: pangeo/pangeo-notebook:2023.09.11 - scipy: - display_name: Jupyter SciPy Notebook - slug: scipy - kubespawner_override: - image: jupyter/scipy-notebook:2023-06-26 - ``` - -3. Deploy, using the command from step 5 of the section above. - -4. Access the JupyterHub itself, using the external IP you got from step 5 of the section - above (not `{{ hub IP }}/services/binder/`). Once you log in (you can use _any_ username - and password), you should see a UI that allows you to choose two pre-existing - images (pangeo and scipy), specify your own image, or 'build' your own image. - The last option lets you access the binder functionality! Test it out :) - -From now on, you can customize this JupyterHub as you would any other JupyterHub set up -using z2jh. The [customization guide](https://z2jh.jupyter.org/en/stable/jupyterhub/customization.html) -contains many helpful examples of how you can customize your hub. In particular, -you probably want to set up more restrictive -[authentication](https://z2jh.jupyter.org/en/stable/administrator/authentication.html) -so not everyone can log in to your hub! diff --git a/docs/source/index.md b/docs/source/index.md index 341fbd161..1b76d1deb 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -18,6 +18,7 @@ Write description here... ```{toctree} :maxdepth: 2 +howto/index ``` ## Reference diff --git a/docs/source/tutorials/index.md b/docs/source/tutorials/index.md index aeb8101eb..a83113022 100644 --- a/docs/source/tutorials/index.md +++ b/docs/source/tutorials/index.md @@ -15,4 +15,6 @@ Please see our [contributing guide](contributing) if you'd like to add to it. ```{toctree} :maxdepth: 2 + +install.md ``` From 34183eb5d6a28672a19924b3373c98b32a1ee1ab Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Thu, 7 Mar 2024 10:55:45 +0200 Subject: [PATCH 110/200] Add more structure per the diataxis framework Co-authored-by: Sarah Gibson <44771837+sgibson91@users.noreply.github.com> --- docs/source/contributing.md | 7 + docs/source/explanation/implementation.md | 5 +- docs/source/explanation/index.md | 20 ++ docs/source/howto/index.md | 1 + docs/source/index.md | 32 +-- docs/source/reference/index.md | 19 ++ docs/source/tutorials/index.md | 1 - docs/source/tutorials/install.md | 317 ++++++++++++++++++++++ 8 files changed, 383 insertions(+), 19 deletions(-) create mode 100644 docs/source/explanation/index.md create mode 100644 docs/source/reference/index.md create mode 100644 docs/source/tutorials/install.md diff --git a/docs/source/contributing.md b/docs/source/contributing.md index 854139a31..4a8bb0f34 100644 --- a/docs/source/contributing.md +++ b/docs/source/contributing.md @@ -1 +1,8 @@ +(contributing)= + # Contributing + +Hello and thank you for contributing to binderhub-service! We are really excited to have you here! +Below you'll find some useful tasks, guidelines, and instructions for working in this repository. + +Notice something that's missing? Please open an issue or file a pull request! \ No newline at end of file diff --git a/docs/source/explanation/implementation.md b/docs/source/explanation/implementation.md index 6f9bbd7f3..27a80a924 100644 --- a/docs/source/explanation/implementation.md +++ b/docs/source/explanation/implementation.md @@ -1,6 +1,6 @@ # Implementation -The [binderhub-service](https://github.com/2i2c-org/binderhub-service/) Helm chart runs BinderHub, the Python software, as a standalone service to build and push images with [repo2docker](https://github.com/jupyterhub/repo2docker), next to JupyterHub. +The [binderhub-service](https://github.com/2i2c-org/binderhub-service/) Helm chart runs BinderHub, the Python software, as a standalone service to build and push images with [repo2docker], next to [JupyterHub]. The `binderhub-service` installation starts a `docker-api` pod on each of the user nodes via the following [DaemonSet definition](https://github.com/2i2c-org/binderhub-service/blob/main/binderhub-service/templates/docker-api/daemonset.yaml). @@ -10,10 +10,11 @@ The `build pods` are created as a result of an image build request, and they mus ## Technical stack -[jupyterhub]: https://jupyterhub.readthedocs.io/en/stable/ +[JupyterHub]: https://jupyterhub.readthedocs.io/en/stable/ [jupyterhub rbac]: https://jupyterhub.readthedocs.io/en/stable/rbac/index.html [readthedocs]: https://readthedocs.org/ [sphinx]: https://www.sphinx-doc.org/en/master/ [sphinx-book-theme]: https://sphinx-book-theme.readthedocs.io/en/stable/ [myst-parser]: https://myst-parser.readthedocs.io/en/stable/ [github actions]: https://github.com/features/actions +[repo2docker]: https://github.com/jupyterhub/repo2docker diff --git a/docs/source/explanation/index.md b/docs/source/explanation/index.md new file mode 100644 index 000000000..c60d2a2a7 --- /dev/null +++ b/docs/source/explanation/index.md @@ -0,0 +1,20 @@ +(explanation)= +# Explanation + +The documentation in this sections aims to _explain_ and _clarify_ a particular +topic within the project in order to broaden the reader's understanding. + +```{note} +[Read more about the Explanation quadrant of the diataxis framework.](https://diataxis.fr/explanation/) +``` + +```{attention} +This documentation is a Work in Progress! +Please see our [contributing guide](contributing) if you'd like to add to it. +``` + +```{toctree} +:maxdepth: 2 +architecture.md +implementation.md +``` diff --git a/docs/source/howto/index.md b/docs/source/howto/index.md index 0f314c739..3cef19a82 100644 --- a/docs/source/howto/index.md +++ b/docs/source/howto/index.md @@ -14,4 +14,5 @@ Please see our [contributing guide](contributing) if you'd like to add to it. ``` ```{toctree} +:maxdepth: 2 ``` diff --git a/docs/source/index.md b/docs/source/index.md index 1b76d1deb..aaac14579 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,48 +1,48 @@ # binderhub-service -Write description here... +`binderhub-service` is a [Helm chart](https://helm.sh/) and guide to run [BinderHub](https://github.com/jupyterhub/binderhub) (the Python software), as a standalone service to build and push images with [repo2docker](https://github.com/jupyterhub/repo2docker). It can be configured for use with a JupyterHub Helm chart installation. -## Tutorials +## How the documentation is organised + +We are currently using the [diátaxis framework](https://diataxis.fr/) to organise +our docs into four main categories: -**TODO** +- [**Tutorials**](tutorials): Step-by-step guides to complete a specific task +- [**How-To guides**](howto): Directions to solve scenarios faced while using the project. Their titles often complete the sentence "How do I...?" +- [**Explanation**](explanation): More in-depth discussion of topics within the project to broaden understanding. +- [**Reference**](ref): Technical descriptions of the components within the project, and how to use them + +## Tutorials ```{toctree} :maxdepth: 2 +tutorials/index.md ``` ## How-to guides -**TODO** - -- making some kind of change of relevance - ```{toctree} :maxdepth: 2 -howto/index +howto/index.md ``` ## Reference -**TODO** - ```{toctree} :maxdepth: 2 -reference/changelog +reference/index.md ``` ## Explanation -**TODO** - ```{toctree} :maxdepth: 2 -explanation/architecture -explanation/implementation +explanation/index.md ``` ## Contributing ```{toctree} :maxdepth: 2 -contributing +contributing.md ``` diff --git a/docs/source/reference/index.md b/docs/source/reference/index.md new file mode 100644 index 000000000..04981043a --- /dev/null +++ b/docs/source/reference/index.md @@ -0,0 +1,19 @@ +(ref)= +# Reference + +The documentation in this section provides technical descriptions of the +components used throughout the project, and how to use them. + +```{note} +[Read more about the Reference quadrant of the diataxis framework.](https://diataxis.fr/reference/) +``` + +```{attention} +This documentation is a Work in Progress! +Please see our [contributing guide](contributing) if you'd like to add to it. +``` + +```{toctree} +:maxdepth: 2 +changelog.md +``` diff --git a/docs/source/tutorials/index.md b/docs/source/tutorials/index.md index a83113022..59e95fa83 100644 --- a/docs/source/tutorials/index.md +++ b/docs/source/tutorials/index.md @@ -15,6 +15,5 @@ Please see our [contributing guide](contributing) if you'd like to add to it. ```{toctree} :maxdepth: 2 - install.md ``` diff --git a/docs/source/tutorials/install.md b/docs/source/tutorials/install.md new file mode 100644 index 000000000..8a33e3bbb --- /dev/null +++ b/docs/source/tutorials/install.md @@ -0,0 +1,317 @@ +# Installation + +1. Add the `binderhub-service` chart repository to helm: + + ```bash + helm repo add binderhub-service https://2i2c.org/binderhub-service + helm repo update + ``` + + Note this URL will change eventually, as binderhub-service is designed + to be a generic service, not something for use only by 2i2c. + +2. Install the latest development version of `binderhub-service` into a + namespace. + + ```bash + helm upgrade \ + --install \ + --create-namespace \ + --devel \ + --wait \ + --namespace \ + \ + binderhub-service/binderhub-service + ``` + + This sets up a binderhub service, but not in a publicly visible way. + +3. Test that it's running by port-forwarding to the correct pod: + + ```bash + kubectl -n port-forward $(kubectl -n get pod -l app.kubernetes.io/component=binderhub -o name) 8585:8585 + ``` + + This should forward requests on port 8585 on your localhost, to the binder service running inside the pod. So if you go + to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success!_. + +4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, + but binderhub supports using other registries. + + Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo). Make sure you're in the correct project (look at the drop + down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. + + In the repository creation page, give it a name (ideally same name you are using for + dedicated to the chart installation), select 'Docker' as the format, 'Standard' as the mode, 'Region' + as the location type and select the same region your kubernetes cluster is in. The + settings about encryption and other options can be left in their default. Hit "Create". + +5. Find the full path of the repository you just created, by opening it in the list + and looking for the small 'copy' icon next to the name of the repository. If you + hit it, it should copy something like `-docker.pkg.dev//`. + Save this. + +6. Create a Google Cloud Service Account that has permissions to push to this + repository ([via this URL] + (https://console.cloud.google.com/iam-admin/serviceaccounts/create) - make + sure you are in the correct project again). You may also need appropriate permissions to set this up. Give it a name (same as the name you used + for the chart installation, but with a '-pusher' suffix) and click 'Create and Continue'. + In the next step, select 'Artifact Registry Writer' as a role. Click "Continue". In the final step, just click "Done". + +7. Now that the service account is created, find it in the list and open it. You will + find a tab named 'Keys' once the informational display opens - select that. Click + 'Add Key' -> 'Create New Key'. In the dialog box that pops up, select 'JSON' as the + key type and click 'Create'. This should download a key file. **Keep this file safe**! + +8. Now that we have the appropriate permissions, let's set up our configuration! Create a + new file named `binderhub-service-config.yaml` with the following contents: + + ```yaml + config: + BinderHub: + use_registry: true + image_prefix: /binder + # Temporarily enable the binderhub UI so we can test image building and pushing + enable_api_only_mode: false + buildPodsRegistryCredentials: + server: "https://-docker.pkg.dev" + username: "_json_key" + password: | + + ``` + + where: + + 1. `` is what you copied from step 5. + + 2. `` is the JSON file you downloaded in step 7. + This is a multi-line file and will need to be indented correctly. The `|` after + `password` allows the value to be multi-line, and each line should be indented at least + 2 spaces from `password`. + + 3. `` is the region your artifact repository was created in. You can see + this in the first part of `` as well. + +9. Run a `helm upgrade` to use the new configuration you just created: + + ```bash + helm upgrade \ + --install \ + --create-namespace \ + --devel \ + --wait \ + --namespace + \ + binderhub-service/binderhub-service \ + --values binderhub-service-config.yaml + ``` + + This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, + you will see that the binderhub pod has restarted - this confirms that the config has been set up! + +10. Let's verify that _image building and pushing_ works. Access the binderhub pod by following the + same instructions as step 3. But this time, you should see a binderhub page very similar to that + on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying + out `binder-examples/requirements`. It might take a while to build, but you should be able to see + logs in the UI. It should succeed at _pushing_ the github image, but will fail to launch. The last + lines in the log in the UI should look like: + + ``` + Successfully pushed europe-west10-docker.pkg.dev/binderhub-service-development/bh-service-test/binderbinder-2dexamples-2drequirements-55ab5c:50533eb470ee6c24e872043d30b2fee463d6943fBuilt image, launching... + Launching server... + Launch attempt 1 failed, retrying... + Launch attempt 2 failed, retrying... + ``` + + You can also go back to the Google Artifact Registry repository you created earlier to verify that the built + image is indeed there. + +11. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. + Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy + using the command from step 9. + +## Connect with a JupyterHub installation + +Next, let's connect this to a JupyterHub set up via [z2jh](https://z2jh.jupyter.org/). While any JupyterHub +that can run containers will work with this, the _most common_ setup is to use this with z2jh. The first +few steps are lifted directly from the [install JupyterHub](https://z2jh.jupyter.org/en/stable/jupyterhub/installation.html) +section of z2jh. + +1. Add the z2jh chart repository to helm: + + ``` + helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ + helm repo update + ``` + +2. We want the binderhub to be available under `http://{{hub url}}/services/binder`, because + that is what `jupyterhub-fancy-profiles` expects. Eventually we would also want authentication + to work correctly. For that, we must set up binderhub as a [JupyterHub Service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html). + This provides two things: + + a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to + expose the service to the external world using JupyterHub's ingress / loadbalancer, without + needing a dedicated ingress / loadbalancer for BinderHub. + + b. (Eventually) Appropriate credentials for authenticated network calls between these two services. + + To make this connection, we need to tell JupyterHub where to find BinderHub. Eventually + this can be automatic (once [this issue](https://github.com/2i2c-org/binderhub-service/issues/57) + gets resolved). In the meantime, you can get the name of the BinderHub service by executing + the following command: + + ```bash + kubectl -n get svc -l app.kubernetes.io/name=binderhub-service + ``` + + Make a note of the name under the `NAME` column, we will use it in the next step. + +3. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. + + ```yaml + hub: + services: + binder: + # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 + # for something more readable and requiring less copy-pasting + url: http://{{ service name from step 2}} + ``` + +4. Find the latest version of the z2jh helm chart. The easiest way is to run the + following command: + + ```bash + helm search repo jupyterhub + ``` + + This should output a few columns. Look for the version under **CHART VERSION** (not _APP VERSION_) + for `jupyterhub/jupyterhub`. That's the latest z2jh chart version, and that is what + we will be using. + +5. Install the JupyterHub helm chart with the following command: + + ```bash + helm upgrade --cleanup-on-fail \ + --install jupyterhub/jupyterhub \ + --namespace \ + --version= \ + --values z2jh-config.yaml \ + --wait + ``` + + where: + + - `` is any name you can use to refer to this imag + (like `jupyterhub`) + + - `` is the _same_ namespace used for the BinderHub install + + - `` is the latest stable version of the JupyterHub + helm chart, determined in the previous step. + +6. Find the external IP on which the JupyterHub is accessible: + + ```bash + kubectl -n get svc proxy-public + ``` + +7. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder/` (the + trailing slash is _important_). You should see an unstyled, somewhat broken + 404 page. This is great and expected. Let's fix that. + +8. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now + be available under `/services/binder`. + + ```yaml + config: + BinderHub: + base_url: /services/binder + ``` + + Deploy this using the `helm upgrade` command from step 9 in the previous section. + +9. Test by going to `http://{{ external ip from step 5}}/services/binder/` (the trailing slash + is _important_!) again, and you should see a _styled_ 404 page! Success - + this means BinderHub is now connected to JupyterHub, even if the end users + can't see it yet. Let's connect them! + +## Connect with `jupyterhub-fancy-profiles` + +The [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) +project provides a user facing frontend for connecting the JupyterHub to BinderHub, +allowing them to build their own images the same way they would on `mybinder.org`! + +1. First, we need to install the `jupyterhub-fancy-profiles` package in the container + that is running the JupyterHub process itself (not the user containers). The + easiest way to do this is to use one of the pre-built images provided by + the `jupyterhub-fancy-profiles` project. In the [list of tags](https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags), + select the latest tag that also includes the version of the z2jh chart you are + using (the `version` specified in step 4 of the previous step). This is _most likely_ + the tag on the top of the page, and looks something like `z2jh-v{{ z2jh version }}-fancy-profiles-sha-{{ some string}}`. + + Once you find the tag, _modify_ the `z2jh-config.yaml` file to enable `jupyterhub-fancy-profiles`. + While it is hidden here for clarity, make sure to preserve the `hub.services` section that + you added in step 3 of the previous section while editing this file. + + ```yaml + hub: + services: ... + image: + # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags + name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles + tag: "" # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" + + extraConfig: + enable-fancy-profiles: | + from jupyterhub_fancy_profiles import setup_ui + setup_ui(c) + ``` + +2. Since `jupyterhub-fancy-profiles` adds on to the [profileList](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) + feature of KubeSpawner, we need to configure a profile list here as well. + Add this to the `z2jh-config.yaml` file: + + ```yaml + singleuser: + profileList: + - display_name: "Only Profile Available, this info is not shown in the UI" + slug: only-choice + profile_options: + image: + display_name: Image + unlisted_choice: + enabled: True + display_name: "Custom image" + validation_regex: "^.+:.+$" + validation_message: "Must be a publicly available docker image, of form :" + display_name_in_choices: "Specify an existing docker image" + description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" + kubespawner_override: + image: "{value}" + choices: + pangeo: + display_name: Pangeo Notebook Image + description: "Python image with scientific, dask and geospatial tools" + kubespawner_override: + image: pangeo/pangeo-notebook:2023.09.11 + scipy: + display_name: Jupyter SciPy Notebook + slug: scipy + kubespawner_override: + image: jupyter/scipy-notebook:2023-06-26 + ``` + +3. Deploy, using the command from step 5 of the section above. + +4. Access the JupyterHub itself, using the external IP you got from step 5 of the section + above (not `{{ hub IP }}/services/binder/`). Once you log in (you can use _any_ username + and password), you should see a UI that allows you to choose two pre-existing + images (pangeo and scipy), specify your own image, or 'build' your own image. + The last option lets you access the binder functionality! Test it out :) + +From now on, you can customize this JupyterHub as you would any other JupyterHub set up +using z2jh. The [customization guide](https://z2jh.jupyter.org/en/stable/jupyterhub/customization.html) +contains many helpful examples of how you can customize your hub. In particular, +you probably want to set up more restrictive +[authentication](https://z2jh.jupyter.org/en/stable/administrator/authentication.html) +so not everyone can log in to your hub! From fe1bb274087cce44933cb74483614bb8e700743c Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Thu, 7 Mar 2024 11:25:35 +0200 Subject: [PATCH 111/200] Split into three tutorials --- .../connect-with-jupyterhub-fancy-profiles.md | 82 ++++++++ .../tutorials/connect-with-jupyterhub.md | 103 ++++++++++ docs/source/tutorials/index.md | 2 + docs/source/tutorials/install.md | 189 +----------------- 4 files changed, 191 insertions(+), 185 deletions(-) create mode 100644 docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md create mode 100644 docs/source/tutorials/connect-with-jupyterhub.md diff --git a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md new file mode 100644 index 000000000..c5621f9c3 --- /dev/null +++ b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md @@ -0,0 +1,82 @@ +# Connect with `jupyterhub-fancy-profiles` + +The [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) +project provides a user facing frontend for connecting the JupyterHub to BinderHub, +allowing them to build their own images the same way they would on `mybinder.org`! + +The following steps describe how to connect your `binderhub-service` [](installation) to `jupyterhub-fancy-profiles` + +1. First, we need to install the `jupyterhub-fancy-profiles` package in the container + that is running the JupyterHub process itself (not the user containers). The + easiest way to do this is to use one of the pre-built images provided by + the `jupyterhub-fancy-profiles` project. In the [list of tags](https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags), + select the latest tag that also includes the version of the z2jh chart you are + using (the `version` specified in step 4 of the previous step). This is _most likely_ + the tag on the top of the page, and looks something like `z2jh-v{{ z2jh version }}-fancy-profiles-sha-{{ some string}}`. + + Once you find the tag, _modify_ the `z2jh-config.yaml` file to enable `jupyterhub-fancy-profiles`. + While it is hidden here for clarity, make sure to preserve the `hub.services` section that + you added in step 3 of the previous section while editing this file. + + ```yaml + hub: + services: ... + image: + # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags + name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles + tag: "" # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" + + extraConfig: + enable-fancy-profiles: | + from jupyterhub_fancy_profiles import setup_ui + setup_ui(c) + ``` + +2. Since `jupyterhub-fancy-profiles` adds on to the [profileList](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) + feature of KubeSpawner, we need to configure a profile list here as well. + Add this to the `z2jh-config.yaml` file: + + ```yaml + singleuser: + profileList: + - display_name: "Only Profile Available, this info is not shown in the UI" + slug: only-choice + profile_options: + image: + display_name: Image + unlisted_choice: + enabled: True + display_name: "Custom image" + validation_regex: "^.+:.+$" + validation_message: "Must be a publicly available docker image, of form :" + display_name_in_choices: "Specify an existing docker image" + description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" + kubespawner_override: + image: "{value}" + choices: + pangeo: + display_name: Pangeo Notebook Image + description: "Python image with scientific, dask and geospatial tools" + kubespawner_override: + image: pangeo/pangeo-notebook:2023.09.11 + scipy: + display_name: Jupyter SciPy Notebook + slug: scipy + kubespawner_override: + image: jupyter/scipy-notebook:2023-06-26 + ``` + +3. Deploy, using the command from step 5 of the section above. + +4. Access the JupyterHub itself, using the external IP you got from step 5 of the section + above (not `{{ hub IP }}/services/binder/`). Once you log in (you can use _any_ username + and password), you should see a UI that allows you to choose two pre-existing + images (pangeo and scipy), specify your own image, or 'build' your own image. + The last option lets you access the binder functionality! Test it out :) + +From now on, you can customize this JupyterHub as you would any other JupyterHub set up +using z2jh. The [customization guide](https://z2jh.jupyter.org/en/stable/jupyterhub/customization.html) +contains many helpful examples of how you can customize your hub. In particular, +you probably want to set up more restrictive +[authentication](https://z2jh.jupyter.org/en/stable/administrator/authentication.html) +so not everyone can log in to your hub! diff --git a/docs/source/tutorials/connect-with-jupyterhub.md b/docs/source/tutorials/connect-with-jupyterhub.md new file mode 100644 index 000000000..79bda5487 --- /dev/null +++ b/docs/source/tutorials/connect-with-jupyterhub.md @@ -0,0 +1,103 @@ +# Connect with a JupyterHub installation + +The next steps describe how to connect the [`binderhub-service` installation](installation) to a JupyterHub set up via [z2jh](https://z2jh.jupyter.org/). While any JupyterHub that can run containers will work with this, the _most common_ setup is to use this with z2jh. + +The first few steps are lifted directly from the [install JupyterHub](https://z2jh.jupyter.org/en/stable/jupyterhub/installation.html) section of z2jh. + +1. Add the z2jh chart repository to helm: + + ``` + helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ + helm repo update + ``` + +2. We want the binderhub to be available under `http://{{hub url}}/services/binder`, because + that is what `jupyterhub-fancy-profiles` expects. Eventually we would also want authentication + to work correctly. For that, we must set up binderhub as a [JupyterHub Service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html). + This provides two things: + + a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to + expose the service to the external world using JupyterHub's ingress / loadbalancer, without + needing a dedicated ingress / loadbalancer for BinderHub. + + b. (Eventually) Appropriate credentials for authenticated network calls between these two services. + + To make this connection, we need to tell JupyterHub where to find BinderHub. Eventually + this can be automatic (once [this issue](https://github.com/2i2c-org/binderhub-service/issues/57) + gets resolved). In the meantime, you can get the name of the BinderHub service by executing + the following command: + + ```bash + kubectl -n get svc -l app.kubernetes.io/name=binderhub-service + ``` + + Make a note of the name under the `NAME` column, we will use it in the next step. + +3. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. + + ```yaml + hub: + services: + binder: + # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 + # for something more readable and requiring less copy-pasting + url: http://{{ service name from step 2}} + ``` + +4. Find the latest version of the z2jh helm chart. The easiest way is to run the + following command: + + ```bash + helm search repo jupyterhub + ``` + + This should output a few columns. Look for the version under **CHART VERSION** (not _APP VERSION_) + for `jupyterhub/jupyterhub`. That's the latest z2jh chart version, and that is what + we will be using. + +5. Install the JupyterHub helm chart with the following command: + + ```bash + helm upgrade --cleanup-on-fail \ + --install jupyterhub/jupyterhub \ + --namespace \ + --version= \ + --values z2jh-config.yaml \ + --wait + ``` + + where: + + - `` is any name you can use to refer to this imag + (like `jupyterhub`) + + - `` is the _same_ namespace used for the BinderHub install + + - `` is the latest stable version of the JupyterHub + helm chart, determined in the previous step. + +6. Find the external IP on which the JupyterHub is accessible: + + ```bash + kubectl -n get svc proxy-public + ``` + +7. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder/` (the + trailing slash is _important_). You should see an unstyled, somewhat broken + 404 page. This is great and expected. Let's fix that. + +8. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now + be available under `/services/binder`. + + ```yaml + config: + BinderHub: + base_url: /services/binder + ``` + + Deploy this using the `helm upgrade` command from step 9 in the previous section. + +9. Test by going to `http://{{ external ip from step 5}}/services/binder/` (the trailing slash + is _important_!) again, and you should see a _styled_ 404 page! Success - + this means BinderHub is now connected to JupyterHub, even if the end users + can't see it yet. Let's connect them! diff --git a/docs/source/tutorials/index.md b/docs/source/tutorials/index.md index 59e95fa83..7337976c2 100644 --- a/docs/source/tutorials/index.md +++ b/docs/source/tutorials/index.md @@ -16,4 +16,6 @@ Please see our [contributing guide](contributing) if you'd like to add to it. ```{toctree} :maxdepth: 2 install.md +connect-with-jupyterhub-fancy-profiles.md +connect-with-jupyterhub.md ``` diff --git a/docs/source/tutorials/install.md b/docs/source/tutorials/install.md index 8a33e3bbb..f9f67fade 100644 --- a/docs/source/tutorials/install.md +++ b/docs/source/tutorials/install.md @@ -1,5 +1,9 @@ +(installation)= + # Installation +The following steps describe how to install the `binderhub-service` helm chart. + 1. Add the `binderhub-service` chart repository to helm: ```bash @@ -130,188 +134,3 @@ Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy using the command from step 9. -## Connect with a JupyterHub installation - -Next, let's connect this to a JupyterHub set up via [z2jh](https://z2jh.jupyter.org/). While any JupyterHub -that can run containers will work with this, the _most common_ setup is to use this with z2jh. The first -few steps are lifted directly from the [install JupyterHub](https://z2jh.jupyter.org/en/stable/jupyterhub/installation.html) -section of z2jh. - -1. Add the z2jh chart repository to helm: - - ``` - helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ - helm repo update - ``` - -2. We want the binderhub to be available under `http://{{hub url}}/services/binder`, because - that is what `jupyterhub-fancy-profiles` expects. Eventually we would also want authentication - to work correctly. For that, we must set up binderhub as a [JupyterHub Service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html). - This provides two things: - - a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to - expose the service to the external world using JupyterHub's ingress / loadbalancer, without - needing a dedicated ingress / loadbalancer for BinderHub. - - b. (Eventually) Appropriate credentials for authenticated network calls between these two services. - - To make this connection, we need to tell JupyterHub where to find BinderHub. Eventually - this can be automatic (once [this issue](https://github.com/2i2c-org/binderhub-service/issues/57) - gets resolved). In the meantime, you can get the name of the BinderHub service by executing - the following command: - - ```bash - kubectl -n get svc -l app.kubernetes.io/name=binderhub-service - ``` - - Make a note of the name under the `NAME` column, we will use it in the next step. - -3. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. - - ```yaml - hub: - services: - binder: - # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 - # for something more readable and requiring less copy-pasting - url: http://{{ service name from step 2}} - ``` - -4. Find the latest version of the z2jh helm chart. The easiest way is to run the - following command: - - ```bash - helm search repo jupyterhub - ``` - - This should output a few columns. Look for the version under **CHART VERSION** (not _APP VERSION_) - for `jupyterhub/jupyterhub`. That's the latest z2jh chart version, and that is what - we will be using. - -5. Install the JupyterHub helm chart with the following command: - - ```bash - helm upgrade --cleanup-on-fail \ - --install jupyterhub/jupyterhub \ - --namespace \ - --version= \ - --values z2jh-config.yaml \ - --wait - ``` - - where: - - - `` is any name you can use to refer to this imag - (like `jupyterhub`) - - - `` is the _same_ namespace used for the BinderHub install - - - `` is the latest stable version of the JupyterHub - helm chart, determined in the previous step. - -6. Find the external IP on which the JupyterHub is accessible: - - ```bash - kubectl -n get svc proxy-public - ``` - -7. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder/` (the - trailing slash is _important_). You should see an unstyled, somewhat broken - 404 page. This is great and expected. Let's fix that. - -8. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now - be available under `/services/binder`. - - ```yaml - config: - BinderHub: - base_url: /services/binder - ``` - - Deploy this using the `helm upgrade` command from step 9 in the previous section. - -9. Test by going to `http://{{ external ip from step 5}}/services/binder/` (the trailing slash - is _important_!) again, and you should see a _styled_ 404 page! Success - - this means BinderHub is now connected to JupyterHub, even if the end users - can't see it yet. Let's connect them! - -## Connect with `jupyterhub-fancy-profiles` - -The [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) -project provides a user facing frontend for connecting the JupyterHub to BinderHub, -allowing them to build their own images the same way they would on `mybinder.org`! - -1. First, we need to install the `jupyterhub-fancy-profiles` package in the container - that is running the JupyterHub process itself (not the user containers). The - easiest way to do this is to use one of the pre-built images provided by - the `jupyterhub-fancy-profiles` project. In the [list of tags](https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags), - select the latest tag that also includes the version of the z2jh chart you are - using (the `version` specified in step 4 of the previous step). This is _most likely_ - the tag on the top of the page, and looks something like `z2jh-v{{ z2jh version }}-fancy-profiles-sha-{{ some string}}`. - - Once you find the tag, _modify_ the `z2jh-config.yaml` file to enable `jupyterhub-fancy-profiles`. - While it is hidden here for clarity, make sure to preserve the `hub.services` section that - you added in step 3 of the previous section while editing this file. - - ```yaml - hub: - services: ... - image: - # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags - name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles - tag: "" # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" - - extraConfig: - enable-fancy-profiles: | - from jupyterhub_fancy_profiles import setup_ui - setup_ui(c) - ``` - -2. Since `jupyterhub-fancy-profiles` adds on to the [profileList](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) - feature of KubeSpawner, we need to configure a profile list here as well. - Add this to the `z2jh-config.yaml` file: - - ```yaml - singleuser: - profileList: - - display_name: "Only Profile Available, this info is not shown in the UI" - slug: only-choice - profile_options: - image: - display_name: Image - unlisted_choice: - enabled: True - display_name: "Custom image" - validation_regex: "^.+:.+$" - validation_message: "Must be a publicly available docker image, of form :" - display_name_in_choices: "Specify an existing docker image" - description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" - kubespawner_override: - image: "{value}" - choices: - pangeo: - display_name: Pangeo Notebook Image - description: "Python image with scientific, dask and geospatial tools" - kubespawner_override: - image: pangeo/pangeo-notebook:2023.09.11 - scipy: - display_name: Jupyter SciPy Notebook - slug: scipy - kubespawner_override: - image: jupyter/scipy-notebook:2023-06-26 - ``` - -3. Deploy, using the command from step 5 of the section above. - -4. Access the JupyterHub itself, using the external IP you got from step 5 of the section - above (not `{{ hub IP }}/services/binder/`). Once you log in (you can use _any_ username - and password), you should see a UI that allows you to choose two pre-existing - images (pangeo and scipy), specify your own image, or 'build' your own image. - The last option lets you access the binder functionality! Test it out :) - -From now on, you can customize this JupyterHub as you would any other JupyterHub set up -using z2jh. The [customization guide](https://z2jh.jupyter.org/en/stable/jupyterhub/customization.html) -contains many helpful examples of how you can customize your hub. In particular, -you probably want to set up more restrictive -[authentication](https://z2jh.jupyter.org/en/stable/administrator/authentication.html) -so not everyone can log in to your hub! From c453ef2b8529b103e9424d1a916c20dc31a65c7a Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Thu, 7 Mar 2024 11:34:10 +0200 Subject: [PATCH 112/200] Add a link to binderhub docs --- docs/source/tutorials/install.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/tutorials/install.md b/docs/source/tutorials/install.md index f9f67fade..8bbcc9b15 100644 --- a/docs/source/tutorials/install.md +++ b/docs/source/tutorials/install.md @@ -39,8 +39,7 @@ The following steps describe how to install the `binderhub-service` helm chart. This should forward requests on port 8585 on your localhost, to the binder service running inside the pod. So if you go to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success!_. -4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, - but binderhub supports using other registries. +4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, but [binderhub supports using other registries](https://binderhub.readthedocs.io/en/latest/zero-to-binderhub/setup-registry.html#set-up-the-container-registry). Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo). Make sure you're in the correct project (look at the drop down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. From cb66ac966bf2a9aae3fa1b0c78f0d39b388c1bfa Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Thu, 7 Mar 2024 12:12:19 +0200 Subject: [PATCH 113/200] Add more details about the implementation --- docs/source/explanation/implementation.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/source/explanation/implementation.md b/docs/source/explanation/implementation.md index 27a80a924..628a75bf5 100644 --- a/docs/source/explanation/implementation.md +++ b/docs/source/explanation/implementation.md @@ -4,9 +4,15 @@ The [binderhub-service](https://github.com/2i2c-org/binderhub-service/) Helm cha The `binderhub-service` installation starts a `docker-api` pod on each of the user nodes via the following [DaemonSet definition](https://github.com/2i2c-org/binderhub-service/blob/main/binderhub-service/templates/docker-api/daemonset.yaml). -The `docker-api` pod setups and starts the [dockerd](https://docs.docker.com/engine/reference/commandline/dockerd/) daemon, that will then be accessible via a mounted unix socket on the node, by the `build pods`. +The daemonset also setups a [hostPath](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath) volume that mounts a [unix socket](https://man7.org/linux/man-pages/man7/unix.7.html) from the user node into the `docker-api` pods. -The `build pods` are created as a result of an image build request, and they must run on the same node as the builder pods to make use of the docker daemon. These pods mount a k8s Secret with the docker config file holding the necessary registry credentials so they can push to the container registry. +The `docker-api` pod setups and starts the [dockerd](https://docs.docker.com/engine/reference/commandline/dockerd/) daemon, that will then be accessible via this unix socket by the `build pods`. + +```{warning} +The `binderhub-service` chart currently support only Docker and not yet Podman. Checkout https://github.com/2i2c-org/binderhub-service/issues/31 for updates on Podmand support. +``` + +The `build pods` are managed by BinderHub through [`KubernetesBuildExecutor`](https://github.com/jupyterhub/binderhub/blob/7f8b6c3137a6f8e66e6c193ee81d32bcf0826a6e/binderhub/build.py#L222-L242) and are created as a result of an image build request. They must run on the same node as the builder pods to make use of the docker daemon. These pods mount a k8s Secret with the docker config file holding the necessary registry credentials so they can push to the container registry. ## Technical stack From deffab7fc024a3e5c1597086aeceabe0a6166d91 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:13:06 +0000 Subject: [PATCH 114/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/contributing.md | 2 +- docs/source/explanation/index.md | 1 + docs/source/howto/index.md | 1 + docs/source/reference/index.md | 1 + docs/source/tutorials/index.md | 1 + docs/source/tutorials/install.md | 1 - 6 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/source/contributing.md b/docs/source/contributing.md index 4a8bb0f34..8bcb92363 100644 --- a/docs/source/contributing.md +++ b/docs/source/contributing.md @@ -5,4 +5,4 @@ Hello and thank you for contributing to binderhub-service! We are really excited to have you here! Below you'll find some useful tasks, guidelines, and instructions for working in this repository. -Notice something that's missing? Please open an issue or file a pull request! \ No newline at end of file +Notice something that's missing? Please open an issue or file a pull request! diff --git a/docs/source/explanation/index.md b/docs/source/explanation/index.md index c60d2a2a7..48ddaa3f4 100644 --- a/docs/source/explanation/index.md +++ b/docs/source/explanation/index.md @@ -1,4 +1,5 @@ (explanation)= + # Explanation The documentation in this sections aims to _explain_ and _clarify_ a particular diff --git a/docs/source/howto/index.md b/docs/source/howto/index.md index 3cef19a82..715e8961a 100644 --- a/docs/source/howto/index.md +++ b/docs/source/howto/index.md @@ -1,4 +1,5 @@ (howto)= + # How-To Guides The documentation in this section is _goal-oriented_ and designed to guide the diff --git a/docs/source/reference/index.md b/docs/source/reference/index.md index 04981043a..a224472d9 100644 --- a/docs/source/reference/index.md +++ b/docs/source/reference/index.md @@ -1,4 +1,5 @@ (ref)= + # Reference The documentation in this section provides technical descriptions of the diff --git a/docs/source/tutorials/index.md b/docs/source/tutorials/index.md index 7337976c2..ce5654294 100644 --- a/docs/source/tutorials/index.md +++ b/docs/source/tutorials/index.md @@ -1,4 +1,5 @@ (tutorials)= + # Tutorials The documentation in this section are step-by-step guides that lead the reader diff --git a/docs/source/tutorials/install.md b/docs/source/tutorials/install.md index 8bbcc9b15..aa1d28a7c 100644 --- a/docs/source/tutorials/install.md +++ b/docs/source/tutorials/install.md @@ -132,4 +132,3 @@ The following steps describe how to install the `binderhub-service` helm chart. 11. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy using the command from step 9. - From b841c0bdf29cd34f6a3f690c8e9c4b80b2305794 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Thu, 7 Mar 2024 13:02:28 +0200 Subject: [PATCH 115/200] Separate for more clarity --- docs/source/explanation/implementation.md | 13 ++++++++++--- .../connect-with-jupyterhub-fancy-profiles.md | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/source/explanation/implementation.md b/docs/source/explanation/implementation.md index 628a75bf5..abbed7228 100644 --- a/docs/source/explanation/implementation.md +++ b/docs/source/explanation/implementation.md @@ -2,9 +2,14 @@ The [binderhub-service](https://github.com/2i2c-org/binderhub-service/) Helm chart runs BinderHub, the Python software, as a standalone service to build and push images with [repo2docker], next to [JupyterHub]. -The `binderhub-service` installation starts a `docker-api` pod on each of the user nodes via the following [DaemonSet definition](https://github.com/2i2c-org/binderhub-service/blob/main/binderhub-service/templates/docker-api/daemonset.yaml). +The following main resource components are most relevant for how the machinery works: -The daemonset also setups a [hostPath](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath) volume that mounts a [unix socket](https://man7.org/linux/man-pages/man7/unix.7.html) from the user node into the `docker-api` pods. +## The binderhub service + + +## The DaemonSet resource - DockerApi + +The `binderhub-service` installation starts a `docker-api` pod on each of the user nodes via the following [DaemonSet definition](https://github.com/2i2c-org/binderhub-service/blob/main/binderhub-service/templates/docker-api/daemonset.yaml). The daemonset also setups a [hostPath](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath) volume that mounts a [unix socket](https://man7.org/linux/man-pages/man7/unix.7.html) from the user node into the `docker-api` pods. The `docker-api` pod setups and starts the [dockerd](https://docs.docker.com/engine/reference/commandline/dockerd/) daemon, that will then be accessible via this unix socket by the `build pods`. @@ -12,7 +17,9 @@ The `docker-api` pod setups and starts the [dockerd](https://docs.docker.com/eng The `binderhub-service` chart currently support only Docker and not yet Podman. Checkout https://github.com/2i2c-org/binderhub-service/issues/31 for updates on Podmand support. ``` -The `build pods` are managed by BinderHub through [`KubernetesBuildExecutor`](https://github.com/jupyterhub/binderhub/blob/7f8b6c3137a6f8e66e6c193ee81d32bcf0826a6e/binderhub/build.py#L222-L242) and are created as a result of an image build request. They must run on the same node as the builder pods to make use of the docker daemon. These pods mount a k8s Secret with the docker config file holding the necessary registry credentials so they can push to the container registry. +### The build pods + +The `build pods` are managed by BinderHub through [`KubernetesBuildExecutor`](https://github.com/jupyterhub/binderhub/blob/7f8b6c3137a6f8e66e6c193ee81d32bcf0826a6e/binderhub/build.py#L222-L242) and are created as a result of an image build request. They must run on the same node as the builder pods to make use of the docker daemon. These pods mount **a k8s Secret** with the docker config file holding the necessary registry credentials so they can push to the container registry. ## Technical stack diff --git a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md index c5621f9c3..ca6b8f1f2 100644 --- a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md +++ b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md @@ -1,4 +1,4 @@ -# Connect with `jupyterhub-fancy-profiles` +# Connect with jupyterhub-fancy-profiles The [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) project provides a user facing frontend for connecting the JupyterHub to BinderHub, From 8cc776e011b11bb4cd10b695a2082ea5446b2f3b Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Tue, 12 Mar 2024 11:58:51 +0200 Subject: [PATCH 116/200] Add architecture details and have one file --- ....md => architecture-and-implementation.md} | 31 ++++++++++++++++--- docs/source/explanation/architecture.md | 3 -- docs/source/explanation/index.md | 3 +- 3 files changed, 27 insertions(+), 10 deletions(-) rename docs/source/explanation/{implementation.md => architecture-and-implementation.md} (55%) delete mode 100644 docs/source/explanation/architecture.md diff --git a/docs/source/explanation/implementation.md b/docs/source/explanation/architecture-and-implementation.md similarity index 55% rename from docs/source/explanation/implementation.md rename to docs/source/explanation/architecture-and-implementation.md index abbed7228..2827b2e32 100644 --- a/docs/source/explanation/implementation.md +++ b/docs/source/explanation/architecture-and-implementation.md @@ -1,13 +1,32 @@ -# Implementation +(architecture-and-implementation)= +# Architecture and Implementation -The [binderhub-service](https://github.com/2i2c-org/binderhub-service/) Helm chart runs BinderHub, the Python software, as a standalone service to build and push images with [repo2docker], next to [JupyterHub]. +## The architecture + +The `binderhub-service` chart runs the [BinderHub] Python software, in [api-only mode](https://binderhub.readthedocs.io/en/latest/reference/app.html#binderhub.app.BinderHub.enable_api_only_mode) (the default), as a standalone service to build, and push [Docker] images from source code repositories, on demand, using [repo2docker]. This service can then be paired with [JupyterHub] to allow users to initiate build requests from their hubs. + +Thus, the architecture of this system must: +- facilitate the building and pushing of Docker images with repo2docker +- easily integrate with a JupyterHub deployment +- but also run as a standalone service +- operate within a Kubernetes environment + +### What happens when a build & push request is fired + +1. BinderHub creates and starts a `build pod` +2. this build pod needs to be able to run `repo2docker` to build and push the image to the registry that was setup via the config +3. for this, repo2docker needs Docker to build and push the images +4. a running daemon will intercept the docker commands initiated by the the docker client processes running on these build pods +5. these build pods will then use the configured credentials to push the image to the repository. + +## Implementation details The following main resource components are most relevant for how the machinery works: -## The binderhub service +### The binderhub service -## The DaemonSet resource - DockerApi +### The DaemonSet resource - DockerApi The `binderhub-service` installation starts a `docker-api` pod on each of the user nodes via the following [DaemonSet definition](https://github.com/2i2c-org/binderhub-service/blob/main/binderhub-service/templates/docker-api/daemonset.yaml). The daemonset also setups a [hostPath](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath) volume that mounts a [unix socket](https://man7.org/linux/man-pages/man7/unix.7.html) from the user node into the `docker-api` pods. @@ -17,12 +36,13 @@ The `docker-api` pod setups and starts the [dockerd](https://docs.docker.com/eng The `binderhub-service` chart currently support only Docker and not yet Podman. Checkout https://github.com/2i2c-org/binderhub-service/issues/31 for updates on Podmand support. ``` -### The build pods +#### The build pods The `build pods` are managed by BinderHub through [`KubernetesBuildExecutor`](https://github.com/jupyterhub/binderhub/blob/7f8b6c3137a6f8e66e6c193ee81d32bcf0826a6e/binderhub/build.py#L222-L242) and are created as a result of an image build request. They must run on the same node as the builder pods to make use of the docker daemon. These pods mount **a k8s Secret** with the docker config file holding the necessary registry credentials so they can push to the container registry. ## Technical stack +[BinderHub]: https://binderhub.readthedocs.io/en/latest/index.html [JupyterHub]: https://jupyterhub.readthedocs.io/en/stable/ [jupyterhub rbac]: https://jupyterhub.readthedocs.io/en/stable/rbac/index.html [readthedocs]: https://readthedocs.org/ @@ -31,3 +51,4 @@ The `build pods` are managed by BinderHub through [`KubernetesBuildExecutor`](ht [myst-parser]: https://myst-parser.readthedocs.io/en/stable/ [github actions]: https://github.com/features/actions [repo2docker]: https://github.com/jupyterhub/repo2docker +[Docker]: https://binderhub.readthedocs.io/en/latest/index.html diff --git a/docs/source/explanation/architecture.md b/docs/source/explanation/architecture.md deleted file mode 100644 index 54ebbdd22..000000000 --- a/docs/source/explanation/architecture.md +++ /dev/null @@ -1,3 +0,0 @@ -# Architecture - -## Goals diff --git a/docs/source/explanation/index.md b/docs/source/explanation/index.md index 48ddaa3f4..9736c814a 100644 --- a/docs/source/explanation/index.md +++ b/docs/source/explanation/index.md @@ -16,6 +16,5 @@ Please see our [contributing guide](contributing) if you'd like to add to it. ```{toctree} :maxdepth: 2 -architecture.md -implementation.md +architecture-and-implementation.md ``` From 18deeae78424c642f048484c1abbd57744b5dfb7 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Tue, 12 Mar 2024 14:24:25 +0200 Subject: [PATCH 117/200] Combine steps with implementation details --- .../architecture-and-implementation.md | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/docs/source/explanation/architecture-and-implementation.md b/docs/source/explanation/architecture-and-implementation.md index 2827b2e32..6de7bca0c 100644 --- a/docs/source/explanation/architecture-and-implementation.md +++ b/docs/source/explanation/architecture-and-implementation.md @@ -1,7 +1,7 @@ (architecture-and-implementation)= # Architecture and Implementation -## The architecture +## Architecture The `binderhub-service` chart runs the [BinderHub] Python software, in [api-only mode](https://binderhub.readthedocs.io/en/latest/reference/app.html#binderhub.app.BinderHub.enable_api_only_mode) (the default), as a standalone service to build, and push [Docker] images from source code repositories, on demand, using [repo2docker]. This service can then be paired with [JupyterHub] to allow users to initiate build requests from their hubs. @@ -11,34 +11,35 @@ Thus, the architecture of this system must: - but also run as a standalone service - operate within a Kubernetes environment -### What happens when a build & push request is fired +## Implementation details -1. BinderHub creates and starts a `build pod` -2. this build pod needs to be able to run `repo2docker` to build and push the image to the registry that was setup via the config -3. for this, repo2docker needs Docker to build and push the images -4. a running daemon will intercept the docker commands initiated by the the docker client processes running on these build pods -5. these build pods will then use the configured credentials to push the image to the repository. +When a build & push request is fired, the following events happen: -## Implementation details +1. **BinderHub creates and starts a `build pod` that runs `repo2docker`** -The following main resource components are most relevant for how the machinery works: + The `build pods` are managed by BinderHub through [`KubernetesBuildExecutor`](https://github.com/jupyterhub/binderhub/blob/7f8b6c3137a6f8e66e6c193ee81d32bcf0826a6e/binderhub/build.py#L222-L242) and are created as a result of an image build request. -### The binderhub service + For the image build to work, the docker client processes running on these nodes need to be able to communicate with the dockerd daemon. This communication is done via unix socket mounted on the node. +2. **repo2docker uses Docker to build and push the images** -### The DaemonSet resource - DockerApi + A running [dockerd](https://docs.docker.com/engine/reference/commandline/dockerd/) daemon will intercept the docker commands initiated by the the docker client processes running on these build pods. This dockerd daemon is setup by the `docker-api` pods. -The `binderhub-service` installation starts a `docker-api` pod on each of the user nodes via the following [DaemonSet definition](https://github.com/2i2c-org/binderhub-service/blob/main/binderhub-service/templates/docker-api/daemonset.yaml). The daemonset also setups a [hostPath](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath) volume that mounts a [unix socket](https://man7.org/linux/man-pages/man7/unix.7.html) from the user node into the `docker-api` pods. + The `docker-api` pods are setup to start on each node matching the [`dockerApi.nodeSelector`](https://github.com/2i2c-org/binderhub-service/blob/308965029a901993293539f159c66d15b767e8c8/binderhub-service/values.yaml#L131) by the following [DaemonSet definition](https://github.com/2i2c-org/binderhub-service/blob/main/binderhub-service/templates/docker-api/daemonset.yaml). -The `docker-api` pod setups and starts the [dockerd](https://docs.docker.com/engine/reference/commandline/dockerd/) daemon, that will then be accessible via this unix socket by the `build pods`. + The daemonset also setups a [hostPath](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath) volume that mounts a [unix socket](https://man7.org/linux/man-pages/man7/unix.7.html) from this node into the `docker-api` pods. -```{warning} -The `binderhub-service` chart currently support only Docker and not yet Podman. Checkout https://github.com/2i2c-org/binderhub-service/issues/31 for updates on Podmand support. -``` + ```{important} + The docker-api pods and the build pods must run on the same node so they can use the unix socket on it to interact with the docker daemon listening on this socket. + ``` + +3. **the build pods will then use the configured credentials to push the image to the repository** -#### The build pods + The build pods mount [**a k8s Secret** with the docker config file](https://github.com/2i2c-org/binderhub-service/blob/308965029a901993293539f159c66d15b767e8c8/binderhub-service/templates/secret.yaml#L5) holding the necessary registry credentials so they can push to the container registry. -The `build pods` are managed by BinderHub through [`KubernetesBuildExecutor`](https://github.com/jupyterhub/binderhub/blob/7f8b6c3137a6f8e66e6c193ee81d32bcf0826a6e/binderhub/build.py#L222-L242) and are created as a result of an image build request. They must run on the same node as the builder pods to make use of the docker daemon. These pods mount **a k8s Secret** with the docker config file holding the necessary registry credentials so they can push to the container registry. +```{warning} +The `binderhub-service` chart currently only supports Docker and Podman is not yet available. Checkout https://github.com/2i2c-org/binderhub-service/issues/31 for updates on Podmand support. +``` ## Technical stack From 9627415822b8a930a8f5c6f8ff908406277c3d84 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Tue, 12 Mar 2024 14:31:29 +0200 Subject: [PATCH 118/200] Add a diagram --- .../images/binderhub-service-diagram.png | Bin 0 -> 80751 bytes .../architecture-and-implementation.md | 8 +++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 docs/source/_static/images/binderhub-service-diagram.png diff --git a/docs/source/_static/images/binderhub-service-diagram.png b/docs/source/_static/images/binderhub-service-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..5aa0830107c3fb0de4760ad923cc6e87324d3c77 GIT binary patch literal 80751 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 SZ-rCvckhmdVzIpW^Zx Date: Tue, 12 Mar 2024 14:32:42 +0200 Subject: [PATCH 119/200] Add note about the binderhub's diagram also --- docs/source/explanation/architecture-and-implementation.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/explanation/architecture-and-implementation.md b/docs/source/explanation/architecture-and-implementation.md index d88b2d7eb..2ce4ef1d4 100644 --- a/docs/source/explanation/architecture-and-implementation.md +++ b/docs/source/explanation/architecture-and-implementation.md @@ -19,6 +19,9 @@ Thus, the architecture of this system must: :alt: Here is a high-level overview of the components that make up binderhub-service. ``` +```{tip} +Checking out the BinderHub's architecture diagram might also be helpful. +``` When a build & push request is fired, the following events happen: 1. **BinderHub creates and starts a `build pod` that runs `repo2docker`** From 5089434927467d7ca1fd951633f7f7b327117cf7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 12:34:07 +0000 Subject: [PATCH 120/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../explanation/architecture-and-implementation.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/source/explanation/architecture-and-implementation.md b/docs/source/explanation/architecture-and-implementation.md index 2ce4ef1d4..1e2996bee 100644 --- a/docs/source/explanation/architecture-and-implementation.md +++ b/docs/source/explanation/architecture-and-implementation.md @@ -1,11 +1,13 @@ (architecture-and-implementation)= + # Architecture and Implementation ## Architecture -The `binderhub-service` chart runs the [BinderHub] Python software, in [api-only mode](https://binderhub.readthedocs.io/en/latest/reference/app.html#binderhub.app.BinderHub.enable_api_only_mode) (the default), as a standalone service to build, and push [Docker] images from source code repositories, on demand, using [repo2docker]. This service can then be paired with [JupyterHub] to allow users to initiate build requests from their hubs. +The `binderhub-service` chart runs the [BinderHub] Python software, in [api-only mode](https://binderhub.readthedocs.io/en/latest/reference/app.html#binderhub.app.BinderHub.enable_api_only_mode) (the default), as a standalone service to build, and push [Docker] images from source code repositories, on demand, using [repo2docker]. This service can then be paired with [JupyterHub] to allow users to initiate build requests from their hubs. Thus, the architecture of this system must: + - facilitate the building and pushing of Docker images with repo2docker - easily integrate with a JupyterHub deployment - but also run as a standalone service @@ -15,13 +17,14 @@ Thus, the architecture of this system must: % (This image was generated at the following URL: https://docs.google.com/presentation/d/1KC9cyXGPGBQoeZ0sLxHORyhjXDklIfn-rZ5SAdRB08Q/edit?usp=sharing) following the BinderHub architecture chart at https://docs.google.com/presentation/d/1t5W4Rnez6xBRz4YxCxWYAx8t4KRfUosbCjS4Z1or7rM/edit#slide=id.g25dbc82125_0_53 -``` {figure} ../_static/images/binderhub-service-diagram.png +```{figure} ../_static/images/binderhub-service-diagram.png :alt: Here is a high-level overview of the components that make up binderhub-service. ``` ```{tip} Checking out the BinderHub's architecture diagram might also be helpful. ``` + When a build & push request is fired, the following events happen: 1. **BinderHub creates and starts a `build pod` that runs `repo2docker`** @@ -44,7 +47,7 @@ When a build & push request is fired, the following events happen: 3. **the build pods will then use the configured credentials to push the image to the repository** - The build pods mount [**a k8s Secret** with the docker config file](https://github.com/2i2c-org/binderhub-service/blob/308965029a901993293539f159c66d15b767e8c8/binderhub-service/templates/secret.yaml#L5) holding the necessary registry credentials so they can push to the container registry. + The build pods mount [**a k8s Secret** with the docker config file](https://github.com/2i2c-org/binderhub-service/blob/308965029a901993293539f159c66d15b767e8c8/binderhub-service/templates/secret.yaml#L5) holding the necessary registry credentials so they can push to the container registry. ```{warning} The `binderhub-service` chart currently only supports Docker and Podman is not yet available. Checkout https://github.com/2i2c-org/binderhub-service/issues/31 for updates on Podmand support. From 2b9f9f3325df282bb34a870275e341fe1381a75c Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Tue, 12 Mar 2024 14:34:06 +0200 Subject: [PATCH 121/200] Add link to diagram --- docs/source/explanation/architecture-and-implementation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/explanation/architecture-and-implementation.md b/docs/source/explanation/architecture-and-implementation.md index 1e2996bee..6705c6805 100644 --- a/docs/source/explanation/architecture-and-implementation.md +++ b/docs/source/explanation/architecture-and-implementation.md @@ -22,7 +22,7 @@ Thus, the architecture of this system must: ``` ```{tip} -Checking out the BinderHub's architecture diagram might also be helpful. +Checking out the [BinderHub's architecture diagram](https://binderhub.readthedocs.io/en/latest/overview.html) might also be helpful. ``` When a build & push request is fired, the following events happen: From 824ae6007ec9696846cddcb3871fac7ce131f5ec Mon Sep 17 00:00:00 2001 From: Georgiana Date: Wed, 13 Mar 2024 08:59:51 +0200 Subject: [PATCH 122/200] Reword for clarity Co-authored-by: Sarah Gibson <44771837+sgibson91@users.noreply.github.com> --- docs/source/explanation/architecture-and-implementation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/explanation/architecture-and-implementation.md b/docs/source/explanation/architecture-and-implementation.md index 6705c6805..f1c57434e 100644 --- a/docs/source/explanation/architecture-and-implementation.md +++ b/docs/source/explanation/architecture-and-implementation.md @@ -50,7 +50,7 @@ When a build & push request is fired, the following events happen: The build pods mount [**a k8s Secret** with the docker config file](https://github.com/2i2c-org/binderhub-service/blob/308965029a901993293539f159c66d15b767e8c8/binderhub-service/templates/secret.yaml#L5) holding the necessary registry credentials so they can push to the container registry. ```{warning} -The `binderhub-service` chart currently only supports Docker and Podman is not yet available. Checkout https://github.com/2i2c-org/binderhub-service/issues/31 for updates on Podmand support. +The `binderhub-service` chart currently only supports Docker. Checkout https://github.com/2i2c-org/binderhub-service/issues/31 for updates on Podman support. ``` ## Technical stack From 60ef63b44ffaf81c08317d93a23a9395e304bd56 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Wed, 13 Mar 2024 09:00:08 +0200 Subject: [PATCH 123/200] Be more explicit Co-authored-by: Sarah Gibson <44771837+sgibson91@users.noreply.github.com> --- docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md index ca6b8f1f2..b231f0f2c 100644 --- a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md +++ b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md @@ -2,7 +2,7 @@ The [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) project provides a user facing frontend for connecting the JupyterHub to BinderHub, -allowing them to build their own images the same way they would on `mybinder.org`! +allowing users to build their own images the same way they would on `mybinder.org`! The following steps describe how to connect your `binderhub-service` [](installation) to `jupyterhub-fancy-profiles` From fc3ccc2849465c04c00b5a67636ed7371c055148 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Wed, 13 Mar 2024 09:00:29 +0200 Subject: [PATCH 124/200] Typo Co-authored-by: Sarah Gibson <44771837+sgibson91@users.noreply.github.com> --- docs/source/tutorials/connect-with-jupyterhub.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/tutorials/connect-with-jupyterhub.md b/docs/source/tutorials/connect-with-jupyterhub.md index 79bda5487..b1549d56d 100644 --- a/docs/source/tutorials/connect-with-jupyterhub.md +++ b/docs/source/tutorials/connect-with-jupyterhub.md @@ -68,7 +68,7 @@ The first few steps are lifted directly from the [install JupyterHub](https://z2 where: - - `` is any name you can use to refer to this imag + - `` is any name you can use to refer to this image (like `jupyterhub`) - `` is the _same_ namespace used for the BinderHub install From 4b363b0b2fe0dedc5733c66d36c68ff0c17df4e9 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Wed, 13 Mar 2024 09:14:11 +0200 Subject: [PATCH 125/200] Click it Co-authored-by: Sarah Gibson <44771837+sgibson91@users.noreply.github.com> --- docs/source/tutorials/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/tutorials/install.md b/docs/source/tutorials/install.md index aa1d28a7c..317774f09 100644 --- a/docs/source/tutorials/install.md +++ b/docs/source/tutorials/install.md @@ -47,11 +47,11 @@ The following steps describe how to install the `binderhub-service` helm chart. In the repository creation page, give it a name (ideally same name you are using for dedicated to the chart installation), select 'Docker' as the format, 'Standard' as the mode, 'Region' as the location type and select the same region your kubernetes cluster is in. The - settings about encryption and other options can be left in their default. Hit "Create". + settings about encryption and other options can be left in their default. Click "Create". 5. Find the full path of the repository you just created, by opening it in the list and looking for the small 'copy' icon next to the name of the repository. If you - hit it, it should copy something like `-docker.pkg.dev//`. + click it, it should copy something like `-docker.pkg.dev//`. Save this. 6. Create a Google Cloud Service Account that has permissions to push to this From c8016eebed8e47a541bdf768881e6628ffec7d7f Mon Sep 17 00:00:00 2001 From: Georgiana Date: Wed, 13 Mar 2024 09:19:36 +0200 Subject: [PATCH 126/200] Fix render Co-authored-by: Sarah Gibson <44771837+sgibson91@users.noreply.github.com> --- docs/source/tutorials/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/tutorials/install.md b/docs/source/tutorials/install.md index 317774f09..b7d3e45bd 100644 --- a/docs/source/tutorials/install.md +++ b/docs/source/tutorials/install.md @@ -37,7 +37,7 @@ The following steps describe how to install the `binderhub-service` helm chart. ``` This should forward requests on port 8585 on your localhost, to the binder service running inside the pod. So if you go - to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success!_. + to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success_! 4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, but [binderhub supports using other registries](https://binderhub.readthedocs.io/en/latest/zero-to-binderhub/setup-registry.html#set-up-the-container-registry). From c5ce6c36fe89f1d48e261673043839b491f6c271 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Wed, 13 Mar 2024 09:44:43 +0200 Subject: [PATCH 127/200] Add links and badges to docs in the readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 2ece99539..329a884b9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # binderhub-service +[![Documentation Status](https://img.shields.io/readthedocs/binderhub-service?logo=read-the-docs)](https://binderhub-service.readthedocs.io/en/latest/) +[![Latest chart development release](https://img.shields.io/badge/Helm_releases-https://2i2c.org/binderhub-service/blue?link=https://2i2c.org/binderhub-service/)](https://2i2c.org/binderhub-service/) + + The binderhub-service is a Helm chart and guide to run BinderHub (the Python software), as a standalone service to build and push images with repo2docker, possibly configured for use with a JupyterHub chart installation. @@ -43,6 +47,10 @@ The documentation should help configure the BinderHub service to: [persistent binderhub chart]: https://github.com/gesiscss/persistent_binderhub [was added]: https://github.com/jupyterhub/binderhub/pull/666 +## Installation + +Checkout this project's documentation for installation guide https://binderhub-service.readthedocs.io/en/latest. + ## Funding Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with From fcc19860844387c665d86d603392de048f5e85e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 07:45:08 +0000 Subject: [PATCH 128/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 329a884b9..272b55728 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Documentation Status](https://img.shields.io/readthedocs/binderhub-service?logo=read-the-docs)](https://binderhub-service.readthedocs.io/en/latest/) [![Latest chart development release](https://img.shields.io/badge/Helm_releases-https://2i2c.org/binderhub-service/blue?link=https://2i2c.org/binderhub-service/)](https://2i2c.org/binderhub-service/) - The binderhub-service is a Helm chart and guide to run BinderHub (the Python software), as a standalone service to build and push images with repo2docker, possibly configured for use with a JupyterHub chart installation. From 3b3fda65482e1535c5c603687656fdb0afd17f9e Mon Sep 17 00:00:00 2001 From: Georgiana Date: Wed, 13 Mar 2024 09:47:58 +0200 Subject: [PATCH 129/200] Be more specific about docker client Co-authored-by: Erik Sundell --- docs/source/explanation/architecture-and-implementation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/explanation/architecture-and-implementation.md b/docs/source/explanation/architecture-and-implementation.md index f1c57434e..5900eaabf 100644 --- a/docs/source/explanation/architecture-and-implementation.md +++ b/docs/source/explanation/architecture-and-implementation.md @@ -33,7 +33,7 @@ When a build & push request is fired, the following events happen: For the image build to work, the docker client processes running on these nodes need to be able to communicate with the dockerd daemon. This communication is done via unix socket mounted on the node. -2. **repo2docker uses Docker to build and push the images** +2. **repo2docker use a docker client to build and push images** A running [dockerd](https://docs.docker.com/engine/reference/commandline/dockerd/) daemon will intercept the docker commands initiated by the the docker client processes running on these build pods. This dockerd daemon is setup by the `docker-api` pods. From 027b14886fa18983a7b5e029819ee0ac47be6f58 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Wed, 13 Mar 2024 09:48:37 +0200 Subject: [PATCH 130/200] Correct the link to the secret.yaml file Co-authored-by: Erik Sundell --- docs/source/explanation/architecture-and-implementation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/explanation/architecture-and-implementation.md b/docs/source/explanation/architecture-and-implementation.md index 5900eaabf..6fab5ba55 100644 --- a/docs/source/explanation/architecture-and-implementation.md +++ b/docs/source/explanation/architecture-and-implementation.md @@ -47,7 +47,7 @@ When a build & push request is fired, the following events happen: 3. **the build pods will then use the configured credentials to push the image to the repository** - The build pods mount [**a k8s Secret** with the docker config file](https://github.com/2i2c-org/binderhub-service/blob/308965029a901993293539f159c66d15b767e8c8/binderhub-service/templates/secret.yaml#L5) holding the necessary registry credentials so they can push to the container registry. + The build pods mount [**a k8s Secret** with the docker config file](https://github.com/2i2c-org/binderhub-service/blob/308965029a901993293539f159c66d15b767e8c8/binderhub-service/templates/build-pods-docker-config/secret.yaml) holding the necessary registry credentials so they can push to the container registry. ```{warning} The `binderhub-service` chart currently only supports Docker. Checkout https://github.com/2i2c-org/binderhub-service/issues/31 for updates on Podman support. From 92b69826f91fc4e4d46f595c39fc03b5e76a9ce4 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Wed, 13 Mar 2024 09:47:15 +0200 Subject: [PATCH 131/200] Make it blue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 272b55728..dab731845 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # binderhub-service [![Documentation Status](https://img.shields.io/readthedocs/binderhub-service?logo=read-the-docs)](https://binderhub-service.readthedocs.io/en/latest/) -[![Latest chart development release](https://img.shields.io/badge/Helm_releases-https://2i2c.org/binderhub-service/blue?link=https://2i2c.org/binderhub-service/)](https://2i2c.org/binderhub-service/) +[![Latest chart development release](https://img.shields.io/badge/Helm_releases-https://2i2c.org/binderhub-service/blue?link=https://2i2c.org/binderhub-service&color=blue)](https://2i2c.org/binderhub-service/) The binderhub-service is a Helm chart and guide to run BinderHub (the Python software), as a standalone service to build and push images with repo2docker, From 4737ff1e0d831cd61ea74ba601e884866172c50c Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Wed, 13 Mar 2024 10:00:58 +0200 Subject: [PATCH 132/200] Redo the structure --- .../images/binderhub-service-diagram.png | Bin 80751 -> 84156 bytes .../architecture-and-implementation.md | 8 +++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/_static/images/binderhub-service-diagram.png b/docs/source/_static/images/binderhub-service-diagram.png index 5aa0830107c3fb0de4760ad923cc6e87324d3c77..4c22249f89397ebc4d89dd66a1ea9dc6dea72d7d 100644 GIT binary patch literal 84156 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 SZ-rCvckhmdVzIpW^Zx Date: Wed, 13 Mar 2024 10:02:46 +0200 Subject: [PATCH 133/200] Rename back to only infrastructure --- .../{architecture-and-implementation.md => architecture.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename docs/source/explanation/{architecture-and-implementation.md => architecture.md} (98%) diff --git a/docs/source/explanation/architecture-and-implementation.md b/docs/source/explanation/architecture.md similarity index 98% rename from docs/source/explanation/architecture-and-implementation.md rename to docs/source/explanation/architecture.md index c4354ee00..672ed1225 100644 --- a/docs/source/explanation/architecture-and-implementation.md +++ b/docs/source/explanation/architecture.md @@ -1,6 +1,6 @@ -(architecture-and-implementation)= +(architecture)= -# Architecture and Implementation +# Architecture The `binderhub-service` chart runs the [BinderHub] Python software, in [api-only mode](https://binderhub.readthedocs.io/en/latest/reference/app.html#binderhub.app.BinderHub.enable_api_only_mode) (the default), as a standalone service to build, and push [Docker] images from source code repositories, on demand, using [repo2docker]. This service can then be paired with [JupyterHub] to allow users to initiate build requests from their hubs. From a276b8341d2f973daded40be05b8e0e112c78622 Mon Sep 17 00:00:00 2001 From: Georgiana Dolocan Date: Wed, 13 Mar 2024 10:04:37 +0200 Subject: [PATCH 134/200] Add the renamed file to the index --- docs/source/explanation/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/explanation/index.md b/docs/source/explanation/index.md index 9736c814a..33d7ebfc3 100644 --- a/docs/source/explanation/index.md +++ b/docs/source/explanation/index.md @@ -16,5 +16,5 @@ Please see our [contributing guide](contributing) if you'd like to add to it. ```{toctree} :maxdepth: 2 -architecture-and-implementation.md +architecture.md ``` From f52e9320f081466f86aade64dfa55d4f1ef5d775 Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Mon, 1 Apr 2024 05:02:12 +0000 Subject: [PATCH 135/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 7a8138379..fa484bd6b 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -34,7 +34,7 @@ docker==7.0.0 # via binderhub escapism==1.0.1 # via binderhub -google-auth==2.28.1 +google-auth==2.29.0 # via kubernetes greenlet==3.0.3 # via sqlalchemy @@ -52,7 +52,7 @@ jsonschema-specifications==2023.12.1 # via jsonschema jupyter-telemetry==0.1.0 # via jupyterhub -jupyterhub==4.0.2 +jupyterhub==4.1.4 # via binderhub kubernetes==29.0.0 # via binderhub @@ -67,7 +67,7 @@ oauthlib==3.2.2 # jupyterhub # kubernetes # requests-oauthlib -packaging==23.2 +packaging==24.0 # via # docker # jupyterhub @@ -77,21 +77,21 @@ prometheus-client==0.20.0 # via # binderhub # jupyterhub -pyasn1==0.5.1 +pyasn1==0.6.0 # via # pyasn1-modules # rsa -pyasn1-modules==0.3.0 +pyasn1-modules==0.4.0 # via google-auth -pycparser==2.21 +pycparser==2.22 # via cffi pycurl==7.45.3 # via binderhub pyjwt==2.8.0 # via binderhub -pyopenssl==24.0.0 +pyopenssl==24.1.0 # via certipy -python-dateutil==2.9.0 +python-dateutil==2.9.0.post0 # via # jupyterhub # kubernetes @@ -101,7 +101,7 @@ python-json-logger==2.0.7 # jupyter-telemetry pyyaml==6.0.1 # via kubernetes -referencing==0.33.0 +referencing==0.34.0 # via # jsonschema # jsonschema-specifications @@ -111,7 +111,7 @@ requests==2.31.0 # jupyterhub # kubernetes # requests-oauthlib -requests-oauthlib==1.3.1 +requests-oauthlib==2.0.0 # via kubernetes rpds-py==0.18.0 # via @@ -127,7 +127,7 @@ six==1.16.0 # via # kubernetes # python-dateutil -sqlalchemy==2.0.27 +sqlalchemy==2.0.29 # via # alembic # jupyterhub @@ -135,7 +135,7 @@ tornado==6.4 # via # binderhub # jupyterhub -traitlets==5.14.1 +traitlets==5.14.2 # via # binderhub # jupyter-telemetry From 36da2f71528c80d9ccc32664d61a269d03672e33 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 19:53:24 +0000 Subject: [PATCH 136/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.0 → v3.15.2](https://github.com/asottile/pyupgrade/compare/v3.15.0...v3.15.2) - [github.com/PyCQA/autoflake: v2.2.1 → v2.3.1](https://github.com/PyCQA/autoflake/compare/v2.2.1...v2.3.1) - [github.com/psf/black: 24.1.1 → 24.3.0](https://github.com/psf/black/compare/24.1.1...24.3.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08bc79dd0..17808463b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.2 hooks: - id: pyupgrade args: @@ -19,7 +19,7 @@ repos: # Autoformat: Python code - repo: https://github.com/PyCQA/autoflake - rev: v2.2.1 + rev: v2.3.1 hooks: - id: autoflake args: @@ -27,7 +27,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 24.1.1 + rev: 24.3.0 hooks: - id: black args: From 2c7ffc5998f54ef71ed70134e0a49da75492350c Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Wed, 1 May 2024 05:01:50 +0000 Subject: [PATCH 137/200] Update library/docker version from 24.0.6-dind to 26.1.1-dind --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 54dfd76e6..d2a1ae21f 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -118,7 +118,7 @@ dockerApi: image: repository: docker.io/library/docker # Temporarily pinned, until https://github.com/2i2c-org/infrastructure/issues/3588 is fixed - tag: "24.0.6-dind" # source: https://hub.docker.com/_/docker/tags + tag: "26.1.1-dind" # source: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From 161a08824b3fbeff62891f75fe77bc294190a27d Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Wed, 1 May 2024 05:02:18 +0000 Subject: [PATCH 138/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index fa484bd6b..fbd7fabcb 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -38,13 +38,13 @@ google-auth==2.29.0 # via kubernetes greenlet==3.0.3 # via sqlalchemy -idna==3.6 +idna==3.7 # via requests jinja2==3.1.3 # via # binderhub # jupyterhub -jsonschema==4.21.1 +jsonschema==4.22.0 # via # binderhub # jupyter-telemetry @@ -52,11 +52,11 @@ jsonschema-specifications==2023.12.1 # via jsonschema jupyter-telemetry==0.1.0 # via jupyterhub -jupyterhub==4.1.4 +jupyterhub==4.1.5 # via binderhub kubernetes==29.0.0 # via binderhub -mako==1.3.2 +mako==1.3.3 # via alembic markupsafe==2.1.5 # via @@ -101,7 +101,7 @@ python-json-logger==2.0.7 # jupyter-telemetry pyyaml==6.0.1 # via kubernetes -referencing==0.34.0 +referencing==0.35.0 # via # jsonschema # jsonschema-specifications @@ -135,12 +135,12 @@ tornado==6.4 # via # binderhub # jupyterhub -traitlets==5.14.2 +traitlets==5.14.3 # via # binderhub # jupyter-telemetry # jupyterhub -typing-extensions==4.10.0 +typing-extensions==4.11.0 # via # alembic # sqlalchemy @@ -149,5 +149,5 @@ urllib3==2.2.1 # docker # kubernetes # requests -websocket-client==1.7.0 +websocket-client==1.8.0 # via kubernetes From 6ba57a09dc9345ed032743ebf5a541f05e352717 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 19:55:07 +0000 Subject: [PATCH 139/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.3.0 → 24.4.2](https://github.com/psf/black/compare/24.3.0...24.4.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17808463b..6728b24f6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 24.4.2 hooks: - id: black args: From 5c079e473ec58e61d778d5c94f30c2597c0d2743 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 22 May 2024 14:49:53 +0200 Subject: [PATCH 140/200] Add extraEnv config for the deployment pod --- binderhub-service/templates/deployment.yaml | 3 +++ binderhub-service/values.schema.yaml | 2 ++ binderhub-service/values.yaml | 1 + tools/templates/lint-and-validate-values.yaml | 3 +++ 4 files changed, 9 insertions(+) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index d432a438a..bef5967ae 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -53,6 +53,9 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace + {{- with .Values.extraEnv }} + {{- tpl (. | toYaml) $ | nindent 12 }} + {{- end }} resources: {{- .Values.resources | toYaml | nindent 12 }} securityContext: diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 01d61000a..d59525945 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -85,6 +85,8 @@ properties: patternProperties: ".*": type: [string, "null"] + extraEnv: + type: array replicas: type: integer diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 54dfd76e6..660fc1989 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -51,6 +51,7 @@ extraConfig: namespace = os.environ["NAMESPACE"] c.KubernetesBuildExecutor.docker_host = f"/var/run/{ namespace }-{ helm_release_name }/docker-api/docker-api.sock" +extraEnv: [] replicas: 1 image: repository: quay.io/2i2c/binderhub-service diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml index 4b337afcb..90da1abc4 100644 --- a/tools/templates/lint-and-validate-values.yaml +++ b/tools/templates/lint-and-validate-values.yaml @@ -21,6 +21,9 @@ buildPodsRegistryCredentials: image: repository: quay.io/2i2c/binderhub-service tag: "set-by-chartpress" +extraEnv: + - name: HELM_RELEASE_NAME + value: "{{ .Release.Name }}" # RBAC resources # ----------------------------------------------------------------------------- From f865710c19827a131657f683bde596875bdc1557 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 22 May 2024 15:01:18 +0200 Subject: [PATCH 141/200] ci: pin requests for now to make chartpress work --- dev-requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 4d16ff995..16129ae73 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -15,3 +15,7 @@ pyyaml # yamllint is used by tools/templates/lint-and-validate.py script yamllint + +# FIXME: We pin requests as docker-py breaks when using 2.32.0, see +# https://github.com/docker/docker-py/issues/3256. +requests==2.31.0 From e15a544b4aade9707023dd9624cbd823d015c297 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 22 May 2024 15:08:30 +0200 Subject: [PATCH 142/200] Drop support for k8s 1.23-1.26 --- .github/workflows/test-chart.yaml | 2 +- binderhub-service/Chart.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index b13d72ae6..d36237d5e 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -115,7 +115,7 @@ jobs: include: - k3s-channel: latest - k3s-channel: stable - - k3s-channel: v1.24 + - k3s-channel: v1.27 steps: - uses: actions/checkout@v4 diff --git a/binderhub-service/Chart.yaml b/binderhub-service/Chart.yaml index d37799576..23c59ecbf 100644 --- a/binderhub-service/Chart.yaml +++ b/binderhub-service/Chart.yaml @@ -11,7 +11,7 @@ keywords: [binderhub, binderhub-service, repo2docker, jupyterhub, jupyter] home: https://2i2c.org/binderhub-service sources: [https://github.com/2i2c-org/binderhub-service] icon: https://binderhub.readthedocs.io/en/latest/_static/logo.png -kubeVersion: ">=1.23.0-0" +kubeVersion: ">=1.27.0-0" maintainers: - name: Erik Sundell email: erik@sundellopensource.se From 1a24e0ae6d1d525fa18d747c1da43244961e02e6 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 22 May 2024 15:11:40 +0200 Subject: [PATCH 143/200] ci: fix pin of requests in release.yaml --- .github/workflows/release.yaml | 3 +++ dev-requirements.txt | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 943f4f915..c03287fc0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -73,6 +73,9 @@ jobs: - name: Install chart publishing dependencies (chartpress, pyyaml, helm) run: | + # FIXME: remove this pin, and the one in dev-requirements.txt + pip install requests==2.31.0 + pip install chartpress pyyaml pip list diff --git a/dev-requirements.txt b/dev-requirements.txt index 16129ae73..02c855b72 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -18,4 +18,5 @@ yamllint # FIXME: We pin requests as docker-py breaks when using 2.32.0, see # https://github.com/docker/docker-py/issues/3256. +# When removing this, also remove the pin in release.yaml. requests==2.31.0 From 263a589b639bf8a4cd6fe54dfa4ed3bbf4336ac7 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 22 May 2024 15:35:53 +0200 Subject: [PATCH 144/200] Update ingress config to align with zj2h config format --- binderhub-service/templates/ingress.yaml | 34 ++++++++----------- binderhub-service/values.schema.yaml | 4 +-- binderhub-service/values.yaml | 10 +++--- tools/templates/lint-and-validate-values.yaml | 13 ++++++- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/binderhub-service/templates/ingress.yaml b/binderhub-service/templates/ingress.yaml index fbab7374c..7b8d39ba1 100644 --- a/binderhub-service/templates/ingress.yaml +++ b/binderhub-service/templates/ingress.yaml @@ -10,32 +10,26 @@ metadata: {{- . | toYaml | nindent 4 }} {{- end }} spec: - {{- with .Values.ingress.className }} - ingressClassName: {{ . }} - {{- end }} - {{- with .Values.ingress.tls }} - tls: - {{- range . }} - - hosts: - {{- .hosts | toYaml | nindent 8 }} - secretName: {{ .secretName }} - {{- end }} + {{- with .Values.ingress.ingressClassName }} + ingressClassName: "{{ . }}" {{- end }} rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: + {{- range $host := .Values.ingress.hosts | default (list "") }} + - http: paths: - {{- range .paths }} - - path: {{ .path }} - {{- with .pathType }} - pathType: {{ . }} - {{- end }} + - path: {{ $.Values.config.BinderHub.base_url | trimSuffix "/" }}/{{ $.Values.ingress.pathSuffix }} + pathType: {{ $.Values.ingress.pathType }} backend: service: name: {{ include "binderhub-service.fullname" $ }} port: - number: {{ $.Values.service.port }} - {{- end }} + name: http + {{- if $host }} + host: {{ $host | quote }} + {{- end }} {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- . | toYaml | nindent 4 }} + {{- end }} {{- end }} diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index d59525945..66e3505a2 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -178,7 +178,8 @@ properties: properties: enabled: type: boolean - className: + annotations: *labels-and-annotations + ingressClassName: type: [string, "null"] hosts: type: array @@ -188,7 +189,6 @@ properties: enum: [Prefix, Exact, ImplementationSpecific] tls: type: array - annotations: *labels-and-annotations # DaemonSet resource - docker-api # --------------------------------------------------------------------------- diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 7dd2da7c0..88ae3ccec 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -99,13 +99,11 @@ service: # ingress: enabled: false - className: "" annotations: {} - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific + ingressClassName: + hosts: [] + pathSuffix: + pathType: Prefix tls: [] # DaemonSet resource - docker-api diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml index 90da1abc4..d0b11bf9f 100644 --- a/tools/templates/lint-and-validate-values.yaml +++ b/tools/templates/lint-and-validate-values.yaml @@ -49,7 +49,18 @@ service: # ----------------------------------------------------------------------------- # ingress: - enabled: false + enabled: true + ingressClassName: mock-ingress-class-name + hosts: + - mocked1.domain.name + - mocked2.domain.name + pathSuffix: dummy-pathSuffix + pathType: ImplementationSpecific + tls: + - secretName: tls + hosts: + - mocked1.domain.name + - mocked2.domain.name # DaemonSet resource - docker-api # ----------------------------------------------------------------------------- From 06e9a55094009f85f05b1db1492785e4fea44b5a Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 22 May 2024 23:04:58 +0200 Subject: [PATCH 145/200] image: install binderhub from main branch instead of pinned commit --- images/binderhub-service/requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/binderhub-service/requirements.in b/images/binderhub-service/requirements.in index 010c446a5..a382677db 100644 --- a/images/binderhub-service/requirements.in +++ b/images/binderhub-service/requirements.in @@ -2,4 +2,4 @@ # To update requirements.txt, use the "Run workflow" button at # https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -binderhub[pycurl] @ git+https://github.com/jupyterhub/binderhub@0dde0c044e70b89ee7bceac9eac73928d215290c +binderhub[pycurl] @ https://github.com/jupyterhub/binderhub/archive/main.zip From d9b3854d4af0f99b585ac43b3efe021a567f54f4 Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Thu, 23 May 2024 08:55:26 +0000 Subject: [PATCH 146/200] Update library/docker version from 26.1.1-dind to 26.1.3-dind --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 88ae3ccec..7c78f51f5 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -117,7 +117,7 @@ dockerApi: image: repository: docker.io/library/docker # Temporarily pinned, until https://github.com/2i2c-org/infrastructure/issues/3588 is fixed - tag: "26.1.1-dind" # source: https://hub.docker.com/_/docker/tags + tag: "26.1.3-dind" # source: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From b9b49ce56f42f3292f0398bfb3e71fa6bd29553b Mon Sep 17 00:00:00 2001 From: GeorgianaElena <7579677+GeorgianaElena@users.noreply.github.com> Date: Fri, 24 May 2024 10:58:29 +0000 Subject: [PATCH 147/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 84 +++++++++++++++-------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index fbd7fabcb..0baef7a9b 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -6,13 +6,15 @@ # alembic==1.13.1 # via jupyterhub -async-generator==1.10 - # via jupyterhub +annotated-types==0.7.0 + # via pydantic +arrow==1.3.0 + # via isoduration attrs==23.2.0 # via # jsonschema # referencing -binderhub @ git+https://github.com/jupyterhub/binderhub@0dde0c044e70b89ee7bceac9eac73928d215290c +binderhub @ https://github.com/jupyterhub/binderhub/archive/main.zip # via # -r requirements.in # binderhub @@ -28,35 +30,44 @@ cffi==1.16.0 # via cryptography charset-normalizer==3.3.2 # via requests -cryptography==42.0.5 +cryptography==42.0.7 # via pyopenssl -docker==7.0.0 +docker==7.1.0 # via binderhub escapism==1.0.1 # via binderhub +fqdn==1.5.1 + # via jsonschema google-auth==2.29.0 # via kubernetes greenlet==3.0.3 # via sqlalchemy idna==3.7 - # via requests -jinja2==3.1.3 + # via + # jsonschema + # jupyterhub + # requests +isoduration==20.11.0 + # via jsonschema +jinja2==3.1.4 # via # binderhub # jupyterhub -jsonschema==4.22.0 +jsonpointer==2.4 + # via jsonschema +jsonschema[format-nongpl]==4.22.0 # via # binderhub - # jupyter-telemetry + # jupyter-events jsonschema-specifications==2023.12.1 # via jsonschema -jupyter-telemetry==0.1.0 +jupyter-events==0.10.0 # via jupyterhub -jupyterhub==4.1.5 +jupyterhub==5.0.0 # via binderhub kubernetes==29.0.0 # via binderhub -mako==1.3.3 +mako==1.3.5 # via alembic markupsafe==2.1.5 # via @@ -68,9 +79,7 @@ oauthlib==3.2.2 # kubernetes # requests-oauthlib packaging==24.0 - # via - # docker - # jupyterhub + # via jupyterhub pamela==1.1.0 # via jupyterhub prometheus-client==0.20.0 @@ -87,25 +96,33 @@ pycparser==2.22 # via cffi pycurl==7.45.3 # via binderhub +pydantic==2.7.1 + # via jupyterhub +pydantic-core==2.18.2 + # via pydantic pyjwt==2.8.0 # via binderhub pyopenssl==24.1.0 # via certipy python-dateutil==2.9.0.post0 # via + # arrow # jupyterhub # kubernetes python-json-logger==2.0.7 # via # binderhub - # jupyter-telemetry + # jupyter-events pyyaml==6.0.1 - # via kubernetes -referencing==0.35.0 + # via + # jupyter-events + # kubernetes +referencing==0.35.1 # via # jsonschema # jsonschema-specifications -requests==2.31.0 + # jupyter-events +requests==2.32.2 # via # docker # jupyterhub @@ -113,21 +130,26 @@ requests==2.31.0 # requests-oauthlib requests-oauthlib==2.0.0 # via kubernetes -rpds-py==0.18.0 +rfc3339-validator==0.1.4 + # via + # jsonschema + # jupyter-events +rfc3986-validator==0.1.1 + # via + # jsonschema + # jupyter-events +rpds-py==0.18.1 # via # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.18.6 - # via jupyter-telemetry -ruamel-yaml-clib==0.2.8 - # via ruamel-yaml six==1.16.0 # via # kubernetes # python-dateutil -sqlalchemy==2.0.29 + # rfc3339-validator +sqlalchemy==2.0.30 # via # alembic # jupyterhub @@ -138,16 +160,24 @@ tornado==6.4 traitlets==5.14.3 # via # binderhub - # jupyter-telemetry + # jupyter-events # jupyterhub -typing-extensions==4.11.0 +types-python-dateutil==2.9.0.20240316 + # via arrow +typing-extensions==4.12.0 # via # alembic + # pydantic + # pydantic-core # sqlalchemy +uri-template==1.3.0 + # via jsonschema urllib3==2.2.1 # via # docker # kubernetes # requests +webcolors==1.13 + # via jsonschema websocket-client==1.8.0 # via kubernetes From fb7015e7cec6c56bb722d3e62f2b09040051cbaf Mon Sep 17 00:00:00 2001 From: Georgiana Date: Fri, 24 May 2024 14:47:10 +0300 Subject: [PATCH 148/200] Install pycurl from source --- images/binderhub-service/requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/binderhub-service/requirements.in b/images/binderhub-service/requirements.in index a382677db..efb65e54f 100644 --- a/images/binderhub-service/requirements.in +++ b/images/binderhub-service/requirements.in @@ -2,4 +2,4 @@ # To update requirements.txt, use the "Run workflow" button at # https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -binderhub[pycurl] @ https://github.com/jupyterhub/binderhub/archive/main.zip +binderhub[pycurl] @ https://github.com/jupyterhub/binderhub/archive/main.zip --no-binary pycurl From 95b492aaa0fde0284252fd432d5ffb126867dec0 Mon Sep 17 00:00:00 2001 From: GeorgianaElena <7579677+GeorgianaElena@users.noreply.github.com> Date: Fri, 24 May 2024 12:12:27 +0000 Subject: [PATCH 149/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 0baef7a9b..69300ede0 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,6 +4,8 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # +--no-binary pycurl + alembic==1.13.1 # via jupyterhub annotated-types==0.7.0 From cd031d9ff6711278ca37e9368c1c90de0ab31024 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Fri, 24 May 2024 15:18:55 +0300 Subject: [PATCH 150/200] Update requirements.in to specify pycurl separately --- images/binderhub-service/requirements.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/images/binderhub-service/requirements.in b/images/binderhub-service/requirements.in index efb65e54f..0dea4272d 100644 --- a/images/binderhub-service/requirements.in +++ b/images/binderhub-service/requirements.in @@ -2,4 +2,5 @@ # To update requirements.txt, use the "Run workflow" button at # https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -binderhub[pycurl] @ https://github.com/jupyterhub/binderhub/archive/main.zip --no-binary pycurl +binderhub @ https://github.com/jupyterhub/binderhub/archive/main.zip +pycurl --no-binary From 4587ac36d36a5be7cb14937579cd0001671d41e1 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Fri, 24 May 2024 15:30:33 +0300 Subject: [PATCH 151/200] The other way around --- images/binderhub-service/requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/binderhub-service/requirements.in b/images/binderhub-service/requirements.in index 0dea4272d..79ef57097 100644 --- a/images/binderhub-service/requirements.in +++ b/images/binderhub-service/requirements.in @@ -3,4 +3,4 @@ # https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # binderhub @ https://github.com/jupyterhub/binderhub/archive/main.zip -pycurl --no-binary +--no-binary pycurl From ddc4c34693ddabc04e3328d7eb4afb701f99c3ab Mon Sep 17 00:00:00 2001 From: GeorgianaElena <7579677+GeorgianaElena@users.noreply.github.com> Date: Fri, 24 May 2024 12:31:42 +0000 Subject: [PATCH 152/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 69300ede0..9d1d76c1b 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -17,9 +17,7 @@ attrs==23.2.0 # jsonschema # referencing binderhub @ https://github.com/jupyterhub/binderhub/archive/main.zip - # via - # -r requirements.in - # binderhub + # via -r requirements.in cachetools==5.3.3 # via google-auth certifi==2024.2.2 @@ -96,8 +94,6 @@ pyasn1-modules==0.4.0 # via google-auth pycparser==2.22 # via cffi -pycurl==7.45.3 - # via binderhub pydantic==2.7.1 # via jupyterhub pydantic-core==2.18.2 From d7e3287ec495790517b65f0da4b0de23d6ab08c0 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Fri, 24 May 2024 15:39:33 +0300 Subject: [PATCH 153/200] Update requirements.in --- images/binderhub-service/requirements.in | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/images/binderhub-service/requirements.in b/images/binderhub-service/requirements.in index 79ef57097..9bb695b75 100644 --- a/images/binderhub-service/requirements.in +++ b/images/binderhub-service/requirements.in @@ -2,5 +2,4 @@ # To update requirements.txt, use the "Run workflow" button at # https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -binderhub @ https://github.com/jupyterhub/binderhub/archive/main.zip ---no-binary pycurl +binderhub[pycurl] @ git+https://github.com/jupyterhub/binderhub@main From c03f07f5df597c5feaa449bdb147d6e6c8498e83 Mon Sep 17 00:00:00 2001 From: GeorgianaElena <7579677+GeorgianaElena@users.noreply.github.com> Date: Fri, 24 May 2024 12:40:38 +0000 Subject: [PATCH 154/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 9d1d76c1b..038bb756c 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,8 +4,6 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # ---no-binary pycurl - alembic==1.13.1 # via jupyterhub annotated-types==0.7.0 @@ -16,8 +14,10 @@ attrs==23.2.0 # via # jsonschema # referencing -binderhub @ https://github.com/jupyterhub/binderhub/archive/main.zip - # via -r requirements.in +binderhub @ git+https://github.com/jupyterhub/binderhub@main + # via + # -r requirements.in + # binderhub cachetools==5.3.3 # via google-auth certifi==2024.2.2 @@ -94,6 +94,8 @@ pyasn1-modules==0.4.0 # via google-auth pycparser==2.22 # via cffi +pycurl==7.45.3 + # via binderhub pydantic==2.7.1 # via jupyterhub pydantic-core==2.18.2 From 53ef60a972b7e2ba0148c0c33d749840e9ccdb1f Mon Sep 17 00:00:00 2001 From: Georgiana Date: Mon, 27 May 2024 18:22:07 +0300 Subject: [PATCH 155/200] Build pycurl from source --- images/binderhub-service/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/images/binderhub-service/Dockerfile b/images/binderhub-service/Dockerfile index 328901c9f..2cca9fa50 100644 --- a/images/binderhub-service/Dockerfile +++ b/images/binderhub-service/Dockerfile @@ -27,6 +27,9 @@ RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ pip install build \ && pip wheel \ --wheel-dir=/tmp/wheels \ + # pycurl wheels for 7.45.3 have problems finding CAs + # https://github.com/pycurl/pycurl/issues/834 + --no-binary pycurl \ -r requirements.txt From 748f2c82868a2700489d6698b27af1d50d06f701 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Mon, 27 May 2024 18:24:23 +0300 Subject: [PATCH 156/200] Rm unused build installation --- images/binderhub-service/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/images/binderhub-service/Dockerfile b/images/binderhub-service/Dockerfile index 2cca9fa50..a5f0b30f9 100644 --- a/images/binderhub-service/Dockerfile +++ b/images/binderhub-service/Dockerfile @@ -24,8 +24,7 @@ RUN apt-get update \ COPY requirements.txt requirements.txt ARG PIP_CACHE_DIR=/tmp/pip-cache RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ - pip install build \ - && pip wheel \ + pip wheel \ --wheel-dir=/tmp/wheels \ # pycurl wheels for 7.45.3 have problems finding CAs # https://github.com/pycurl/pycurl/issues/834 From 05267bd72fad17dbecfff50de43c28b0b80d2402 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Mon, 27 May 2024 18:29:56 +0300 Subject: [PATCH 157/200] Use --no-index to make sure we install the built wheels --- images/binderhub-service/Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/images/binderhub-service/Dockerfile b/images/binderhub-service/Dockerfile index a5f0b30f9..096385ead 100644 --- a/images/binderhub-service/Dockerfile +++ b/images/binderhub-service/Dockerfile @@ -64,9 +64,12 @@ COPY requirements.txt requirements.txt # RUN sed -i -E 's/binderhub @ git.+/binderhub/' requirements.txt ARG PIP_CACHE_DIR=/tmp/pip-cache +# --no-index ensures _only_ wheels from the build stage are installed RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ --mount=type=cache,from=build-stage,source=/tmp/wheels,target=/tmp/wheels \ - pip install --find-links=/tmp/wheels/ \ + pip install + --no-index \ + --find-links=/tmp/wheels/ \ -r requirements.txt ENV PYTHONUNBUFFERED=1 From e4c10c5ddf59ac2184ec9427fa36ff9e4a9a9345 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Mon, 27 May 2024 18:34:03 +0300 Subject: [PATCH 158/200] Add missing line sep --- images/binderhub-service/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/binderhub-service/Dockerfile b/images/binderhub-service/Dockerfile index 096385ead..1f8f5a4b6 100644 --- a/images/binderhub-service/Dockerfile +++ b/images/binderhub-service/Dockerfile @@ -67,7 +67,7 @@ ARG PIP_CACHE_DIR=/tmp/pip-cache # --no-index ensures _only_ wheels from the build stage are installed RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ --mount=type=cache,from=build-stage,source=/tmp/wheels,target=/tmp/wheels \ - pip install + pip install \ --no-index \ --find-links=/tmp/wheels/ \ -r requirements.txt From d263f84afec67d0aaf96572e1e874dd5aabe5455 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Mon, 27 May 2024 20:44:57 +0300 Subject: [PATCH 159/200] Make sure ruamel-yaml is installed We need ruamel-yaml pkg in the binderhub-service image because it gets used by binderhub_config.py. Prior to this, it get installed via jupyterhub (via jupyter-telemetry), but since jupyterhub 5 (the version that gets installed with this chart), jupyter-telemetry was replaced, so we need be explicitly install it. --- images/binderhub-service/requirements.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/images/binderhub-service/requirements.in b/images/binderhub-service/requirements.in index 9bb695b75..bd3d87828 100644 --- a/images/binderhub-service/requirements.in +++ b/images/binderhub-service/requirements.in @@ -2,4 +2,7 @@ # To update requirements.txt, use the "Run workflow" button at # https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # +# needed by binderhub.config.py +ruamel-yaml + binderhub[pycurl] @ git+https://github.com/jupyterhub/binderhub@main From daf17cc225a92e38b9b85ba456db3fc3bae02ec7 Mon Sep 17 00:00:00 2001 From: GeorgianaElena <7579677+GeorgianaElena@users.noreply.github.com> Date: Mon, 27 May 2024 17:46:42 +0000 Subject: [PATCH 160/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 038bb756c..cc5e87dc9 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -144,6 +144,10 @@ rpds-py==0.18.1 # referencing rsa==4.9 # via google-auth +ruamel-yaml==0.18.6 + # via -r requirements.in +ruamel-yaml-clib==0.2.8 + # via ruamel-yaml six==1.16.0 # via # kubernetes From 98c3312854dca67c68d56e552e21c81760de17d7 Mon Sep 17 00:00:00 2001 From: Georgiana Date: Tue, 11 Jun 2024 10:11:50 +0300 Subject: [PATCH 161/200] Add LICENSE --- LICENSE | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 1333ed77b..c38d4181a 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,28 @@ -TODO +BSD 3-Clause License + +Copyright (c) 2024, 2i2c + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 1e880149ebc6e71c3ce7280a9121769cd9b99c8e Mon Sep 17 00:00:00 2001 From: GeorgianaElena <7579677+GeorgianaElena@users.noreply.github.com> Date: Thu, 13 Jun 2024 12:32:50 +0000 Subject: [PATCH 162/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index cc5e87dc9..aeaf73de8 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -20,7 +20,7 @@ binderhub @ git+https://github.com/jupyterhub/binderhub@main # binderhub cachetools==5.3.3 # via google-auth -certifi==2024.2.2 +certifi==2024.6.2 # via # kubernetes # requests @@ -30,7 +30,7 @@ cffi==1.16.0 # via cryptography charset-normalizer==3.3.2 # via requests -cryptography==42.0.7 +cryptography==42.0.8 # via pyopenssl docker==7.1.0 # via binderhub @@ -38,7 +38,7 @@ escapism==1.0.1 # via binderhub fqdn==1.5.1 # via jsonschema -google-auth==2.29.0 +google-auth==2.30.0 # via kubernetes greenlet==3.0.3 # via sqlalchemy @@ -53,7 +53,7 @@ jinja2==3.1.4 # via # binderhub # jupyterhub -jsonpointer==2.4 +jsonpointer==3.0.0 # via jsonschema jsonschema[format-nongpl]==4.22.0 # via @@ -65,7 +65,7 @@ jupyter-events==0.10.0 # via jupyterhub jupyterhub==5.0.0 # via binderhub -kubernetes==29.0.0 +kubernetes==30.1.0 # via binderhub mako==1.3.5 # via alembic @@ -78,7 +78,7 @@ oauthlib==3.2.2 # jupyterhub # kubernetes # requests-oauthlib -packaging==24.0 +packaging==24.1 # via jupyterhub pamela==1.1.0 # via jupyterhub @@ -96,9 +96,9 @@ pycparser==2.22 # via cffi pycurl==7.45.3 # via binderhub -pydantic==2.7.1 +pydantic==2.7.4 # via jupyterhub -pydantic-core==2.18.2 +pydantic-core==2.18.4 # via pydantic pyjwt==2.8.0 # via binderhub @@ -122,7 +122,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications # jupyter-events -requests==2.32.2 +requests==2.32.3 # via # docker # jupyterhub @@ -157,7 +157,7 @@ sqlalchemy==2.0.30 # via # alembic # jupyterhub -tornado==6.4 +tornado==6.4.1 # via # binderhub # jupyterhub @@ -168,7 +168,7 @@ traitlets==5.14.3 # jupyterhub types-python-dateutil==2.9.0.20240316 # via arrow -typing-extensions==4.12.0 +typing-extensions==4.12.2 # via # alembic # pydantic @@ -181,7 +181,7 @@ urllib3==2.2.1 # docker # kubernetes # requests -webcolors==1.13 +webcolors==24.6.0 # via jsonschema websocket-client==1.8.0 # via kubernetes From 44f1d1f28078d0c938d67436df429703bf4df98b Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Tue, 18 Jun 2024 17:03:24 -0700 Subject: [PATCH 163/200] Document the primary restriction of this chart Fixes https://github.com/2i2c-org/binderhub-service/issues/95 --- README.md | 59 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index dab731845..c40344246 100644 --- a/README.md +++ b/README.md @@ -7,32 +7,44 @@ The binderhub-service is a Helm chart and guide to run BinderHub (the Python software), as a standalone service to build and push images with repo2docker, possibly configured for use with a JupyterHub chart installation. -## Background +## History -The [binderhub chart]'s main use case has been to build images and launch -servers based on them for anonymous users without persistent home folder -storage. The binderhub chart does this by installing the [jupyterhub chart] -opinionatedly configured to not authenticate and provide users with home folder -storage. +The BinderHub project provides two major pieces of functionality: -There are use cases for putting binderhub behind authentication though, so -support for that [was added]. There are also use cases for providing users with -persistent home folders, and this led to [persistent binderhub chart] being -developed. The persistent binderhub chart, by depending on the binderhub chart, -depending on the jupyterhub chart, is even more complex than the binderhub chart -though. Currently, the project isn't actively maintained. +1. Building (and pushing) images via an API using content from various + providers. +2. Launch interactive sessions using the built images (via a JupyterHub). -Could a new chart be developed to deploy binderhub next to an existing -jupyterhub instead, or even entirely on its own without the part where the built -image is launched in a jupyterhub? Could this enable existing jupyterhub chart -installations to add on binderhub like functionality? This is what this project -is exploring! +The current upstream [binderhub helm chart](https://github.com/jupyterhub/binderhub/tree/main/helm-chart) +is a very opinionated distribution, focusing purely on public instances of BinderHub +(such as [mybinder.org](https://mybinder.org)). It has strong opinions on how +the JupyterHub should be configured, and how it should be connected to the BinderHub. +While historically this allowed for faster iteration on mybinder.org itself, +it has major limitations when used elsewhere. -## Project scope +1. It places restrictions on how the JupyterHub used to launch the interactive sessions + can be installed and configured. It required workarounds for several types + of configuration, particularly around persistence (see [persistent binderhub](https://github.com/gesiscss/persistent_binderhub) + for example). +2. It can not be deployed without the attached, opinionated JupyterHub it comes with. + This makes deployment for use with alternate frontends (such as + [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) + difficult) -This project is currently developed to provide a Helm chart and documentation to -deploy and configure BinderHub the Python software for use either by itself, or -next to a JupyterHub Helm chart installation. +This project is designed to provide a standalone helm chart that does not have these +restrictions. + +## Restrictions + +To prevent a recurrance of the issues with the existing binderhub chart, the following +restrictions are in place for any work on this chart: + +> There will not be a *direct* dependency on a JupyterHub. We can provide documentation on +> how to set this chart up next to a JupyterHub, but we will not provide a JupyterHub +> directly (via a [helm dependency](https://helm.sh/docs/chart_best_practices/dependencies/)) +> or otherwise. + +## Scope The documentation should help configure the BinderHub service to: @@ -41,11 +53,6 @@ The documentation should help configure the BinderHub service to: - in one or more ways handle the issue repo2docker building an image with data put where JupyterHub user home folders typically is mounted -[binderhub chart]: https://github.com/jupyterhub/binderhub -[jupyterhub chart]: https://github.com/jupyterhub/zero-to-jupyterhub-k8s -[persistent binderhub chart]: https://github.com/gesiscss/persistent_binderhub -[was added]: https://github.com/jupyterhub/binderhub/pull/666 - ## Installation Checkout this project's documentation for installation guide https://binderhub-service.readthedocs.io/en/latest. From 045ca1f69a1ceda00980f199d2d7b927d4eeab3b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:06:14 +0000 Subject: [PATCH 164/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c40344246..12c5dffa9 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ restrictions. To prevent a recurrance of the issues with the existing binderhub chart, the following restrictions are in place for any work on this chart: -> There will not be a *direct* dependency on a JupyterHub. We can provide documentation on +> There will not be a _direct_ dependency on a JupyterHub. We can provide documentation on > how to set this chart up next to a JupyterHub, but we will not provide a JupyterHub > directly (via a [helm dependency](https://helm.sh/docs/chart_best_practices/dependencies/)) > or otherwise. From d5bbfe6cfeb5e32eeda0142d986e3ee6ebca2750 Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Mon, 1 Jul 2024 05:02:10 +0000 Subject: [PATCH 165/200] Update library/docker version from 26.1.3-dind to 27.0.2-dind --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 7c78f51f5..2826c991d 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -117,7 +117,7 @@ dockerApi: image: repository: docker.io/library/docker # Temporarily pinned, until https://github.com/2i2c-org/infrastructure/issues/3588 is fixed - tag: "26.1.3-dind" # source: https://hub.docker.com/_/docker/tags + tag: "27.0.2-dind" # source: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From d511f9586351096a213257a8ce98f20f5623d033 Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Mon, 1 Jul 2024 05:02:23 +0000 Subject: [PATCH 166/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index aeaf73de8..69de4f63e 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,7 +4,7 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -alembic==1.13.1 +alembic==1.13.2 # via jupyterhub annotated-types==0.7.0 # via pydantic @@ -153,7 +153,7 @@ six==1.16.0 # kubernetes # python-dateutil # rfc3339-validator -sqlalchemy==2.0.30 +sqlalchemy==2.0.31 # via # alembic # jupyterhub @@ -176,7 +176,7 @@ typing-extensions==4.12.2 # sqlalchemy uri-template==1.3.0 # via jsonschema -urllib3==2.2.1 +urllib3==2.2.2 # via # docker # kubernetes From 26c87073a65d31fa2f574d2b844a7fd13ee6f87a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:48:54 +0000 Subject: [PATCH 167/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.2 → v3.16.0](https://github.com/asottile/pyupgrade/compare/v3.15.2...v3.16.0) - [github.com/PyCQA/flake8: 7.0.0 → 7.1.0](https://github.com/PyCQA/flake8/compare/7.0.0...7.1.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6728b24f6..2a7e88926 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.16.0 hooks: - id: pyupgrade args: @@ -51,7 +51,7 @@ repos: # Linting: Python code (see the file .flake8) - repo: https://github.com/PyCQA/flake8 - rev: "7.0.0" + rev: "7.1.0" hooks: - id: flake8 # Ignore style and complexity From 7580133577c844e0673143787934b52243e3e3fa Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 4 Jul 2024 15:47:30 +0200 Subject: [PATCH 168/200] Change default naming to not include release name --- .../templates/_helpers-labels.tpl | 19 +++ .../templates/_helpers-names.tpl | 119 ++++++++++++++++++ binderhub-service/templates/_helpers.tpl | 65 ---------- .../build-pods-docker-config/secret.yaml | 4 +- binderhub-service/templates/deployment.yaml | 8 +- .../templates/docker-api/daemonset.yaml | 4 +- .../templates/docker-api/secret.yaml | 2 +- binderhub-service/templates/ingress.yaml | 4 +- binderhub-service/templates/role.yaml | 2 +- binderhub-service/templates/rolebinding.yaml | 6 +- binderhub-service/templates/secret.yaml | 2 +- binderhub-service/templates/service.yaml | 2 +- .../templates/serviceaccount.yaml | 2 +- binderhub-service/values.schema.yaml | 4 +- 14 files changed, 157 insertions(+), 86 deletions(-) create mode 100644 binderhub-service/templates/_helpers-labels.tpl create mode 100644 binderhub-service/templates/_helpers-names.tpl diff --git a/binderhub-service/templates/_helpers-labels.tpl b/binderhub-service/templates/_helpers-labels.tpl new file mode 100644 index 000000000..d500be365 --- /dev/null +++ b/binderhub-service/templates/_helpers-labels.tpl @@ -0,0 +1,19 @@ +{{- /* + Common labels +*/}} +{{- define "binderhub-service.labels" -}} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{ include "binderhub-service.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- /* + Selector labels +*/}} +{{- define "binderhub-service.selectorLabels" -}} +app.kubernetes.io/name: {{ .Values.nameOverride | default .Chart.Name | trunc 63 | trimSuffix "-" }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/binderhub-service/templates/_helpers-names.tpl b/binderhub-service/templates/_helpers-names.tpl new file mode 100644 index 000000000..c84bdd53d --- /dev/null +++ b/binderhub-service/templates/_helpers-names.tpl @@ -0,0 +1,119 @@ +{{- /* + These helpers encapsulates logic on how we name resources. They also enable + parent charts to reference these dynamic resource names. + + To avoid duplicating documentation, for more information, please see the the + fullnameOverride entry the jupyterhub chart's configuration reference: + https://z2jh.jupyter.org/en/latest/resources/reference.html#fullnameOverride +*/}} + + + +{{- /* + Utility templates +*/}} + +{{- /* + Renders to a prefix for the chart's resource names. This prefix is assumed to + make the resource name cluster unique. +*/}} +{{- define "binderhub-service.fullname" -}} + {{- /* + We have implemented a trick to allow a parent chart depending on this + chart to call these named templates. + + Caveats and notes: + + 1. While parent charts can reference these, grandparent charts can't. + 2. Parent charts must not use an alias for this chart. + 3. There is no failsafe workaround to above due to + https://github.com/helm/helm/issues/9214. + 4. .Chart is of its own type (*chart.Metadata) and needs to be casted + using "toYaml | fromYaml" in order to be able to use normal helm + template functions on it. + */}} + {{- $fullname_override := .Values.fullnameOverride }} + {{- $name_override := .Values.nameOverride }} + {{- if ne .Chart.Name "binderhub-service" }} + {{- if .Values.jupyterhub }} + {{- $fullname_override = .Values.jupyterhub.fullnameOverride }} + {{- $name_override = .Values.jupyterhub.nameOverride }} + {{- end }} + {{- end }} + + {{- if eq (typeOf $fullname_override) "string" }} + {{- $fullname_override }} + {{- else }} + {{- $name := $name_override | default .Chart.Name }} + {{- if contains $name .Release.Name }} + {{- .Release.Name }} + {{- else }} + {{- .Release.Name }}-{{ $name }} + {{- end }} + {{- end }} +{{- end }} + +{{- /* + Renders to a blank string or if the fullname template is truthy renders to it + with an appended dash. +*/}} +{{- define "binderhub-service.fullname.dash" -}} + {{- if (include "binderhub-service.fullname" .) }} + {{- include "binderhub-service.fullname" . }}- + {{- end }} +{{- end }} + + + +{{- /* + Namespaced resources +*/}} + +{{- /* binderhub resources' default name */}} +{{- define "binderhub-service.binderhub.fullname" -}} + {{- include "binderhub-service.fullname.dash" . }}binderhub +{{- end }} + +{{- /* binderhub's ServiceAccount name */}} +{{- define "binderhub-service.binderhub.serviceaccount.fullname" -}} + {{- if .Values.serviceAccount.create }} + {{- .Values.serviceAccount.name | default (include "binderhub-service.binderhub.fullname" .) }} + {{- else }} + {{- .Values.serviceAccount.name }} + {{- end }} +{{- end }} + +{{- /* binderhub's Ingress name */}} +{{- define "binderhub-service.binderhub.ingress.fullname" -}} + {{- if (include "binderhub-service.fullname" .) }} + {{- include "binderhub-service.fullname" . }} + {{- else -}} + binderhub + {{- end }} +{{- end }} + +{{- /* docker-api resources' default name */}} +{{- define "binderhub-service.docker-api.fullname" -}} + {{- include "binderhub-service.fullname.dash" . }}docker-api +{{- end }} + +{{- /* build-pods-docker-config name */}} +{{- define "binderhub-service.build-pods-docker-config.fullname" -}} + {{- include "binderhub-service.fullname.dash" . }}build-pods-docker-config +{{- end }} + + + +{{- /* + Cluster wide resources + + We enforce uniqueness of names for our cluster wide resources. We assume that + the prefix from setting fullnameOverride to null or a string will be cluster + unique. +*/}} + +{{- /* + We currently have no cluster wide resources, but if you add one below in the + future, remove this comment and add an entry mimicing how the jupyterhub helm + chart does it. +*/}} diff --git a/binderhub-service/templates/_helpers.tpl b/binderhub-service/templates/_helpers.tpl index 5a443a6d1..a5762b721 100644 --- a/binderhub-service/templates/_helpers.tpl +++ b/binderhub-service/templates/_helpers.tpl @@ -1,68 +1,3 @@ -{{- /* - Expand the name of the chart. -*/}} -{{- define "binderhub-service.name" -}} -{{- .Values.nameOverride | default .Chart.Name | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{- /* - Create a default fully qualified app name. - We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). - If release name contains chart name it will be used as a full name. -*/}} -{{- define "binderhub-service.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := .Values.nameOverride | default .Chart.Name }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{- /* - Create chart name and version as used by the chart label. -*/}} -{{- define "binderhub-service.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{- /* - Common labels -*/}} -{{- define "binderhub-service.labels" -}} -helm.sh/chart: {{ include "binderhub-service.chart" . }} -{{ include "binderhub-service.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{- /* - Selector labels -*/}} -{{- define "binderhub-service.selectorLabels" -}} -app.kubernetes.io/name: {{ include "binderhub-service.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{- /* - Create the name of the service account to use -*/}} -{{- define "binderhub-service.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- include "binderhub-service.fullname" . | default .Values.serviceAccount.name }} -{{- else }} -{{- .Values.serviceAccount.name }} -{{- end }} -{{- end }} - - - {{- /* binderhub-service.chart-version-to-git-ref: Renders a valid git reference from a chartpress generated version string. diff --git a/binderhub-service/templates/build-pods-docker-config/secret.yaml b/binderhub-service/templates/build-pods-docker-config/secret.yaml index 9d3a3f176..410b7a1f1 100644 --- a/binderhub-service/templates/build-pods-docker-config/secret.yaml +++ b/binderhub-service/templates/build-pods-docker-config/secret.yaml @@ -7,9 +7,7 @@ kind: Secret apiVersion: v1 metadata: - # If this is changed, update the value of the PUSH_SECRET_NAME environment - # variable in the binderhub deployment (in deployment.yaml) - name: {{ include "binderhub-service.fullname" . }}-build-pods-docker-config + name: {{ include "binderhub-service.build-pods-docker-config.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} type: Opaque diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index bef5967ae..ae2904ca5 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "binderhub-service.fullname" . }} + name: {{ include "binderhub-service.binderhub.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} spec: @@ -24,7 +24,7 @@ spec: volumes: - name: secret secret: - secretName: {{ include "binderhub-service.fullname" . }} + secretName: {{ include "binderhub-service.binderhub.fullname" . }} containers: - name: binderhub image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" @@ -40,7 +40,7 @@ spec: readOnly: true env: - name: PUSH_SECRET_NAME - value: {{ include "binderhub-service.fullname" . }}-build-pods-docker-config + value: {{ include "binderhub-service.build-pods-docker-config.fullname" . }} - name: HELM_RELEASE_NAME value: {{ .Release.Name }} # Namespace build pods should be placed in @@ -70,7 +70,7 @@ spec: imagePullSecrets: {{- . | toYaml | nindent 8 }} {{- end }} - {{- with include "binderhub-service.serviceAccountName" . }} + {{- with include "binderhub-service.binderhub.serviceaccount.fullname" . }} serviceAccountName: {{ . }} {{- end }} {{- with .Values.podSecurityContext }} diff --git a/binderhub-service/templates/docker-api/daemonset.yaml b/binderhub-service/templates/docker-api/daemonset.yaml index 5385795ca..3427b8014 100644 --- a/binderhub-service/templates/docker-api/daemonset.yaml +++ b/binderhub-service/templates/docker-api/daemonset.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: DaemonSet metadata: - name: {{ include "binderhub-service.fullname" . }}-docker-api + name: {{ include "binderhub-service.docker-api.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} app.kubernetes.io/component: docker-api @@ -57,7 +57,7 @@ spec: {{- if .Values.dockerApi.extraFiles }} - name: files secret: - secretName: {{ include "binderhub-service.fullname" . }}-docker-api + secretName: {{ include "binderhub-service.docker-api.fullname" . }} items: {{- range $file_key, $file_details := .Values.dockerApi.extraFiles }} - key: {{ $file_key | quote }} diff --git a/binderhub-service/templates/docker-api/secret.yaml b/binderhub-service/templates/docker-api/secret.yaml index 719add616..258369dd7 100644 --- a/binderhub-service/templates/docker-api/secret.yaml +++ b/binderhub-service/templates/docker-api/secret.yaml @@ -2,7 +2,7 @@ kind: Secret apiVersion: v1 metadata: - name: {{ include "binderhub-service.fullname" . }}-docker-api + name: {{ include "binderhub-service.docker-api.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} type: Opaque diff --git a/binderhub-service/templates/ingress.yaml b/binderhub-service/templates/ingress.yaml index 7b8d39ba1..510fbe3d6 100644 --- a/binderhub-service/templates/ingress.yaml +++ b/binderhub-service/templates/ingress.yaml @@ -2,7 +2,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: {{ include "binderhub-service.fullname" . }} + name: {{ include "binderhub-service.binderhub.ingress.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} @@ -21,7 +21,7 @@ spec: pathType: {{ $.Values.ingress.pathType }} backend: service: - name: {{ include "binderhub-service.fullname" $ }} + name: {{ include "binderhub-service.binderhub.fullname" $ }} port: name: http {{- if $host }} diff --git a/binderhub-service/templates/role.yaml b/binderhub-service/templates/role.yaml index 9d56b73cb..40323383c 100644 --- a/binderhub-service/templates/role.yaml +++ b/binderhub-service/templates/role.yaml @@ -2,7 +2,7 @@ kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: {{ include "binderhub-service.fullname" . }} + name: {{ include "binderhub-service.binderhub.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} rules: diff --git a/binderhub-service/templates/rolebinding.yaml b/binderhub-service/templates/rolebinding.yaml index c8265fc63..067b1dba7 100644 --- a/binderhub-service/templates/rolebinding.yaml +++ b/binderhub-service/templates/rolebinding.yaml @@ -2,15 +2,15 @@ kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: {{ include "binderhub-service.fullname" . }} + name: {{ include "binderhub-service.binderhub.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} subjects: - kind: ServiceAccount namespace: {{ .Release.Namespace }} - name: {{ include "binderhub-service.serviceAccountName" . }} + name: {{ include "binderhub-service.binderhub.serviceaccount.fullname" . }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role - name: {{ include "binderhub-service.fullname" . }} + name: {{ include "binderhub-service.binderhub.fullname" . }} {{- end }} diff --git a/binderhub-service/templates/secret.yaml b/binderhub-service/templates/secret.yaml index 86f1631a6..eca525c1a 100644 --- a/binderhub-service/templates/secret.yaml +++ b/binderhub-service/templates/secret.yaml @@ -5,7 +5,7 @@ kind: Secret apiVersion: v1 metadata: - name: {{ include "binderhub-service.fullname" . }} + name: {{ include "binderhub-service.binderhub.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} type: Opaque diff --git a/binderhub-service/templates/service.yaml b/binderhub-service/templates/service.yaml index 5bd11cc42..f3ba4b042 100644 --- a/binderhub-service/templates/service.yaml +++ b/binderhub-service/templates/service.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "binderhub-service.fullname" . }} + name: {{ include "binderhub-service.binderhub.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} {{- with .Values.service.annotations }} annotations: diff --git a/binderhub-service/templates/serviceaccount.yaml b/binderhub-service/templates/serviceaccount.yaml index ef917ccf9..0ba471204 100644 --- a/binderhub-service/templates/serviceaccount.yaml +++ b/binderhub-service/templates/serviceaccount.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ include "binderhub-service.serviceAccountName" . }} + name: {{ include "binderhub-service.binderhub.serviceaccount.fullname" . }} labels: {{- include "binderhub-service.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 66e3505a2..46eaedb3b 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -41,9 +41,9 @@ properties: # --------------------------------------------------------------------------- # nameOverride: - type: string + type: [string, "null"] fullnameOverride: - type: string + type: [string, "null"] global: type: object additionalProperties: true From 81551667d49585e7b38b49a7214a90c2f9a1f185 Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Thu, 11 Jul 2024 09:33:56 +0000 Subject: [PATCH 169/200] Update library/docker version from 27.0.2-dind to 27.0.3-dind --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 2826c991d..b82c1672f 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -117,7 +117,7 @@ dockerApi: image: repository: docker.io/library/docker # Temporarily pinned, until https://github.com/2i2c-org/infrastructure/issues/3588 is fixed - tag: "27.0.2-dind" # source: https://hub.docker.com/_/docker/tags + tag: "27.0.3-dind" # source: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From 07e95a909a98b4e2cc0a74ffe2f6060962fad2c0 Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Thu, 11 Jul 2024 09:34:13 +0000 Subject: [PATCH 170/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 69de4f63e..e4090547e 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -20,7 +20,7 @@ binderhub @ git+https://github.com/jupyterhub/binderhub@main # binderhub cachetools==5.3.3 # via google-auth -certifi==2024.6.2 +certifi==2024.7.4 # via # kubernetes # requests @@ -38,7 +38,7 @@ escapism==1.0.1 # via binderhub fqdn==1.5.1 # via jsonschema -google-auth==2.30.0 +google-auth==2.32.0 # via kubernetes greenlet==3.0.3 # via sqlalchemy @@ -55,7 +55,7 @@ jinja2==3.1.4 # jupyterhub jsonpointer==3.0.0 # via jsonschema -jsonschema[format-nongpl]==4.22.0 +jsonschema[format-nongpl]==4.23.0 # via # binderhub # jupyter-events @@ -96,9 +96,9 @@ pycparser==2.22 # via cffi pycurl==7.45.3 # via binderhub -pydantic==2.7.4 +pydantic==2.8.2 # via jupyterhub -pydantic-core==2.18.4 +pydantic-core==2.20.1 # via pydantic pyjwt==2.8.0 # via binderhub @@ -138,7 +138,7 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rpds-py==0.18.1 +rpds-py==0.19.0 # via # jsonschema # referencing From 0b1cfc617ba3dc5c9f8a17d1bdb25d41a11dfd12 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Jul 2024 13:04:46 +0200 Subject: [PATCH 171/200] Add google-cloud-logging to image --- images/binderhub-service/requirements.in | 10 ++++- images/binderhub-service/requirements.txt | 53 ++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/images/binderhub-service/requirements.in b/images/binderhub-service/requirements.in index bd3d87828..6431bda18 100644 --- a/images/binderhub-service/requirements.in +++ b/images/binderhub-service/requirements.in @@ -2,7 +2,13 @@ # To update requirements.txt, use the "Run workflow" button at # https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -# needed by binderhub.config.py +binderhub[pycurl] @ git+https://github.com/jupyterhub/binderhub@main + +# ruamel-yaml is required by this chart's binderhub_config.py ruamel-yaml -binderhub[pycurl] @ git+https://github.com/jupyterhub/binderhub@main +# google-cloud-logging is an optional dependency to help log BinderHub launch +# events to a Google Cloud Logging as done by mybinder.org deployments here: +# https://github.com/jupyterhub/mybinder.org-deploy/blob/e47021fe/mybinder/values.yaml#L193-L216. +# +google-cloud-logging==3.* diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index e4090547e..fa6dcd61d 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -38,10 +38,45 @@ escapism==1.0.1 # via binderhub fqdn==1.5.1 # via jsonschema +google-api-core[grpc]==2.19.1 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-core + # google-cloud-logging google-auth==2.32.0 - # via kubernetes + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-core + # google-cloud-logging + # kubernetes +google-cloud-appengine-logging==1.4.4 + # via google-cloud-logging +google-cloud-audit-log==0.2.5 + # via google-cloud-logging +google-cloud-core==2.4.1 + # via google-cloud-logging +google-cloud-logging==3.10.0 + # via -r requirements.in +googleapis-common-protos[grpc]==1.63.2 + # via + # google-api-core + # google-cloud-audit-log + # grpc-google-iam-v1 + # grpcio-status greenlet==3.0.3 # via sqlalchemy +grpc-google-iam-v1==0.13.1 + # via google-cloud-logging +grpcio==1.65.0 + # via + # google-api-core + # googleapis-common-protos + # grpc-google-iam-v1 + # grpcio-status +grpcio-status==1.62.2 + # via google-api-core idna==3.7 # via # jsonschema @@ -86,6 +121,21 @@ prometheus-client==0.20.0 # via # binderhub # jupyterhub +proto-plus==1.24.0 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-logging +protobuf==4.25.3 + # via + # google-api-core + # google-cloud-appengine-logging + # google-cloud-audit-log + # google-cloud-logging + # googleapis-common-protos + # grpc-google-iam-v1 + # grpcio-status + # proto-plus pyasn1==0.6.0 # via # pyasn1-modules @@ -125,6 +175,7 @@ referencing==0.35.1 requests==2.32.3 # via # docker + # google-api-core # jupyterhub # kubernetes # requests-oauthlib From 38001b0b514f460bf2229946646c2a9e8f61c6af Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Jul 2024 13:12:34 +0200 Subject: [PATCH 172/200] ci: trigger tests when image is refreezed by automation --- .github/workflows/test-chart.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index d36237d5e..f582253a6 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -19,7 +19,6 @@ on: branches-ignore: - "dependabot/**" - "pre-commit-ci-update-config" - - "update-*" workflow_dispatch: jobs: From 42e251ffb2b782b3d7ef4340037ee3a72ce2b86b Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Jul 2024 13:26:07 +0200 Subject: [PATCH 173/200] Bump pip-tools to v7 used by ci/refreeze script updating requirements.txt files The changes in flags are to respect deprecation notices in https://github.com/jazzband/pip-tools?tab=readme-ov-file#deprecations. --- ci/refreeze | 2 +- images/binderhub-service/requirements.txt | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/ci/refreeze b/ci/refreeze index a3142cb5b..1cf62524f 100755 --- a/ci/refreeze +++ b/ci/refreeze @@ -13,4 +13,4 @@ docker run \ --workdir=/io \ --user=root \ python:3.11-bullseye \ - sh -c 'pip install pip-tools==6.* && pip-compile --resolver=backtracking --upgrade' + sh -c 'pip install pip-tools==7.* && pip-compile --allow-unsafe --strip-extras --upgrade' diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index fa6dcd61d..73e151b20 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -15,9 +15,7 @@ attrs==23.2.0 # jsonschema # referencing binderhub @ git+https://github.com/jupyterhub/binderhub@main - # via - # -r requirements.in - # binderhub + # via -r requirements.in cachetools==5.3.3 # via google-auth certifi==2024.7.4 @@ -38,9 +36,8 @@ escapism==1.0.1 # via binderhub fqdn==1.5.1 # via jsonschema -google-api-core[grpc]==2.19.1 +google-api-core==2.19.1 # via - # google-api-core # google-cloud-appengine-logging # google-cloud-core # google-cloud-logging @@ -59,7 +56,7 @@ google-cloud-core==2.4.1 # via google-cloud-logging google-cloud-logging==3.10.0 # via -r requirements.in -googleapis-common-protos[grpc]==1.63.2 +googleapis-common-protos==1.63.2 # via # google-api-core # google-cloud-audit-log @@ -90,7 +87,7 @@ jinja2==3.1.4 # jupyterhub jsonpointer==3.0.0 # via jsonschema -jsonschema[format-nongpl]==4.23.0 +jsonschema==4.23.0 # via # binderhub # jupyter-events From 8c51cf53a6f5c89f0682bb96edc4c9ce92eca837 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 11 Jul 2024 16:39:58 +0200 Subject: [PATCH 174/200] Cleanup remnant comment --- binderhub-service/values.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index b82c1672f..f559f3029 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -116,7 +116,6 @@ ingress: dockerApi: image: repository: docker.io/library/docker - # Temporarily pinned, until https://github.com/2i2c-org/infrastructure/issues/3588 is fixed tag: "27.0.3-dind" # source: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] From ec0782fb1eab8c9aa5d1de1d35a9e562bdea4e35 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 12 Jul 2024 10:11:18 +0200 Subject: [PATCH 175/200] Allow nameOverride and fullnameOverride to be undefined --- binderhub-service/values.schema.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 46eaedb3b..24856f936 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -16,8 +16,6 @@ type: object additionalProperties: false required: # General configuration - - nameOverride - - fullnameOverride - global # Resources for the BinderHub created build pods - buildPodsRegistryCredentials From baac3d7a1c1454c8c954dbe92e5e0d1068352e1f Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 12 Jul 2024 11:02:26 +0200 Subject: [PATCH 176/200] Add config extraCredentials.googleServiceAccountKey --- binderhub-service/templates/deployment.yaml | 4 ++++ binderhub-service/templates/secret.yaml | 5 +++++ binderhub-service/values.schema.yaml | 6 ++++++ binderhub-service/values.yaml | 2 ++ tools/templates/lint-and-validate-values.yaml | 14 ++++++++++++++ 5 files changed, 31 insertions(+) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index ae2904ca5..3a8e7132d 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -53,6 +53,10 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace + {{- if .Values.extraCredentials.googleServiceAccountKey }} + - name: GOOGLE_APPLICATION_CREDENTIALS + value: /etc/binderhub/mounted-secret/gcp-sa-key.json + {{- end }} {{- with .Values.extraEnv }} {{- tpl (. | toYaml) $ | nindent 12 }} {{- end }} diff --git a/binderhub-service/templates/secret.yaml b/binderhub-service/templates/secret.yaml index eca525c1a..cbe4d3182 100644 --- a/binderhub-service/templates/secret.yaml +++ b/binderhub-service/templates/secret.yaml @@ -18,6 +18,11 @@ stringData: chart-config.yaml: | {{- pick .Values "config" "extraConfig" | toYaml | nindent 4 }} + {{- with .Values.extraCredentials.googleServiceAccountKey }} + gcp-sa-key.json: | + {{- . | nindent 4 }} + {{- end }} + {{- /* Glob files to allow them to be mounted by the binderhub pod */}} {{- /* key=filename: value=content */}} {{- (.Files.Glob "mounted-files/*").AsConfig | nindent 2 }} diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 24856f936..310fb2790 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -83,6 +83,12 @@ properties: patternProperties: ".*": type: [string, "null"] + extraCredentials: + type: object + additionalProperties: false + properties: + googleServiceAccountKey: + type: string extraEnv: type: array diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index f559f3029..d3f7b5071 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -51,6 +51,8 @@ extraConfig: namespace = os.environ["NAMESPACE"] c.KubernetesBuildExecutor.docker_host = f"/var/run/{ namespace }-{ helm_release_name }/docker-api/docker-api.sock" +extraCredentials: + googleServiceAccountKey: "" extraEnv: [] replicas: 1 image: diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml index d0b11bf9f..02fde3337 100644 --- a/tools/templates/lint-and-validate-values.yaml +++ b/tools/templates/lint-and-validate-values.yaml @@ -21,6 +21,20 @@ buildPodsRegistryCredentials: image: repository: quay.io/2i2c/binderhub-service tag: "set-by-chartpress" +extraCredentials: + googleServiceAccountKey: | + { + "type": "service_account", + "project_id": "PROJECT_ID", + "private_key_id": "KEY_ID", + "private_key": "-----BEGIN PRIVATE KEY-----\nPRIVATE_KEY\n-----END PRIVATE KEY-----\n", + "client_email": "SERVICE_ACCOUNT_EMAIL", + "client_id": "CLIENT_ID", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/SERVICE_ACCOUNT_EMAIL" + } extraEnv: - name: HELM_RELEASE_NAME value: "{{ .Release.Name }}" From 470611b690c0e463f6ac831a2cf0044381b76582 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 12 Jul 2024 11:21:49 +0200 Subject: [PATCH 177/200] Require extraCredentials in schema If we don't require this, it could get unset, and if it becomes unset we would error with a harder to understand error message than if we would require this via the schema. --- binderhub-service/values.schema.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 310fb2790..1599be6ee 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -21,6 +21,7 @@ required: - buildPodsRegistryCredentials # Deployment resource - image + - extraCredentials # Other resources - rbac - serviceAccount From f6aec6ffd038ca4b5dca41112b2822937703b814 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 12 Jul 2024 14:10:54 +0200 Subject: [PATCH 178/200] Add config `custom` This mimics z2jh, and is [documented there](https://z2jh.jupyter.org/en/stable/resources/reference.html#custom): >Additional values to pass to the Hub. JupyterHub will not itself look at these, but you can read values in your own custom config via hub.extraConfig. For example: > >```yaml >custom: > myHost: "https://example.horse" >hub: > extraConfig: > myConfig.py: | > c.MyAuthenticator.host = get_config("custom.myHost") >``` --- binderhub-service/values.schema.yaml | 4 ++++ binderhub-service/values.yaml | 1 + tools/templates/lint-and-validate-values.yaml | 1 + 3 files changed, 6 insertions(+) diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 1599be6ee..9e3b18bde 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -17,6 +17,7 @@ additionalProperties: false required: # General configuration - global + - custom # Resources for the BinderHub created build pods - buildPodsRegistryCredentials # Deployment resource @@ -46,6 +47,9 @@ properties: global: type: object additionalProperties: true + custom: + type: object + additionalProperties: true # Resources for the BinderHub created build pods # --------------------------------------------------------------------------- diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index d3f7b5071..767eb3bcf 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -4,6 +4,7 @@ nameOverride: "" fullnameOverride: "" global: {} +custom: {} # Resources for the BinderHub created build pods # ----------------------------------------------------------------------------- diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml index 02fde3337..7143819e8 100644 --- a/tools/templates/lint-and-validate-values.yaml +++ b/tools/templates/lint-and-validate-values.yaml @@ -4,6 +4,7 @@ nameOverride: "" fullnameOverride: "" global: {} +custom: {} # Resources for the BinderHub created build pods # ----------------------------------------------------------------------------- From 00ba9986698c755a37775e2786f6d1f4457dcef2 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 12 Jul 2024 14:17:58 +0200 Subject: [PATCH 179/200] Fix adding of `custom` config --- binderhub-service/templates/secret.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/templates/secret.yaml b/binderhub-service/templates/secret.yaml index cbe4d3182..123d7ae7b 100644 --- a/binderhub-service/templates/secret.yaml +++ b/binderhub-service/templates/secret.yaml @@ -16,7 +16,7 @@ stringData: file. */}} chart-config.yaml: | - {{- pick .Values "config" "extraConfig" | toYaml | nindent 4 }} + {{- pick .Values "custom" "config" "extraConfig" | toYaml | nindent 4 }} {{- with .Values.extraCredentials.googleServiceAccountKey }} gcp-sa-key.json: | From 07ae36e109bef815d7b4e8603d8c335f3b476cdb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:35:11 +0000 Subject: [PATCH 180/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.16.0 → v3.17.0](https://github.com/asottile/pyupgrade/compare/v3.16.0...v3.17.0) - [github.com/psf/black: 24.4.2 → 24.8.0](https://github.com/psf/black/compare/24.4.2...24.8.0) - [github.com/PyCQA/flake8: 7.1.0 → 7.1.1](https://github.com/PyCQA/flake8/compare/7.1.0...7.1.1) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a7e88926..693b20dd9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade args: @@ -27,7 +27,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black args: @@ -51,7 +51,7 @@ repos: # Linting: Python code (see the file .flake8) - repo: https://github.com/PyCQA/flake8 - rev: "7.1.0" + rev: "7.1.1" hooks: - id: flake8 # Ignore style and complexity From 31c501dfd81bf29eaae5be9362c7c64a79541b0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 05:37:05 +0000 Subject: [PATCH 181/200] Bump peter-evans/create-pull-request from 6 to 7 Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6 to 7. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v6...v7) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/watch-dependencies.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/watch-dependencies.yaml b/.github/workflows/watch-dependencies.yaml index 8531d681f..928d5afb7 100644 --- a/.github/workflows/watch-dependencies.yaml +++ b/.github/workflows/watch-dependencies.yaml @@ -76,7 +76,7 @@ jobs: run: git --no-pager diff --color=always # ref: https://github.com/peter-evans/create-pull-request - - uses: peter-evans/create-pull-request@v6 + - uses: peter-evans/create-pull-request@v7 if: github.event_name != 'pull_request' with: branch: update-image-dependencies @@ -104,7 +104,7 @@ jobs: run: git --no-pager diff --color=always # ref: https://github.com/peter-evans/create-pull-request - - uses: peter-evans/create-pull-request@v6 + - uses: peter-evans/create-pull-request@v7 if: github.event_name != 'pull_request' with: branch: update-image-requirements From e1ac64bcf523ecc4cfb8065f8e8185c85e684628 Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Fri, 1 Nov 2024 05:02:45 +0000 Subject: [PATCH 182/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 94 +++++++++++++---------- 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 73e151b20..1f663049e 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,81 +4,87 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -alembic==1.13.2 +alembic==1.13.3 # via jupyterhub annotated-types==0.7.0 # via pydantic arrow==1.3.0 # via isoduration -attrs==23.2.0 +attrs==24.2.0 # via # jsonschema # referencing binderhub @ git+https://github.com/jupyterhub/binderhub@main # via -r requirements.in -cachetools==5.3.3 +cachetools==5.5.0 # via google-auth -certifi==2024.7.4 +certifi==2024.8.30 # via # kubernetes # requests -certipy==0.1.3 +certipy==0.2.1 # via jupyterhub -cffi==1.16.0 +cffi==1.17.1 # via cryptography -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via requests -cryptography==42.0.8 - # via pyopenssl +cryptography==43.0.3 + # via certipy +deprecated==1.2.14 + # via opentelemetry-api docker==7.1.0 # via binderhub +durationpy==0.9 + # via kubernetes escapism==1.0.1 # via binderhub fqdn==1.5.1 # via jsonschema -google-api-core==2.19.1 +google-api-core==2.22.0 # via # google-cloud-appengine-logging # google-cloud-core # google-cloud-logging -google-auth==2.32.0 +google-auth==2.35.0 # via # google-api-core # google-cloud-appengine-logging # google-cloud-core # google-cloud-logging # kubernetes -google-cloud-appengine-logging==1.4.4 +google-cloud-appengine-logging==1.5.0 # via google-cloud-logging -google-cloud-audit-log==0.2.5 +google-cloud-audit-log==0.3.0 # via google-cloud-logging google-cloud-core==2.4.1 # via google-cloud-logging -google-cloud-logging==3.10.0 +google-cloud-logging==3.11.3 # via -r requirements.in -googleapis-common-protos==1.63.2 +googleapis-common-protos==1.65.0 # via # google-api-core # google-cloud-audit-log # grpc-google-iam-v1 # grpcio-status -greenlet==3.0.3 +greenlet==3.1.1 # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-logging -grpcio==1.65.0 +grpcio==1.67.1 # via # google-api-core # googleapis-common-protos # grpc-google-iam-v1 # grpcio-status -grpcio-status==1.62.2 +grpcio-status==1.67.1 # via google-api-core -idna==3.7 +idna==3.10 # via # jsonschema # jupyterhub # requests +importlib-metadata==8.4.0 + # via opentelemetry-api isoduration==20.11.0 # via jsonschema jinja2==3.1.4 @@ -91,17 +97,17 @@ jsonschema==4.23.0 # via # binderhub # jupyter-events -jsonschema-specifications==2023.12.1 +jsonschema-specifications==2024.10.1 # via jsonschema jupyter-events==0.10.0 # via jupyterhub -jupyterhub==5.0.0 +jupyterhub==5.2.1 # via binderhub -kubernetes==30.1.0 +kubernetes==31.0.0 # via binderhub -mako==1.3.5 +mako==1.3.6 # via alembic -markupsafe==2.1.5 +markupsafe==3.0.2 # via # jinja2 # mako @@ -110,20 +116,22 @@ oauthlib==3.2.2 # jupyterhub # kubernetes # requests-oauthlib +opentelemetry-api==1.27.0 + # via google-cloud-logging packaging==24.1 # via jupyterhub -pamela==1.1.0 +pamela==1.2.0 # via jupyterhub -prometheus-client==0.20.0 +prometheus-client==0.21.0 # via # binderhub # jupyterhub -proto-plus==1.24.0 +proto-plus==1.25.0 # via # google-api-core # google-cloud-appengine-logging # google-cloud-logging -protobuf==4.25.3 +protobuf==5.28.3 # via # google-api-core # google-cloud-appengine-logging @@ -133,24 +141,22 @@ protobuf==4.25.3 # grpc-google-iam-v1 # grpcio-status # proto-plus -pyasn1==0.6.0 +pyasn1==0.6.1 # via # pyasn1-modules # rsa -pyasn1-modules==0.4.0 +pyasn1-modules==0.4.1 # via google-auth pycparser==2.22 # via cffi pycurl==7.45.3 # via binderhub -pydantic==2.8.2 +pydantic==2.9.2 # via jupyterhub -pydantic-core==2.20.1 +pydantic-core==2.23.4 # via pydantic -pyjwt==2.8.0 +pyjwt==2.9.0 # via binderhub -pyopenssl==24.1.0 - # via certipy python-dateutil==2.9.0.post0 # via # arrow @@ -160,7 +166,7 @@ python-json-logger==2.0.7 # via # binderhub # jupyter-events -pyyaml==6.0.1 +pyyaml==6.0.2 # via # jupyter-events # kubernetes @@ -186,7 +192,7 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rpds-py==0.19.0 +rpds-py==0.20.1 # via # jsonschema # referencing @@ -194,14 +200,14 @@ rsa==4.9 # via google-auth ruamel-yaml==0.18.6 # via -r requirements.in -ruamel-yaml-clib==0.2.8 +ruamel-yaml-clib==0.2.12 # via ruamel-yaml six==1.16.0 # via # kubernetes # python-dateutil # rfc3339-validator -sqlalchemy==2.0.31 +sqlalchemy==2.0.36 # via # alembic # jupyterhub @@ -214,7 +220,7 @@ traitlets==5.14.3 # binderhub # jupyter-events # jupyterhub -types-python-dateutil==2.9.0.20240316 +types-python-dateutil==2.9.0.20241003 # via arrow typing-extensions==4.12.2 # via @@ -224,12 +230,16 @@ typing-extensions==4.12.2 # sqlalchemy uri-template==1.3.0 # via jsonschema -urllib3==2.2.2 +urllib3==2.2.3 # via # docker # kubernetes # requests -webcolors==24.6.0 +webcolors==24.8.0 # via jsonschema websocket-client==1.8.0 # via kubernetes +wrapt==1.16.0 + # via deprecated +zipp==3.20.2 + # via importlib-metadata From 756b847b12fb34ec50a6aa6faa788ef63180c6ad Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 11 Dec 2024 15:24:51 +0100 Subject: [PATCH 183/200] Offboarding myself --- binderhub-service/Chart.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/binderhub-service/Chart.yaml b/binderhub-service/Chart.yaml index 23c59ecbf..d96ca6d88 100644 --- a/binderhub-service/Chart.yaml +++ b/binderhub-service/Chart.yaml @@ -12,6 +12,3 @@ home: https://2i2c.org/binderhub-service sources: [https://github.com/2i2c-org/binderhub-service] icon: https://binderhub.readthedocs.io/en/latest/_static/logo.png kubeVersion: ">=1.27.0-0" -maintainers: - - name: Erik Sundell - email: erik@sundellopensource.se From 06b37ab95a1c18e8fb5e749e654fd0c6a8ba7d61 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 15 Jan 2025 15:31:14 -0800 Subject: [PATCH 184/200] Update docs to account for newest fancy-profiles --- .../tutorials/connect-with-jupyterhub-fancy-profiles.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md index b231f0f2c..03e4cb892 100644 --- a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md +++ b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md @@ -39,11 +39,12 @@ The following steps describe how to connect your `binderhub-service` [](installa ```yaml singleuser: profileList: - - display_name: "Only Profile Available, this info is not shown in the UI" - slug: only-choice + - display_name: "Choose Your Environment" profile_options: image: display_name: Image + dynamic_image_building: + enabled: True unlisted_choice: enabled: True display_name: "Custom image" From 5aa24360fac8e53a41f39f0531261005bac2bb8c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 23:31:56 +0000 Subject: [PATCH 185/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md index 03e4cb892..765f326b5 100644 --- a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md +++ b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md @@ -44,7 +44,7 @@ The following steps describe how to connect your `binderhub-service` [](installa image: display_name: Image dynamic_image_building: - enabled: True + enabled: True unlisted_choice: enabled: True display_name: "Custom image" From b18bf9101736f6fc89259a71414698ecdb28ab1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 20:58:16 +0000 Subject: [PATCH 186/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.17.0 → v3.19.1](https://github.com/asottile/pyupgrade/compare/v3.17.0...v3.19.1) - [github.com/psf/black: 24.8.0 → 25.1.0](https://github.com/psf/black/compare/24.8.0...25.1.0) - [github.com/pycqa/isort: 5.13.2 → 6.0.0](https://github.com/pycqa/isort/compare/5.13.2...6.0.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 693b20dd9..a96edc7f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.19.1 hooks: - id: pyupgrade args: @@ -27,7 +27,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 24.8.0 + rev: 25.1.0 hooks: - id: black args: @@ -36,7 +36,7 @@ repos: # Autoformat: Python code - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 6.0.0 hooks: - id: isort args: From cde56f6a73d005a9c6e2175d9ee0bd2d25862571 Mon Sep 17 00:00:00 2001 From: yuvipanda <30430+yuvipanda@users.noreply.github.com> Date: Sun, 16 Feb 2025 17:42:40 +0000 Subject: [PATCH 187/200] Update library/docker version from 27.0.3-dind to 27.5.1-dind --- binderhub-service/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binderhub-service/values.yaml b/binderhub-service/values.yaml index 767eb3bcf..bd688b90b 100644 --- a/binderhub-service/values.yaml +++ b/binderhub-service/values.yaml @@ -119,7 +119,7 @@ ingress: dockerApi: image: repository: docker.io/library/docker - tag: "27.0.3-dind" # source: https://hub.docker.com/_/docker/tags + tag: "27.5.1-dind" # source: https://hub.docker.com/_/docker/tags pullPolicy: "" pullSecrets: [] resources: {} From d31f4c25a3c3338e0854e17557181c3575a69464 Mon Sep 17 00:00:00 2001 From: yuvipanda <30430+yuvipanda@users.noreply.github.com> Date: Sun, 16 Feb 2025 17:42:57 +0000 Subject: [PATCH 188/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 87 ++++++++++++----------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 1f663049e..23708d0a7 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -4,21 +4,21 @@ # # Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml # -alembic==1.13.3 +alembic==1.14.1 # via jupyterhub annotated-types==0.7.0 # via pydantic arrow==1.3.0 # via isoduration -attrs==24.2.0 +attrs==25.1.0 # via # jsonschema # referencing binderhub @ git+https://github.com/jupyterhub/binderhub@main # via -r requirements.in -cachetools==5.5.0 +cachetools==5.5.1 # via google-auth -certifi==2024.8.30 +certifi==2025.1.31 # via # kubernetes # requests @@ -26,11 +26,11 @@ certipy==0.2.1 # via jupyterhub cffi==1.17.1 # via cryptography -charset-normalizer==3.4.0 +charset-normalizer==3.4.1 # via requests -cryptography==43.0.3 +cryptography==44.0.1 # via certipy -deprecated==1.2.14 +deprecated==1.2.18 # via opentelemetry-api docker==7.1.0 # via binderhub @@ -40,27 +40,27 @@ escapism==1.0.1 # via binderhub fqdn==1.5.1 # via jsonschema -google-api-core==2.22.0 +google-api-core==2.24.1 # via # google-cloud-appengine-logging # google-cloud-core # google-cloud-logging -google-auth==2.35.0 +google-auth==2.38.0 # via # google-api-core # google-cloud-appengine-logging # google-cloud-core # google-cloud-logging # kubernetes -google-cloud-appengine-logging==1.5.0 +google-cloud-appengine-logging==1.6.0 # via google-cloud-logging google-cloud-audit-log==0.3.0 # via google-cloud-logging google-cloud-core==2.4.1 # via google-cloud-logging -google-cloud-logging==3.11.3 +google-cloud-logging==3.11.4 # via -r requirements.in -googleapis-common-protos==1.65.0 +googleapis-common-protos==1.67.0 # via # google-api-core # google-cloud-audit-log @@ -68,26 +68,26 @@ googleapis-common-protos==1.65.0 # grpcio-status greenlet==3.1.1 # via sqlalchemy -grpc-google-iam-v1==0.13.1 +grpc-google-iam-v1==0.14.0 # via google-cloud-logging -grpcio==1.67.1 +grpcio==1.70.0 # via # google-api-core # googleapis-common-protos # grpc-google-iam-v1 # grpcio-status -grpcio-status==1.67.1 +grpcio-status==1.70.0 # via google-api-core idna==3.10 # via # jsonschema # jupyterhub # requests -importlib-metadata==8.4.0 +importlib-metadata==8.5.0 # via opentelemetry-api isoduration==20.11.0 # via jsonschema -jinja2==3.1.4 +jinja2==3.1.5 # via # binderhub # jupyterhub @@ -99,13 +99,13 @@ jsonschema==4.23.0 # jupyter-events jsonschema-specifications==2024.10.1 # via jsonschema -jupyter-events==0.10.0 +jupyter-events==0.12.0 # via jupyterhub jupyterhub==5.2.1 # via binderhub -kubernetes==31.0.0 +kubernetes==32.0.0 # via binderhub -mako==1.3.6 +mako==1.3.9 # via alembic markupsafe==3.0.2 # via @@ -116,22 +116,24 @@ oauthlib==3.2.2 # jupyterhub # kubernetes # requests-oauthlib -opentelemetry-api==1.27.0 +opentelemetry-api==1.30.0 # via google-cloud-logging -packaging==24.1 - # via jupyterhub +packaging==24.2 + # via + # jupyter-events + # jupyterhub pamela==1.2.0 # via jupyterhub -prometheus-client==0.21.0 +prometheus-client==0.21.1 # via # binderhub # jupyterhub -proto-plus==1.25.0 +proto-plus==1.26.0 # via # google-api-core # google-cloud-appengine-logging # google-cloud-logging -protobuf==5.28.3 +protobuf==5.29.3 # via # google-api-core # google-cloud-appengine-logging @@ -149,20 +151,20 @@ pyasn1-modules==0.4.1 # via google-auth pycparser==2.22 # via cffi -pycurl==7.45.3 +pycurl==7.45.4 # via binderhub -pydantic==2.9.2 +pydantic==2.10.6 # via jupyterhub -pydantic-core==2.23.4 +pydantic-core==2.27.2 # via pydantic -pyjwt==2.9.0 +pyjwt==2.10.1 # via binderhub python-dateutil==2.9.0.post0 # via # arrow # jupyterhub # kubernetes -python-json-logger==2.0.7 +python-json-logger==3.2.1 # via # binderhub # jupyter-events @@ -170,7 +172,7 @@ pyyaml==6.0.2 # via # jupyter-events # kubernetes -referencing==0.35.1 +referencing==0.36.2 # via # jsonschema # jsonschema-specifications @@ -192,26 +194,26 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rpds-py==0.20.1 +rpds-py==0.22.3 # via # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.18.6 +ruamel-yaml==0.18.10 # via -r requirements.in ruamel-yaml-clib==0.2.12 # via ruamel-yaml -six==1.16.0 +six==1.17.0 # via # kubernetes # python-dateutil # rfc3339-validator -sqlalchemy==2.0.36 +sqlalchemy==2.0.38 # via # alembic # jupyterhub -tornado==6.4.1 +tornado==6.4.2 # via # binderhub # jupyterhub @@ -220,26 +222,27 @@ traitlets==5.14.3 # binderhub # jupyter-events # jupyterhub -types-python-dateutil==2.9.0.20241003 +types-python-dateutil==2.9.0.20241206 # via arrow typing-extensions==4.12.2 # via # alembic # pydantic # pydantic-core + # referencing # sqlalchemy uri-template==1.3.0 # via jsonschema -urllib3==2.2.3 +urllib3==2.3.0 # via # docker # kubernetes # requests -webcolors==24.8.0 +webcolors==24.11.1 # via jsonschema websocket-client==1.8.0 # via kubernetes -wrapt==1.16.0 +wrapt==1.17.2 # via deprecated -zipp==3.20.2 +zipp==3.21.0 # via importlib-metadata From 41fa264c108d4c1a605d085ad55c585beb39f060 Mon Sep 17 00:00:00 2001 From: consideRatio <3837114+consideRatio@users.noreply.github.com> Date: Sat, 1 Mar 2025 05:02:34 +0000 Subject: [PATCH 189/200] binderhub-service image: refreeze requirements.txt --- images/binderhub-service/requirements.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/images/binderhub-service/requirements.txt b/images/binderhub-service/requirements.txt index 23708d0a7..a6b3c6ec0 100644 --- a/images/binderhub-service/requirements.txt +++ b/images/binderhub-service/requirements.txt @@ -16,7 +16,7 @@ attrs==25.1.0 # referencing binderhub @ git+https://github.com/jupyterhub/binderhub@main # via -r requirements.in -cachetools==5.5.1 +cachetools==5.5.2 # via google-auth certifi==2025.1.31 # via @@ -56,11 +56,11 @@ google-cloud-appengine-logging==1.6.0 # via google-cloud-logging google-cloud-audit-log==0.3.0 # via google-cloud-logging -google-cloud-core==2.4.1 +google-cloud-core==2.4.2 # via google-cloud-logging google-cloud-logging==3.11.4 # via -r requirements.in -googleapis-common-protos==1.67.0 +googleapis-common-protos==1.68.0 # via # google-api-core # google-cloud-audit-log @@ -103,7 +103,7 @@ jupyter-events==0.12.0 # via jupyterhub jupyterhub==5.2.1 # via binderhub -kubernetes==32.0.0 +kubernetes==32.0.1 # via binderhub mako==1.3.9 # via alembic @@ -194,7 +194,7 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rpds-py==0.22.3 +rpds-py==0.23.1 # via # jsonschema # referencing From e5648b8a50364b85392428f8ac79664f3983b705 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:38:19 +0000 Subject: [PATCH 190/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pycqa/isort: 6.0.0 → 6.0.1](https://github.com/pycqa/isort/compare/6.0.0...6.0.1) - [github.com/PyCQA/flake8: 7.1.1 → 7.1.2](https://github.com/PyCQA/flake8/compare/7.1.1...7.1.2) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a96edc7f0..25b1af8e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: # Autoformat: Python code - repo: https://github.com/pycqa/isort - rev: 6.0.0 + rev: 6.0.1 hooks: - id: isort args: @@ -51,7 +51,7 @@ repos: # Linting: Python code (see the file .flake8) - repo: https://github.com/PyCQA/flake8 - rev: "7.1.1" + rev: "7.1.2" hooks: - id: flake8 # Ignore style and complexity From 04b775f61d2f433f6130f2e1e51cc5dfc6590b82 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Thu, 6 Mar 2025 18:18:53 -0800 Subject: [PATCH 191/200] Allow setting extraVolumes and extraVolumeMounts --- binderhub-service/templates/deployment.yaml | 6 ++++++ binderhub-service/values.schema.yaml | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/binderhub-service/templates/deployment.yaml b/binderhub-service/templates/deployment.yaml index 3a8e7132d..895e02b6c 100644 --- a/binderhub-service/templates/deployment.yaml +++ b/binderhub-service/templates/deployment.yaml @@ -25,6 +25,9 @@ spec: - name: secret secret: secretName: {{ include "binderhub-service.binderhub.fullname" . }} + {{- with .Values.extraVolumes }} + {{- tpl (. | toYaml) $ | nindent 8 }} + {{- end }} containers: - name: binderhub image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" @@ -38,6 +41,9 @@ spec: - name: secret mountPath: /etc/binderhub/mounted-secret/ readOnly: true + {{- with .Values.extraVolumeMounts }} + {{- tpl (. | toYaml) $ | nindent 12 }} + {{- end }} env: - name: PUSH_SECRET_NAME value: {{ include "binderhub-service.build-pods-docker-config.fullname" . }} diff --git a/binderhub-service/values.schema.yaml b/binderhub-service/values.schema.yaml index 9e3b18bde..88ccd7ea2 100644 --- a/binderhub-service/values.schema.yaml +++ b/binderhub-service/values.schema.yaml @@ -96,6 +96,10 @@ properties: type: string extraEnv: type: array + extraVolumes: + type: array + extraVolumeMounts: + type: array replicas: type: integer From 705bbc08c653b7345ecb562ea94fba1f67c90af1 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Thu, 6 Mar 2025 19:31:13 -0800 Subject: [PATCH 192/200] Preparing to merge --- helm-chart/.gitignore | 134 ---- helm-chart/LICENSE | 208 ------ helm-chart/README.md | 57 -- helm-chart/binderhub/.helmignore | 31 - helm-chart/binderhub/Chart.yaml | 34 - .../binderhub/files/binderhub_config.py | 68 -- helm-chart/binderhub/schema.yaml | 672 ------------------ helm-chart/binderhub/templates/NOTES.txt | 204 ------ helm-chart/binderhub/templates/_helpers.tpl | 48 -- .../container-builder/daemonset.yaml | 148 ---- .../binderhub/templates/deployment.yaml | 173 ----- .../binderhub/templates/image-cleaner.yaml | 92 --- helm-chart/binderhub/templates/ingress.yaml | 51 -- helm-chart/binderhub/templates/pdb.yaml | 29 - helm-chart/binderhub/templates/rbac.yaml | 91 --- helm-chart/binderhub/templates/secret.yaml | 42 -- helm-chart/binderhub/templates/service.yaml | 24 - helm-chart/binderhub/values.yaml | 370 ---------- helm-chart/chartpress.yaml | 40 -- helm-chart/images/binderhub/Dockerfile | 69 -- helm-chart/images/binderhub/README.md | 11 - helm-chart/images/binderhub/requirements.in | 25 - helm-chart/images/binderhub/requirements.txt | 210 ------ 23 files changed, 2831 deletions(-) delete mode 100644 helm-chart/.gitignore delete mode 100644 helm-chart/LICENSE delete mode 100644 helm-chart/README.md delete mode 100644 helm-chart/binderhub/.helmignore delete mode 100644 helm-chart/binderhub/Chart.yaml delete mode 100644 helm-chart/binderhub/files/binderhub_config.py delete mode 100644 helm-chart/binderhub/schema.yaml delete mode 100644 helm-chart/binderhub/templates/NOTES.txt delete mode 100644 helm-chart/binderhub/templates/_helpers.tpl delete mode 100644 helm-chart/binderhub/templates/container-builder/daemonset.yaml delete mode 100644 helm-chart/binderhub/templates/deployment.yaml delete mode 100644 helm-chart/binderhub/templates/image-cleaner.yaml delete mode 100644 helm-chart/binderhub/templates/ingress.yaml delete mode 100644 helm-chart/binderhub/templates/pdb.yaml delete mode 100644 helm-chart/binderhub/templates/rbac.yaml delete mode 100644 helm-chart/binderhub/templates/secret.yaml delete mode 100644 helm-chart/binderhub/templates/service.yaml delete mode 100644 helm-chart/binderhub/values.yaml delete mode 100644 helm-chart/chartpress.yaml delete mode 100644 helm-chart/images/binderhub/Dockerfile delete mode 100644 helm-chart/images/binderhub/README.md delete mode 100644 helm-chart/images/binderhub/requirements.in delete mode 100644 helm-chart/images/binderhub/requirements.txt diff --git a/helm-chart/.gitignore b/helm-chart/.gitignore deleted file mode 100644 index a25dc44b5..000000000 --- a/helm-chart/.gitignore +++ /dev/null @@ -1,134 +0,0 @@ -binderhub/values.schema.json - -### macOS ### -*.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon -# Thumbnails -._* -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Vim ### -# swap -[._]*.s[a-w][a-z] -[._]s[a-w][a-z] -# session -Session.vim -# temporary -.netrwhist -*~ -# auto-generated tag files -tags - -# GCloud Credentials -data8-travis-creds.json - -#### Python .gitignore from https://github.com/github/gitignore/blob/master/Python.gitignore -#### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# dotenv -.env - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject diff --git a/helm-chart/LICENSE b/helm-chart/LICENSE deleted file mode 100644 index aa6780e7c..000000000 --- a/helm-chart/LICENSE +++ /dev/null @@ -1,208 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2016 Allan Wu - Copyright 2016 Derrick Mar - Copyright 2016 Jeff Gong - Copyright 2016 Peter Veerman - Copyright 2016 Ryan Lovett - Copyright 2016 Sam Lau - Copyright 2016 Yuvi Panda - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/helm-chart/README.md b/helm-chart/README.md deleted file mode 100644 index 46b3f3f4f..000000000 --- a/helm-chart/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# BinderHub Helm Chart - -A [helm][] [chart][] for deploying [BinderHub] instances on [Kubernetes]. - -**[Zero to JupyterHub with Kubernetes]** provides detailed instructions for using this project within a JupyerHub deployment. - -## Overview of [Kubernetes] terminology - -### What is [helm]? - -[helm] is the Kubernetes package manager. [Helm] streamlines installing and managing Kubernetes applications. _Reference: [helm repo]_ - -### What is a [chart]? - -Charts are Helm packages that contain at least two things: - -- A description of the package (`Chart.yaml`) -- One or more **templates**, which contain Kubernetes manifest files - -_Reference: [Kubernetes Introduction to charts]_ - -## Contents of this repository - -### `binderhub` folder - -Fundamental elements of a chart including: - -- `templates` folder -- `Chart.yaml.template` -- `values.yaml` - -### `images` folder - -Docker images for applications including: - -- `binderhub` - -### `chartpress` - -Useful for compiling custom charts. - -## Usage - -In the helm-chart directory: - - chartpress - -to build the docker images and rerender the helm chart. - -[binderhub]: https://binderhub.readthedocs.io/en/latest/ -[jupyterhub]: https://jupyterhub.readthedocs.io/en/latest/ -[kubernetes]: https://kubernetes.io -[helm]: https://helm.sh/ -[helm repo]: https://github.com/kubernetes/helm -[chart]: https://helm.sh/docs/topics/charts/ -[kubernetes introduction to charts]: https://helm.sh/docs/topics/charts/ -[zero to jupyterhub with kubernetes]: https://zero-to-jupyterhub.readthedocs.io/en/latest/ diff --git a/helm-chart/binderhub/.helmignore b/helm-chart/binderhub/.helmignore deleted file mode 100644 index 05f3c9858..000000000 --- a/helm-chart/binderhub/.helmignore +++ /dev/null @@ -1,31 +0,0 @@ -# Anything within the root folder of the Helm chart, where Chart.yaml resides, -# will be embedded into the packaged Helm chart. This is reasonable since only -# when the templates render after the chart has been packaged and distributed, -# will the templates logic evaluate that determines if other files were -# referenced, such as our our files/hub/jupyterhub_config.py. -# -# Here are files that we intentionally ignore to avoid them being packaged, -# because we don't want to reference them from our templates anyhow. -schema.yaml - -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/helm-chart/binderhub/Chart.yaml b/helm-chart/binderhub/Chart.yaml deleted file mode 100644 index c04c1b7d4..000000000 --- a/helm-chart/binderhub/Chart.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Chart.yaml v2 reference: https://helm.sh/docs/topics/charts/#the-chartyaml-file -apiVersion: v2 -name: binderhub -version: 0.0.1-set.by.chartpress -dependencies: - # Source code: https://github.com/jupyterhub/zero-to-jupyterhub-k8s - # Latest version: https://github.com/jupyterhub/zero-to-jupyterhub-k8s/tags - # App changelog: https://github.com/jupyterhub/zero-to-jupyterhub-k8s/tree/HEAD/CHANGELOG.md - # - # Important: Whenever you bump the jupyterhub Helm chart version, also inspect - # the helm-chart/images/binderhub pinned version in requirements.in - # and run "./dependencies freeze --upgrade". - # - - name: jupyterhub - version: "4.0.0" - repository: "https://jupyterhub.github.io/helm-chart" -description: |- - BinderHub is like a JupyterHub that automatically builds environments for the - users based on repo2docker. A BinderHub is by default not configured to - authenticate users or provide storage for them. -keywords: [jupyter, jupyterhub, binderhub] -home: https://binderhub.readthedocs.io/en/latest/ -sources: [https://github.com/jupyterhub/binderhub] -icon: https://jupyterhub.github.io/helm-chart/images/hublogo.svg -kubeVersion: ">=1.28.0-0" -maintainers: - # Since it is a requirement of Artifact Hub to have specific maintainers - # listed, we have added some below, but in practice the entire JupyterHub team - # contributes to the maintenance of this Helm chart. Please go ahead and add - # yourself! - - name: Yuvi - email: yuvipanda@gmail.com - - name: Erik Sundell - email: erik@sundellopensource.se diff --git a/helm-chart/binderhub/files/binderhub_config.py b/helm-chart/binderhub/files/binderhub_config.py deleted file mode 100644 index b86afa903..000000000 --- a/helm-chart/binderhub/files/binderhub_config.py +++ /dev/null @@ -1,68 +0,0 @@ -from functools import lru_cache -from urllib.parse import urlparse - -from ruamel.yaml import YAML - -yaml = YAML() - - -# memoize so we only load config once -@lru_cache -def _load_values(): - """Load configuration from disk - - Memoized to only load once - """ - path = "/etc/binderhub/config/values.yaml" - print(f"Loading {path}") - with open(path) as f: - return yaml.load(f) - - -def get_value(key, default=None): - """ - Find an item in values.yaml of a given name & return it - - get_value("a.b.c") returns values['a']['b']['c'] - """ - # start at the top - value = _load_values() - # resolve path in yaml - for level in key.split("."): - if not isinstance(value, dict): - # a parent is a scalar or null, - # can't resolve full path - return default - if level not in value: - return default - else: - value = value[level] - return value - - -# load custom templates, by default -c.BinderHub.template_path = "/etc/binderhub/templates" - -# load config from values.yaml -for section, sub_cfg in get_value("config", {}).items(): - c[section].update(sub_cfg) - -imageBuilderType = get_value("imageBuilderType") -if imageBuilderType in ["dind", "pink"]: - hostSocketDir = get_value(f"{imageBuilderType}.hostSocketDir") - if hostSocketDir: - socketname = "docker" if imageBuilderType == "dind" else "podman" - c.BinderHub.build_docker_host = f"unix://{hostSocketDir}/{socketname}.sock" - -if c.BinderHub.auth_enabled: - if "hub_url" not in c.BinderHub: - c.BinderHub.hub_url = "" - hub_url = urlparse(c.BinderHub.hub_url) - c.HubOAuth.hub_host = f"{hub_url.scheme}://{hub_url.netloc}" - if "base_url" in c.BinderHub: - c.HubOAuth.base_url = c.BinderHub.base_url - -# load extra config snippets -for key, snippet in sorted((get_value("extraConfig") or {}).items()): - print(f"Loading extra config: {key}") - exec(snippet) diff --git a/helm-chart/binderhub/schema.yaml b/helm-chart/binderhub/schema.yaml deleted file mode 100644 index 2a9587716..000000000 --- a/helm-chart/binderhub/schema.yaml +++ /dev/null @@ -1,672 +0,0 @@ -# This schema (a jsonschema in YAML format) is used to generate -# values.schema.json which is packaged with the Helm chart for client side -# validation by Helm of values before template rendering. -# -# This schema is also used by our documentation system to build the -# configuration reference section based on the description fields. See -# docs/source/conf.py for that logic! -# -# We look to document everything we have default values for in values.yaml, but -# we don't look to enforce the perfect validation logic within this file. -# -# ref: https://json-schema.org/learn/getting-started-step-by-step.html -# -$schema: http://json-schema.org/draft-07/schema# -type: object -additionalProperties: false -required: - - pdb - - replicas - - resources - - rbac - - nodeSelector - - tolerations - - image - - registry - - service - - config - - extraConfig - - jupyterhub - - deployment - - dind - - pink - - imageBuilderType - - imageCleaner - - ingress - - initContainers - - lifecycle - - extraFiles - - extraVolumes - - extraVolumeMounts - - extraEnv - - extraPodSpec - - podAnnotations - - global -properties: - pdb: &pdb-spec - type: object - additionalProperties: false - description: | - Configure a PodDisruptionBudget for this Deployment. - - See [the Kubernetes - documentation](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/) - for more details. - properties: - enabled: - type: boolean - description: | - Decides if a PodDisruptionBudget is created targeting the - Deployment's pods. - maxUnavailable: - type: [integer, "null"] - description: | - The maximum number of pods that can be unavailable during voluntary - disruptions. - minAvailable: - type: [integer, "null"] - description: | - The minimum number of pods required to be available during voluntary - disruptions. - - replicas: - type: integer - description: | - You can have multiple binder pods to share the workload or improve - availability on node failure. - - resources: &resources-spec - type: object - additionalProperties: true - description: | - A k8s native specification of resources, see [the - documentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core). - - rbac: - type: object - additionalProperties: false - required: [enabled] - properties: - enabled: - type: boolean - description: | - Decides if RBAC resources are to be created and referenced by the the - Helm chart's workloads. - - nodeSelector: &nodeSelector-spec - type: object - additionalProperties: true - description: | - An object with key value pairs representing labels. K8s Nodes are required - to have match all these labels for this Pod to scheduled on them. - - ```yaml - disktype: ssd - nodetype: awesome - ``` - - See [the Kubernetes - documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) - for more details. - - tolerations: &tolerations-spec - type: array - description: | - Tolerations allow a pod to be scheduled on nodes with taints. - See the - [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) - for more info. - - image: &image-spec - type: object - additionalProperties: false - required: [name, tag] - properties: - name: - type: string - description: | - The name of the image, without the tag. - tag: - type: string - description: | - The tag of the image to pull. This is the value following `:` in - complete image specifications. - pullPolicy: - enum: [null, "", IfNotPresent, Always, Never] - description: | - Configures the Pod's `spec.imagePullPolicy`. - - See the [Kubernetes - docs](https://kubernetes.io/docs/concepts/containers/images/#updating-images) - for more info. - pullSecrets: - type: array - description: | - A list of references to existing Kubernetes Secrets with credentials - to pull the image. - - registry: - type: object - additionalProperties: false - description: | - TODO - properties: - url: - type: [string, "null"] - description: | - TODO - username: - type: [string, "null"] - description: | - TODO - password: - type: [string, "null"] - description: | - TODO - - service: - type: object - additionalProperties: false - description: | - Configuration of the Kubernetes Service resource exposing the BinderHub - web server. - properties: - type: - enum: [ClusterIP, NodePort, LoadBalancer, ExternalName] - description: | - The Kubernetes ServiceType to be used. - - The default type is `ClusterIP`. - See the [Kubernetes docs](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types) - to learn more about service types. - labels: - type: object - additionalProperties: false - patternProperties: &labels-and-annotations-patternProperties - ".*": - type: string - description: | - Labels to add to the binder Service. - annotations: - type: object - additionalProperties: false - patternProperties: *labels-and-annotations-patternProperties - description: | - Annotations to add to the binder Service. - nodePort: - type: [integer, "null"] - description: | - See [the Kubernetes - documentation](https://kubernetes.io/docs/concepts/services-networking/service/#nodeport) - for more details about NodePorts. - loadBalancerIP: - type: [string, "null"] - description: | - See [the Kubernetes - documentation](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) - for more details about loadBalancerIP. - - config: - type: object - additionalProperties: true - description: | - BinderHub and its components are Python classes that expose its - configuration through - [_traitlets_](https://traitlets.readthedocs.io/en/stable/). With this Helm - chart configuration you can directly configure the Python classes through - _static_ YAML values. To _dynamically_ set values, you need to use - `extraConfig` instead. - - __Example__ - - If you inspect documentation or some `binderhub_config.py` to contain the - following section: - - ```python - c.binderhub.some_config_option = True - ``` - - Then, you would be able to represent it with this configuration like: - - ```yaml - config: - BinderHub: - some_config_option: true - ``` - - ```{admonition} YAML limitations - :class: tip - You can't represent Python `Bytes` or `Set` objects in YAML directly. - ``` - - ```{admonition} Helm value merging - :class: tip - `helm` merges a Helm chart's default values with values passed with the - `--values` or `-f` flag. During merging, lists are replaced while - dictionaries are updated. - ``` - - extraConfig: - type: object - additionalProperties: true - description: | - Arbitrary extra python based configuration that should be in `binderhub_config.py`. - - This is the *escape hatch* - if you want to configure BinderHub to do something specific - that is not present here as an option, you can write the raw Python to do it here. - - extraConfig is a *dict*, so there can be multiple configuration snippets - under different names. - The configuration sections are run in alphabetical order. - - Since this is usually a multi-line string, you want to format it using YAML's - [| operator](https://yaml.org/spec/1.2/spec.html#id2795688). - - For example: - ```yaml - extraConfig: - my_binderhub_config.py: | - c.BinderHub.something = 'something' - ``` - - No validation of this python is performed! If you make a mistake here, it will probably - manifest as either the binder pod going into `Error` or `CrashLoopBackoff` states, or in - some special cases, the binder pod running but... just doing very random things. Be careful! - - extraFiles: - type: object - additionalProperties: false - description: | - A dictionary with extra files to be injected into the binder pod's container - on startup. This can for example be used to inject: configuration - files, custom user interface templates, images, and more. - - See zero-to-jupyterhub's extraFiles documentation for reference. - patternProperties: - ".*": - type: object - additionalProperties: false - required: [mountPath] - oneOf: - - required: [data] - - required: [stringData] - - required: [binaryData] - properties: - mountPath: - type: string - data: - type: object - additionalProperties: true - stringData: - type: string - binaryData: - type: string - mode: - type: number - - jupyterhub: - type: object - additionalProperties: true - description: | - Configuration for the JupyterHub Helm chart that is a dependency of the - BinderHub Helm chart. - required: [hub] - properties: - hub: - type: object - additionalProperties: true - required: [services] - properties: - services: - type: object - additionalProperties: true - required: [binder] - properties: - binder: - type: object - additionalProperties: true - properties: - apiToken: - type: [string, "null"] - description: | - TODO - - deployment: - type: object - additionalProperties: false - properties: - readinessProbe: &probe-spec - type: object - additionalProperties: true - description: | - This config option is exactly like the k8s native specification of a - container probe, except that it also supports an `enabled` boolean - flag. - - See [the k8s - documentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#probe-v1-core) - for more details. - required: [enabled] - properties: - enabled: - type: [boolean] - livenessProbe: *probe-spec - labels: - type: object - additionalProperties: false - patternProperties: *labels-and-annotations-patternProperties - description: | - Extra labels to add to the binder pod. - - See the [Kubernetes docs](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) - to learn more about labels. - - imageBuilderType: - type: string - enum: ["host", "dind", "pink"] - default: "host" - description: | - Selected image builder type - - dind: - type: object - additionalProperties: false - properties: - enabled: - type: boolean - description: | - DEPRECATED: Use `imageBuilderType: dind` - initContainers: &initContainers-spec - type: array - description: | - List of additional initContainers. - - See the [Kubernetes - docs](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) - for more info. - daemonset: - type: object - additionalProperties: false - properties: - image: *image-spec - extraArgs: - type: array - description: | - Extra command line arguments for the Docker daemon - lifecycle: &lifecycle-spec - type: object - additionalProperties: false - description: | - A k8s native specification of lifecycle hooks on the container, see [the - documentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#lifecycle-v1-core). - properties: - postStart: - type: object - additionalProperties: true - preStop: - type: object - additionalProperties: true - extraVolumes: &extraVolumes-spec - type: array - description: | - Additional volumes for the Pod. Use a k8s native syntax. - extraVolumeMounts: &extraVolumeMounts-spec - type: array - description: | - Additional volume mounts for the Container. Use a k8s native syntax. - storageDriver: - type: string - description: | - Docker storage driver - resources: *resources-spec - hostSocketDir: &hostSocketDir-spec - type: string - description: | - Host directory where the container socket will be located - hostLibDir: &hostStorageDir-spec - type: string - description: | - Host directory where the containers storage will be located - hostSocketName: &hostSocketName-spec - type: string - description: | - Name of the container socket file - - pink: - type: object - additionalProperties: false - properties: - initContainers: *initContainers-spec - daemonset: - type: object - additionalProperties: false - properties: - image: *image-spec - extraArgs: - type: array - description: | - Extra command line arguments for the Podman service, e.g. `[--log-level=debug]` - lifecycle: *lifecycle-spec - extraVolumes: *extraVolumes-spec - extraVolumeMounts: *extraVolumeMounts-spec - resources: *resources-spec - hostStorageDir: *hostStorageDir-spec - hostSocketDir: *hostSocketDir-spec - hostSocketName: *hostSocketName-spec - - imageCleaner: - type: object - additionalProperties: false - required: - - enabled - properties: - enabled: - type: boolean - description: | - TODO - image: *image-spec - cordon: - type: boolean - description: | - Whether to cordon the node while cleaning its images. - Disable, e.g. for single-node clusters. - delay: - type: integer - description: | - TODO - imageGCThresholdType: - type: string - description: | - TODO - imageGCThresholdHigh: - type: integer - description: | - TODO - imageGCThresholdLow: - type: integer - description: | - TODO - extraEnv: - type: [object, array] - additionalProperties: true - description: | - see binderhub.deployment.extraEnv - - host: - type: object - additionalProperties: false - required: [dockerSocket, dockerLibDir] - properties: - enabled: - type: boolean - description: | - DEPRECATED: use imageCleaner.enabled if the cleaner shall not used. - dockerSocket: - type: string - description: | - TODO - dockerLibDir: - type: string - description: | - TODO - - ingress: - type: object - additionalProperties: false - required: [enabled] - properties: - enabled: - type: boolean - description: | - Enable the creation of a Kubernetes Ingress referencing incoming - network network traffic to the binder k8s Service. - https: - type: object - additionalProperties: false - description: | - TODO - properties: - enabled: - type: boolean - description: | - TODO - type: - type: string - description: | - TODO - annotations: - type: object - additionalProperties: false - patternProperties: *labels-and-annotations-patternProperties - description: | - Annotations to apply to the Ingress resource. - - See [the Kubernetes - documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) - for more details about annotations. - hosts: - type: array - description: | - List of hosts to route requests to the proxy. - ingressClassName: - type: [string, "null"] - description: | - Maps directly to the Ingress resource's spec.ingressClassName. To - configure this, your k8s cluster must have version 1.18+ or above. - - See [the Kubernetes - documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class) - for more details. - pathSuffix: - type: [string, "null"] - description: | - Suffix added to Ingress's routing path pattern. - - Specify `*` if your ingress matches path by glob pattern. - pathType: - enum: [Prefix, Exact, ImplementationSpecific] - description: | - The path type to use. The default value is 'Prefix'. Only applies on Kubernetes v1.18+. - - See [the Kubernetes documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types) - for more details about path types. - tls: - type: array - description: | - TLS configurations for Ingress. - - See [the Kubernetes - documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls) - for more details about annotations. - - initContainers: *initContainers-spec - lifecycle: *lifecycle-spec - extraVolumes: *extraVolumes-spec - extraVolumeMounts: *extraVolumeMounts-spec - extraEnv: - type: [object, array] - additionalProperties: true - description: | - Environment variables to add to the binder pods. - - String literals with `$(ENV_VAR_NAME)` will be expanded by Kubelet which - is a part of Kubernetes. - - ```yaml - extraEnv: - # basic notation (for literal values only) - MY_ENV_VARS_NAME1: "my env var value 1" - - # explicit notation (the "name" field takes precedence) - BINDER_NAMESPACE: - name: BINDER_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - # implicit notation (the "name" field is implied) - PREFIXED_BINDER_NAMESPACE: - value: "my-prefix-$(BINDER_NAMESPACE)" - SECRET_VALUE: - valueFrom: - secretKeyRef: - name: my-k8s-secret - key: password - ``` - - For more information, see the [Kubernetes EnvVar - specification](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#envvar-v1-core). - extraPodSpec: - type: object - additionalProperties: true - description: | - Arbitrary extra k8s pod specification for the binder pod, as a YAML object. - - Use this to set extra pod fields that aren't supported by the Helm chart, e.g. - - ```yaml - extraPodSpec: - priorityClassName: my-priority-class - ``` - - See [PodSpec](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec) - - podAnnotations: - type: object - additionalProperties: false - patternProperties: *labels-and-annotations-patternProperties - description: | - Annotations to add to the binder Pods. - - global: - type: object - additionalProperties: true - - hpa: - type: object - additionalProperties: true - description: | - This is not a supported configuration yet by this Helm chart, but may be - in the future. This schema entry was added because - jupyterhub/mybinder.org-deploy configured a HorizontalPodAutoscaler - resource here. - - networkPolicy: - type: object - additionalProperties: true - description: | - This is not a supported configuration yet by this Helm chart, but may be - in the future. This schema entry was added because - jupyterhub/mybinder.org-deploy configured a Networkpolicy resource here. - - # Deprecated - cors: - type: object - additionalProperties: true - description: | - DEPRECATED. - -# A placeholder as global values that can be referenced from the same location -# of any chart should be possible to provide, but aren't necessarily provided or -# used. -global: {} diff --git a/helm-chart/binderhub/templates/NOTES.txt b/helm-chart/binderhub/templates/NOTES.txt deleted file mode 100644 index 73da374a0..000000000 --- a/helm-chart/binderhub/templates/NOTES.txt +++ /dev/null @@ -1,204 +0,0 @@ -{{- define "removedConfig" }} - -It looks like you still have some unsupported configuration! - -The binderhub chart has removed special-handling -of many simple configurables in the BinderHub source code. -Instead, the Python traits configuration is exposed directly. - -See https://binderhub.readthedocs.io/en/latest/reference/ref-index.html -for what can be configured. - -In general, it works like: - -config: - ClassName: - trait_name: value - -e.g. to set config on BinderHub, use: - -config: - BinderHub: - use_registry: false - -or on GitHubRepoProvider: - -config: - GitHubRepoProvider: - access_token: "..." - -where ``BinderHub.use_registry`` and ``GitHubRepoProvider.access_token`` -are traits to be configured. - ----------------------------------- -Specific unsupported config found: - - -{{- if .Values.hub }} - -Special handling of hub.url is removed. Set config.BinderHub.hub_url instead: - -config: - BinderHub: - hub_url: {{ .Values.hub.url }} -{{- end }} - -{{- if .Values.github }} - -Top-level github configuration is removed. -Use: - -config: - GitHubRepoProvider: - access_token: {{ .Values.github.accessToken }} - client_id: {{ .Values.github.clientId }} - client_secret: {{ .Values.github.clientSecret }} -{{- end }} - -{{- if .Values.gitlab }} - -Top-level gitlab configuration is removed. -Use: - -config: - GitLabRepoProvider: - access_token: {{ .Values.gitlab.accessToken }} - private_token: {{ .Values.gitlab.privateToken }} -{{- end }} - -{{- if ne (typeOf .Values.registry.enabled) "" }} - -registry.enabled is removed. Use: - -config: - BinderHub: - use_registry: {{ .Values.registry.enabled }} -{{- end }} - -{{- if .Values.registry.authHost }} - -registry.authHost is removed. - -Use: - - registry: - url: {{ .Values.registry.authHost }} - -to set the registry url. - -{{- if and .Values.registry.host (ne .Values.registry.host .Values.registry.authHost) }} - -If registry url and auth url differ, use: - -registry: - url: {{ .Values.registry.authHost }} -config: - DockerRegistry: - url: {{ .Values.registry.host }} - config_url: {{ .Values.registry.authHost }} - -{{- end }} -{{- end }} - -{{- if .Values.registry.tokenUrl }} - -special-handling of .registry.tokenUrl is removed. -Use: - -config: - DockerRegistry: - token_url: {{ .Values.registry.tokenUrl }} - -{{- end }} - -{{- if .Values.registry.prefix }} - -registry.prefix is removed. Use: - -config: - BinderHub: - image_prefix: {{.Values.registry.prefix }} - -{{- end }} - -{{- if or .Values.build .Values.perRepoQuota }} - -build is removed. Use: - -config: - BinderHub: - appendix: {{ .Values.build.appendix }} - build_cleanup_interval: {{ .Values.build.cleanupInterval }} - build_image: {{ .Values.build.repo2dockerImage }} - build_max_age: {{ .Values.build.maxAge }} - build_namespace: {{ .Values.build.namespace }} - build_node_selector: {{ .Values.build.nodeSelector }} - log_tail_lines: {{ .Values.build.logTailLines }} - per_repo_quota: {{ .Values.perRepoQuota }} -{{- end }} - -{{- /* end removedConfig */ -}} -{{- end }} - - -{{- if (or .Values.hub .Values.build .Values.github .Values.gitlab .Values.perRepoQuota .Values.registry.host .Values.registry.authHost .Values.registry.tokenUrl .Values.registry.prefix (ne (typeOf .Values.registry.enabled) "")) }} -{{- fail (include "removedConfig" .) }} -{{- end }} - - -1. Get the application URL by running these commands: -{{- if .Values.ingress.hostname }} - http://{{- .Values.ingress.hostname }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get svc -w {{ template "fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') - echo http://$SERVICE_IP:{{ .Values.service.externalPort }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "fullname" . }}" -o jsonpath="{.items[0].metadata.name}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl port-forward $POD_NAME 8080:{{ .Values.service.externalPort }} -{{- end }} - -{{- $breaking := "" }} -{{- $breaking_title := "\n" }} -{{- $breaking_title = print $breaking_title "\n#################################################################################" }} -{{- $breaking_title = print $breaking_title "\n###### BREAKING: The config values passed contained no longer accepted #####" }} -{{- $breaking_title = print $breaking_title "\n###### options. See the messages below for more details. #####" }} -{{- $breaking_title = print $breaking_title "\n###### #####" }} -{{- $breaking_title = print $breaking_title "\n###### To verify your updated config is accepted, you can use #####" }} -{{- $breaking_title = print $breaking_title "\n###### the `helm template` command. #####" }} -{{- $breaking_title = print $breaking_title "\n#################################################################################" }} - -{{- if hasKey .Values.cors "allowOrigin" }} -{{- $breaking = print $breaking "\n\nRENAMED: cors.allowOrigin has been renamed to config.BinderHub.cors_allow_origin" }} -{{- end }} - -{{- if hasKey .Values.jupyterhub.custom.cors "allowOrigin" }} -{{- $breaking = print $breaking "\n\nRENAMED: jupyterhub.custom.cors.allowOrigin has been renamed to jupyterhub.hub.config.BinderSpawner.cors_allow_origin" }} -{{- end }} - -{{- if hasKey .Values.dind "enabled" }} -{{- $breaking = print $breaking "\n\nCHANGED:" }} -{{- $breaking = print $breaking "\n\ndind:" }} -{{- $breaking = print $breaking "\n enabled: true" }} -{{- $breaking = print $breaking "\n\nmust as of version 0.3.0 be replace by" }} -{{- $breaking = print $breaking "\n\nimageBuilderType: dind" }} -{{- end }} - -{{- if hasKey .Values.imageCleaner.host "enabled" }} -{{- $breaking = print $breaking "\n\nCHANGED:" }} -{{- $breaking = print $breaking "\n\nimageCleaner:" }} -{{- $breaking = print $breaking "\n host:" }} -{{- $breaking = print $breaking "\n enabled: true" }} -{{- $breaking = print $breaking "\n\nas of version 0.3.0 is not used anymore." }} -{{- $breaking = print $breaking "\n\nThe image cleaner is either disabled or adapted to the value of imageBuilderType." }} -{{- end }} - -{{- if $breaking }} -{{- fail (print $breaking_title $breaking) }} -{{- end }} diff --git a/helm-chart/binderhub/templates/_helpers.tpl b/helm-chart/binderhub/templates/_helpers.tpl deleted file mode 100644 index 39780deb0..000000000 --- a/helm-chart/binderhub/templates/_helpers.tpl +++ /dev/null @@ -1,48 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -*/}} -{{- define "fullname" -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Render docker config.json for the registry-publishing secret and other docker configuration. -*/}} -{{- define "buildDockerConfig" -}} - -{{- /* default auth url */ -}} -{{- $url := (default "https://index.docker.io/v1" .Values.registry.url) }} - -{{- /* default username if unspecified - (_json_key for gcr.io, otherwise) -*/ -}} - -{{- if not .Values.registry.username }} - {{- if eq $url "https://gcr.io" }} - {{- $_ := set .Values.registry "username" "_json_key" }} - {{- else }} - {{- $_ := set .Values.registry "username" "" }} - {{- end }} -{{- end }} -{{- $username := .Values.registry.username -}} - -{{- /* initialize a dict to represent a docker config with registry credentials */}} -{{- $dockerConfig := dict "auths" (dict $url (dict "auth" (printf "%s:%s" $username .Values.registry.password | b64enc))) }} - -{{- /* augment our initialized docker config with buildDockerConfig */}} -{{- if .Values.config.BinderHub.buildDockerConfig }} -{{- $dockerConfig := merge $dockerConfig .Values.config.BinderHub.buildDockerConfig }} -{{- end }} - -{{- $dockerConfig | toPrettyJson }} -{{- end }} diff --git a/helm-chart/binderhub/templates/container-builder/daemonset.yaml b/helm-chart/binderhub/templates/container-builder/daemonset.yaml deleted file mode 100644 index 427c20e8d..000000000 --- a/helm-chart/binderhub/templates/container-builder/daemonset.yaml +++ /dev/null @@ -1,148 +0,0 @@ -{{- if ne .Values.imageBuilderType "host" -}} -{{- $builderName := .Values.imageBuilderType -}} -{{- $builder := index .Values $builderName -}} -{{- $daemonset := $builder.daemonset -}} -{{- $hostSocketPath := printf "%s/%s" $builder.hostSocketDir $builder.hostSocketName }} - -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: {{ .Release.Name }}-{{ $builderName }} -spec: - updateStrategy: - type: RollingUpdate - selector: - matchLabels: - name: {{ .Release.Name }}-{{ $builderName }} - template: - metadata: - labels: - name: {{ .Release.Name }}-{{ $builderName }} - app: binder - component: image-builder - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - spec: - {{- with include "jupyterhub.imagePullSecrets" (dict "root" . "image" $daemonset.image) }} - imagePullSecrets: {{ . }} - {{- end }} - tolerations: - - effect: NoSchedule - key: hub.jupyter.org/dedicated - operator: Equal - value: user - - effect: NoSchedule - key: hub.jupyter.org_dedicated - operator: Equal - value: user - nodeSelector: {{ .Values.config.BinderHub.build_node_selector | toJson }} - - initContainers: - - name: filesystem - # Reuse the main container image since this is a simple shell command - image: {{ $daemonset.image.name }}:{{ $daemonset.image.tag }} - {{- with $daemonset.image.pullPolicy }} - imagePullPolicy: {{ . }} - {{- end }} - command: - - sh - - -c - - > - if [ -d "{{ $hostSocketPath }}" ]; then - echo "Removing incorrect socket directory {{ $hostSocketPath }}"; - rmdir "{{ $hostSocketPath }}"; - fi - securityContext: - privileged: true - volumeMounts: - - name: run-{{ $builderName }} - mountPath: {{ $builder.hostSocketDir }} - {{- with $builder.initContainers }} - {{- . | toYaml | nindent 8 }} - {{- end }} - - containers: - - name: {{ $builderName }} - image: {{ $daemonset.image.name }}:{{ $daemonset.image.tag }} - {{- with $daemonset.image.pullPolicy }} - imagePullPolicy: {{ . }} - {{- end }} - {{- with $builder.resources }} - resources: - {{- $builder.resources | toYaml | nindent 12 }} - {{- end }} - {{- if eq $builderName "dind" }} - args: - - dockerd - - --storage-driver={{ $builder.storageDriver }} - - -H unix://{{ $hostSocketPath }} - {{- with $daemonset.extraArgs }} - {{- . | toYaml | nindent 12 }} - {{- end }} - securityContext: - privileged: true - volumeMounts: - - name: dockerlib-dind - mountPath: /var/lib/docker - - name: run-dind - mountPath: {{ $builder.hostSocketDir }} - {{- end }} - {{- if eq $builderName "pink" }} - args: - - podman - - system - - service - - --time=0 - - unix://{{ $hostSocketPath }} - {{- with $daemonset.extraArgs }} - {{- . | toYaml | nindent 12 }} - {{- end }} - securityContext: - privileged: true - runAsUser: 0 - volumeMounts: - - name: podman-containers - mountPath: /var/lib/containers/storage - - name: run-pink - mountPath: {{ $builder.hostSocketDir }} - {{- end }} - - {{- with $daemonset.extraVolumeMounts }} - {{- . | toYaml | nindent 10 }} - {{- end }} - - {{- with $daemonset.lifecycle }} - lifecycle: - {{- . | toYaml | nindent 12 }} - {{- end }} - - volumes: - {{- if eq $builderName "dind" }} - - name: dockerlib-dind - hostPath: - path: {{ $builder.hostLibDir }} - type: DirectoryOrCreate - - name: run-dind - hostPath: - path: {{ $builder.hostSocketDir }} - type: DirectoryOrCreate - {{- with $daemonset.extraVolumes }} - {{- . | toYaml | nindent 8 }} - {{- end }} - {{- end }} - {{- if eq $builderName "pink" }} - - name: podman-containers - hostPath: - path: {{ $builder.hostStorageDir }} - type: DirectoryOrCreate - - name: run-pink - hostPath: - path: {{ $builder.hostSocketDir }} - type: DirectoryOrCreate - {{- with $daemonset.extraVolumes }} - {{- . | toYaml | nindent 8 }} - {{- end }} - {{- end }} - - terminationGracePeriodSeconds: 30 -{{- end }} diff --git a/helm-chart/binderhub/templates/deployment.yaml b/helm-chart/binderhub/templates/deployment.yaml deleted file mode 100644 index 42237df36..000000000 --- a/helm-chart/binderhub/templates/deployment.yaml +++ /dev/null @@ -1,173 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: binder -spec: - replicas: {{ .Values.replicas }} - selector: - matchLabels: - app: binder - component: binder - release: {{ .Release.Name }} - strategy: - rollingUpdate: - {{- if eq (.Values.replicas | int) 1 }} - maxSurge: 1 - maxUnavailable: 0 - {{- end }} - template: - metadata: - labels: - app: binder - name: binder - component: binder - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - {{- with .Values.deployment.labels }} - {{- . | toYaml | nindent 8 }} - {{- end }} - annotations: - # This lets us autorestart when the secret's pass-through config changes - checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} - {{- with .Values.podAnnotations }} - {{- . | toYaml | nindent 8 }} - {{- end }} - spec: - {{- with include "jupyterhub.imagePullSecrets" (dict "root" . "image" .Values.image) }} - imagePullSecrets: {{ . }} - {{- end }} - {{- with .Values.initContainers }} - initContainers: - {{- . | toYaml | nindent 8 }} - {{- end }} - nodeSelector: {{ .Values.nodeSelector | toJson }} - {{- with .Values.tolerations }} - tolerations: - {{- . | toYaml | nindent 8 }} - {{- end }} - {{- if .Values.rbac.enabled }} - serviceAccountName: binderhub - {{- end }} - volumes: - - name: config - secret: - secretName: binder-secret - {{- if .Values.extraFiles }} - - name: files - secret: - secretName: binder-secret - items: - {{- range $file_key, $file_details := .Values.extraFiles }} - - key: {{ $file_key | quote }} - path: {{ $file_key | quote }} - {{- with $file_details.mode }} - mode: {{ . }} - {{- end }} - {{- end }} - {{- end }} - {{- if .Values.config.BinderHub.use_registry }} - - name: docker-secret - secret: - secretName: binder-build-docker-config - {{- else }} - - name: docker-socket - hostPath: - path: /var/run/docker.sock - {{- end }} - - {{- with .Values.extraVolumes }} - {{- . | toYaml | nindent 6 }} - {{- end }} - containers: - - name: binder - image: {{ .Values.image.name }}:{{ .Values.image.tag }} - args: - - --config - - /etc/binderhub/config/binderhub_config.py - {{- with .Values.lifecycle }} - lifecycle: - {{- . | toYaml | nindent 10 }} - {{- end }} - volumeMounts: - - mountPath: /etc/binderhub/config/ - name: config - readOnly: true - {{- range $file_key, $file_details := .Values.extraFiles }} - - mountPath: {{ $file_details.mountPath }} - subPath: {{ $file_key | quote }} - {{- with $file_details.mode }} - mode: {{ . }} - {{- end }} - name: files - {{- end }} - {{- if .Values.config.BinderHub.use_registry }} - - mountPath: /root/.docker - name: docker-secret - readOnly: true - {{- else }} - - mountPath: /var/run/docker.sock - name: docker-socket - {{- end }} - {{- with .Values.extraVolumeMounts }} - {{- . | toYaml | nindent 10 }} - {{- end }} - resources: - {{- .Values.resources | toYaml | nindent 10 }} - imagePullPolicy: IfNotPresent - env: - - name: BUILD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: JUPYTERHUB_API_TOKEN - valueFrom: - secretKeyRef: - name: "{{ include "jupyterhub.hub.fullname" . }}" - key: hub.services.binder.apiToken - {{- if .Values.config.BinderHub.auth_enabled }} - - name: JUPYTERHUB_SERVICE_NAME - value: binder - - name: JUPYTERHUB_API_URL - value: {{ (print (.Values.config.BinderHub.hub_url_local | default .Values.config.BinderHub.hub_url | trimSuffix "/") "/hub/api/") }} - - name: JUPYTERHUB_BASE_URL - value: {{ .Values.jupyterhub.hub.baseUrl | quote }} - - name: JUPYTERHUB_CLIENT_ID - value: {{ .Values.jupyterhub.hub.services.binder.oauth_client_id | quote }} - - name: JUPYTERHUB_OAUTH_CALLBACK_URL - value: {{ .Values.jupyterhub.hub.services.binder.oauth_redirect_uri | quote }} - {{- if .Values.jupyterhub.hub.allowNamedServers }} - - name: JUPYTERHUB_ALLOW_NAMED_SERVERS - value: "true" - - name: JUPYTERHUB_NAMED_SERVER_LIMIT_PER_USER - value: {{ .Values.jupyterhub.hub.namedServerLimitPerUser | quote }} - {{- end }} - {{- end }} - {{- with .Values.extraEnv }} - {{- include "jupyterhub.extraEnv" . | nindent 8 }} - {{- end }} - ports: - - containerPort: 8585 - name: binder - {{- if .Values.deployment.readinessProbe.enabled }} - readinessProbe: - httpGet: - path: {{ .Values.config.BinderHub.base_url }}versions - port: binder - initialDelaySeconds: {{ .Values.deployment.readinessProbe.initialDelaySeconds }} - periodSeconds: {{ .Values.deployment.readinessProbe.periodSeconds }} - timeoutSeconds: {{ .Values.deployment.readinessProbe.timeoutSeconds }} - failureThreshold: {{ .Values.deployment.readinessProbe.failureThreshold }} - {{- end }} - {{- if .Values.deployment.livenessProbe.enabled }} - livenessProbe: - httpGet: - path: {{ .Values.config.BinderHub.base_url }}versions - port: binder - initialDelaySeconds: {{ .Values.deployment.livenessProbe.initialDelaySeconds }} - periodSeconds: {{ .Values.deployment.livenessProbe.periodSeconds }} - timeoutSeconds: {{ .Values.deployment.livenessProbe.timeoutSeconds }} - failureThreshold: {{ .Values.deployment.livenessProbe.failureThreshold }} - {{- end }} - {{- with .Values.extraPodSpec }} - {{- toYaml . | nindent 6 }} - {{- end }} diff --git a/helm-chart/binderhub/templates/image-cleaner.yaml b/helm-chart/binderhub/templates/image-cleaner.yaml deleted file mode 100644 index 5497557e8..000000000 --- a/helm-chart/binderhub/templates/image-cleaner.yaml +++ /dev/null @@ -1,92 +0,0 @@ -{{- if .Values.imageCleaner.enabled -}} -{{- $builderName := .Values.imageBuilderType -}} -{{- $builder := index .Values $builderName -}} - -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: {{ .Release.Name }}-image-cleaner -spec: - updateStrategy: - type: RollingUpdate - selector: - matchLabels: - name: {{ .Release.Name }}-image-cleaner - template: - metadata: - labels: - name: {{ .Release.Name }}-image-cleaner - app: binder - component: image-cleaner - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - spec: - {{- with include "jupyterhub.imagePullSecrets" (dict "root" . "image" .Values.imageCleaner.image) }} - imagePullSecrets: {{ . }} - {{- end }} - tolerations: - - effect: NoSchedule - key: hub.jupyter.org/dedicated - operator: Equal - value: user - - effect: NoSchedule - key: hub.jupyter.org_dedicated - operator: Equal - value: user - nodeSelector: {{ .Values.config.BinderHub.build_node_selector | toJson }} - {{- if .Values.rbac.enabled }} - serviceAccountName: {{ .Release.Name }}-image-cleaner - {{- end }} - containers: - - name: image-cleaner-{{ $builderName }} - image: {{ .Values.imageCleaner.image.name }}:{{ .Values.imageCleaner.image.tag }} - {{- with .Values.imageCleaner.image.pullPolicy }} - imagePullPolicy: {{ . }} - {{- end }} - volumeMounts: - - name: storage-{{ $builderName }} - mountPath: /var/lib/{{ $builderName }} - - name: socket-{{ $builderName }} - mountPath: /var/run/docker.sock - env: - {{- if .Values.imageCleaner.cordon }} - - name: DOCKER_IMAGE_CLEANER_NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - {{- end }} - - name: DOCKER_IMAGE_CLEANER_PATH_TO_CHECK - value: /var/lib/{{ $builderName }} - - name: DOCKER_IMAGE_CLEANER_DELAY_SECONDS - value: {{ .Values.imageCleaner.delay | quote }} - - name: DOCKER_IMAGE_CLEANER_THRESHOLD_TYPE - value: {{ .Values.imageCleaner.imageGCThresholdType | quote }} - - name: DOCKER_IMAGE_CLEANER_THRESHOLD_HIGH - value: {{ .Values.imageCleaner.imageGCThresholdHigh | quote }} - - name: DOCKER_IMAGE_CLEANER_THRESHOLD_LOW - value: {{ .Values.imageCleaner.imageGCThresholdLow | quote }} - {{- with .Values.imageCleaner.extraEnv }} - {{- include "jupyterhub.extraEnv" . | nindent 8 }} - {{- end }} - terminationGracePeriodSeconds: 0 - volumes: - {{- if eq $builderName "host" }} - - name: storage-host - hostPath: - path: {{ .Values.imageCleaner.host.dockerLibDir }} - - name: socket-host - hostPath: - path: {{ .Values.imageCleaner.host.dockerSocket }} - type: Socket - {{- end }} - {{- if or (eq $builderName "dind") (eq $builderName "pink") }} - - name: storage-{{ $builderName }} - hostPath: - path: {{ eq $builderName "dind" | ternary $builder.hostLibDir $builder.hostStorageDir }} - type: DirectoryOrCreate - - name: socket-{{ $builderName }} - hostPath: - path: {{ $builder.hostSocketDir }}/{{ $builder.hostSocketName }} - type: Socket - {{- end }} -{{- end }} diff --git a/helm-chart/binderhub/templates/ingress.yaml b/helm-chart/binderhub/templates/ingress.yaml deleted file mode 100644 index f8511e831..000000000 --- a/helm-chart/binderhub/templates/ingress.yaml +++ /dev/null @@ -1,51 +0,0 @@ -{{- if .Values.ingress.enabled -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: binderhub - {{- if or (and .Values.ingress.https.enabled (eq .Values.ingress.https.type "kube-lego")) .Values.ingress.annotations }} - annotations: - {{- if and .Values.ingress.https.enabled (eq .Values.ingress.https.type "kube-lego") }} - kubernetes.io/tls-acme: "true" - {{- end }} - {{- with .Values.ingress.annotations }} - {{- . | toYaml | nindent 4 }} - {{- end }} - {{- end }} -spec: - {{- with .Values.ingress.ingressClassName }} - ingressClassName: {{ . | quote }} - {{- end }} - rules: - {{- range $host := .Values.ingress.hosts | default (list "") }} - - http: - paths: - - path: /{{ $.Values.ingress.pathSuffix }} - pathType: {{ $.Values.ingress.pathType }} - backend: - service: - name: binder - port: - number: 80 - {{- with $host }} - host: {{ . | quote }} - {{- end }} - {{- end }} - {{- if and .Values.ingress.https.enabled (eq .Values.ingress.https.type "kube-lego") }} - tls: - - secretName: kubelego-tls-binder-{{ .Release.Name }} - hosts: - {{- range .Values.ingress.hosts }} - - {{ . | quote }} - {{- end }} - {{- else if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} -{{- end }} diff --git a/helm-chart/binderhub/templates/pdb.yaml b/helm-chart/binderhub/templates/pdb.yaml deleted file mode 100644 index c83949e51..000000000 --- a/helm-chart/binderhub/templates/pdb.yaml +++ /dev/null @@ -1,29 +0,0 @@ -{{- if .Values.pdb.enabled -}} -{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} -apiVersion: policy/v1 -{{- else }} -apiVersion: policy/v1beta1 -{{- end }} -kind: PodDisruptionBudget -metadata: - name: binderhub - labels: - app: binder - name: binder - component: binder - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - {{- if not (.Values.pdb.maxUnavailable | typeIs "") }} - maxUnavailable: {{ .Values.pdb.maxUnavailable }} - {{- end }} - {{- if not (.Values.pdb.minAvailable | typeIs "") }} - minAvailable: {{ .Values.pdb.minAvailable }} - {{- end }} - selector: - matchLabels: - app: binder - name: binder - component: binder - release: {{ .Release.Name }} -{{- end }} diff --git a/helm-chart/binderhub/templates/rbac.yaml b/helm-chart/binderhub/templates/rbac.yaml deleted file mode 100644 index 5716b5abf..000000000 --- a/helm-chart/binderhub/templates/rbac.yaml +++ /dev/null @@ -1,91 +0,0 @@ -{{- if .Values.rbac.enabled -}} -kind: Role -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - labels: - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - name: binderhub -rules: -- apiGroups: [""] # "" indicates the core API group - resources: ["pods"] - verbs: ["get", "watch", "list", "create", "delete"] -- apiGroups: [""] - resources: ["pods/log"] - verbs: ["get"] ---- -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - labels: - app: binderhub - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - name: binderhub -subjects: -- kind: ServiceAccount - namespace: {{ .Release.Namespace }} - name: binderhub -roleRef: - kind: Role - name: binderhub - apiGroup: rbac.authorization.k8s.io ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - labels: - app: binderhub - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - name: binderhub -{{- if .Values.imageCleaner.enabled }} ---- -# image-cleaner role -# needs to cordon nodes during image cleaning -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - labels: - app: binderhub - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - name: {{ .Release.Name }}-image-cleaner -rules: -- apiGroups: [""] # "" indicates the core API group - resources: ["nodes"] - verbs: ["get", "patch"] ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - labels: - app: binderhub - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - name: {{ .Release.Name }}-image-cleaner -subjects: -- kind: ServiceAccount - namespace: {{ .Release.Namespace }} - name: {{ .Release.Name }}-image-cleaner -roleRef: - kind: ClusterRole - name: {{ .Release.Name }}-image-cleaner - apiGroup: rbac.authorization.k8s.io ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - labels: - app: binderhub - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - name: {{ .Release.Name }}-image-cleaner -{{- end }} -{{- end }} diff --git a/helm-chart/binderhub/templates/secret.yaml b/helm-chart/binderhub/templates/secret.yaml deleted file mode 100644 index e6ee24d5c..000000000 --- a/helm-chart/binderhub/templates/secret.yaml +++ /dev/null @@ -1,42 +0,0 @@ -{{- /* - Note that changes to the rendered version of this - file will trigger a restart of the BinderHub pod - through a annotation containing a hash of this - file rendered. -*/ -}} -kind: Secret -apiVersion: v1 -metadata: - name: binder-secret -type: Opaque -stringData: - {{- /* - Stash away relevant Helm template values for - the BinderHub Python application to read from - in binderhub_config.py. - */}} - values.yaml: | - {{- pick .Values "config" "imageBuilderType" "cors" "dind" "pink" "extraConfig" | toYaml | nindent 4 }} - - {{- /* Glob files to allow them to be mounted by the binderhub pod */}} - {{- /* key=filename: value=content */}} - {{- (.Files.Glob "files/*").AsConfig | nindent 2 }} - - {{- with include "jupyterhub.extraFiles.stringData" .Values.extraFiles }} - {{- . | nindent 2 }} - {{- end }} - -{{- with include "jupyterhub.extraFiles.data" .Values.extraFiles }} -data: - {{- . | nindent 2 }} -{{- end }} ---- -{{- if or .Values.config.BinderHub.use_registry .Values.config.BinderHub.buildDockerConfig }} -kind: Secret -apiVersion: v1 -metadata: - name: binder-build-docker-config -type: Opaque -data: - config.json: {{ include "buildDockerConfig" . | b64enc | quote }} -{{- end }} diff --git a/helm-chart/binderhub/templates/service.yaml b/helm-chart/binderhub/templates/service.yaml deleted file mode 100644 index 5fe350568..000000000 --- a/helm-chart/binderhub/templates/service.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: binder - annotations: {{ .Values.service.annotations | toJson }} - labels: {{ .Values.service.labels | toJson }} -spec: - type: {{ .Values.service.type }} - {{- with .Values.service.loadBalancerIP }} - loadBalancerIP: {{ . | quote }} - {{- end }} - selector: - app: binder - name: binder - component: binder - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - ports: - - protocol: TCP - port: 80 - targetPort: 8585 - {{- with .Values.service.nodePort }} - nodePort: {{ . }} - {{- end }} diff --git a/helm-chart/binderhub/values.yaml b/helm-chart/binderhub/values.yaml deleted file mode 100644 index 3109f0bc8..000000000 --- a/helm-chart/binderhub/values.yaml +++ /dev/null @@ -1,370 +0,0 @@ -pdb: - enabled: true - maxUnavailable: 1 - minAvailable: - -replicas: 1 - -resources: - requests: - cpu: 0.2 - memory: 512Mi - -rbac: - enabled: true - -nodeSelector: {} -tolerations: [] - -image: - name: quay.io/jupyterhub/k8s-binderhub - tag: "set-by-chartpress" - pullPolicy: "" - pullSecrets: [] - -# registry here is only used to create docker config.json -registry: - # key in 'auths' in docker config.json, - # ~always the registry url - url: - # registry username+password - username: - password: - -service: - type: LoadBalancer - labels: {} - annotations: - prometheus.io/scrape: "true" - nodePort: - loadBalancerIP: - -config: - # These c.BinderHub properties are referenced by the Helm chart - BinderHub: - # auth_enabled: - base_url: / - build_node_selector: {} - # hub_url: - # hub_url_local: - use_registry: true - KubernetesBuildExecutor: {} - -extraConfig: {} - -extraFiles: {} - -extraPodSpec: {} - -# Two bits of config need to be set to fully enable cors. -# config.BinderHub.cors_allow_origin controls the allowed origins for the -# binderhub api, and jupyterhub.hub.config.BinderSpawner.cors_allow_origin -# controls the allowed origins for the spawned user notebooks. You most -# likely want to set both of those to the same value. - -jupyterhub: - # Deprecated values, kept here so we can provide useful error messages - custom: - cors: {} - cull: - enabled: true - users: true - hub: - config: - JupyterHub: - authenticator_class: "null" - BinderSpawner: - auth_enabled: false - loadRoles: - binder: - services: - - binder - scopes: - - servers - # admin:users is required in order to create a jupyterhub user for an - # anonymous binderhub web-server visitor in non-authenticated - # deployments, and read:users is required for authenticated - # deployments to check the state of a jupyterhub user's running - # servers before trying to launch. - - admin:users - extraConfig: - 0-binderspawnermixin: | - """ - Helpers for creating BinderSpawners - - FIXME: - This file is defined in binderhub/binderspawner_mixin.py - and is copied to helm-chart/binderhub/values.yaml - by ci/check_embedded_chart_code.py - - The BinderHub repo is just used as the distribution mechanism for this spawner, - BinderHub itself doesn't require this code. - - Longer term options include: - - Move BinderSpawnerMixin to a separate Python package and include it in the Z2JH Hub - image - - Override the Z2JH hub with a custom image built in this repository - - Duplicate the code here and in binderhub/binderspawner_mixin.py - """ - - from tornado import web - from traitlets import Bool, Unicode - from traitlets.config import Configurable - - - class BinderSpawnerMixin(Configurable): - """ - Mixin to convert a JupyterHub container spawner to a BinderHub spawner - - Container spawner must support the following properties that will be set - via spawn options: - - image: Container image to launch - - token: JupyterHub API token - """ - - def __init__(self, *args, **kwargs): - # Is this right? Is it possible to having multiple inheritance with both - # classes using traitlets? - # https://stackoverflow.com/questions/9575409/calling-parent-class-init-with-multiple-inheritance-whats-the-right-way - # https://github.com/ipython/traitlets/pull/175 - super().__init__(*args, **kwargs) - - auth_enabled = Bool( - False, - help=""" - Enable authenticated binderhub setup. - - Requires `jupyterhub-singleuser` to be available inside the repositories - being built. - """, - config=True, - ) - - cors_allow_origin = Unicode( - "", - help=""" - Origins that can access the spawned notebooks. - - Sets the Access-Control-Allow-Origin header in the spawned - notebooks. Set to '*' to allow any origin to access spawned - notebook servers. - - See also BinderHub.cors_allow_origin in binderhub config - for controlling CORS policy for the BinderHub API endpoint. - """, - config=True, - ) - - def get_args(self): - if self.auth_enabled: - args = super().get_args() - else: - args = [ - "--ip=0.0.0.0", - f"--port={self.port}", - f"--NotebookApp.base_url={self.server.base_url}", - f"--NotebookApp.token={self.user_options['token']}", - "--NotebookApp.trust_xheaders=True", - ] - if self.default_url: - args.append(f"--NotebookApp.default_url={self.default_url}") - - if self.cors_allow_origin: - args.append("--NotebookApp.allow_origin=" + self.cors_allow_origin) - # allow_origin=* doesn't properly allow cross-origin requests to single files - # see https://github.com/jupyter/notebook/pull/5898 - if self.cors_allow_origin == "*": - args.append("--NotebookApp.allow_origin_pat=.*") - args += self.args - # ServerApp compatibility: duplicate NotebookApp args - for arg in list(args): - if arg.startswith("--NotebookApp."): - args.append(arg.replace("--NotebookApp.", "--ServerApp.")) - return args - - def start(self): - if not self.auth_enabled: - if "token" not in self.user_options: - raise web.HTTPError(400, "token required") - if "image" not in self.user_options: - raise web.HTTPError(400, "image required") - if "image" in self.user_options: - self.image = self.user_options["image"] - return super().start() - - def get_env(self): - env = super().get_env() - if "repo_url" in self.user_options: - env["BINDER_REPO_URL"] = self.user_options["repo_url"] - for key in ( - "binder_ref_url", - "binder_launch_host", - "binder_persistent_request", - "binder_request", - ): - if key in self.user_options: - env[key.upper()] = self.user_options[key] - return env - - 00-binder: | - # image & token are set via spawn options - from kubespawner import KubeSpawner - - class BinderSpawner(BinderSpawnerMixin, KubeSpawner): - pass - - c.JupyterHub.spawner_class = BinderSpawner - services: - binder: - display: false - singleuser: - # start jupyterlab server *if available* - # fallback on jupyter-notebook - cmd: - - python3 - - "-c" - - | - import os - import sys - - try: - import jupyterlab - import jupyterlab.labapp - major = int(jupyterlab.__version__.split(".", 1)[0]) - except Exception as e: - print("Failed to import jupyterlab: {e}", file=sys.stderr) - have_lab = False - else: - have_lab = major >= 3 - - if have_lab: - # technically, we could accept another jupyter-server-based frontend - print("Launching jupyter-lab", file=sys.stderr) - exe = "jupyter-lab" - else: - print("jupyter-lab not found, launching jupyter-notebook", file=sys.stderr) - exe = "jupyter-notebook" - - # launch the notebook server - os.execvp(exe, sys.argv) - events: true - storage: - type: none - memory: - guarantee: - prePuller: - hook: - enabled: false - continuous: - enabled: false - -deployment: - readinessProbe: - enabled: true - initialDelaySeconds: 0 - periodSeconds: 5 - failureThreshold: 1000 # we rely on the liveness probe to resolve issues if needed - timeoutSeconds: 3 - livenessProbe: - enabled: true - initialDelaySeconds: 10 - periodSeconds: 5 - failureThreshold: 3 - timeoutSeconds: 10 - labels: {} - -imageBuilderType: "host" - -dind: - initContainers: [] - daemonset: - image: - name: docker.io/library/docker - tag: "27.5.1-dind" # ref: https://hub.docker.com/_/docker/tags - pullPolicy: "" - pullSecrets: [] - # Additional command line arguments to pass to dockerd - extraArgs: [] - lifecycle: {} - extraVolumes: [] - extraVolumeMounts: [] - storageDriver: overlay2 - resources: {} - hostSocketDir: /var/run/dind - hostLibDir: /var/lib/dind - hostSocketName: docker.sock - -# Podman in Kubernetes -pink: - initContainers: [] - daemonset: - image: - name: quay.io/podman/stable - tag: "v5.3.2" # ref: https://quay.io/repository/podman/stable - pullPolicy: "" - pullSecrets: [] - lifecycle: {} - extraVolumes: [] - extraVolumeMounts: [] - resources: {} - hostStorageDir: /var/lib/pink/storage - hostSocketDir: /var/run/pink - hostSocketName: podman.sock - -imageCleaner: - enabled: true - extraEnv: {} - image: - name: quay.io/jupyterhub/docker-image-cleaner - tag: "1.0.0-beta.3" - pullPolicy: "" - pullSecrets: [] - # whether to cordon nodes while cleaning - cordon: true - # delete an image at most every 5 seconds - delay: 5 - # Interpret threshold values as percentage or bytes - imageGCThresholdType: "relative" - # when 80% of inodes are used, - # cull images until it drops below 60% - imageGCThresholdHigh: 80 - imageGCThresholdLow: 60 - # cull images on the host docker as well as dind - # configuration to use if `imageBuilderType: host` is configured - host: - dockerSocket: /var/run/docker.sock - dockerLibDir: /var/lib/docker - -ingress: - enabled: false - https: - enabled: false - type: kube-lego - hosts: [] - ingressClassName: - annotations: - {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - pathSuffix: - # Suffix added to Ingress's routing path pattern. - # Specify `*` if your ingress matches path by glob pattern. - pathType: Prefix - tls: - [] - # Secrets must be manually created in the namespace. - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -initContainers: [] -lifecycle: {} -extraVolumes: [] -extraVolumeMounts: [] -extraEnv: {} -podAnnotations: {} - -# Deprecated values, kept here so we can provide useful error messages -cors: {} - -global: {} diff --git a/helm-chart/chartpress.yaml b/helm-chart/chartpress.yaml deleted file mode 100644 index 79526391f..000000000 --- a/helm-chart/chartpress.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# For a reference on this configuration, see the chartpress README file. -# ref: https://github.com/jupyterhub/chartpress -# -# NOTE: All paths will be set relative to this file's location, which is in the -# helm-chart folder. -charts: - - name: binderhub - baseVersion: 1.0.0-0.dev - imagePrefix: quay.io/jupyterhub/k8s- - repo: - git: jupyterhub/helm-chart - published: https://jupyterhub.github.io/helm-chart - images: - binderhub: - # We will not use the default build contextPath, and must therefore - # specify the dockerfilePath explicitly. - dockerfilePath: images/binderhub/Dockerfile - # Context to send to docker build for use by the Dockerfile. We pass the - # root folder in order to allow the image to access and build the python - # package. - contextPath: .. - # To avoid chartpress to react to changes in documentation and other - # things, we ask it to not trigger on changes to the contextPath, which - # means we manually should add paths rebuild should be triggered on - rebuildOnContextPathChanges: false - # We manually specify the paths which chartpress should monitor for - # changes that should trigger a rebuild of this image. - paths: - - images/binderhub - - ../binderhub - - ../js - - ../babel.config.json - - ../MANIFEST.in - - ../package.json - - ../pyproject.toml - - ../requirements.txt - - ../setup.cfg - - ../setup.py - - ../webpack.config.js - valuesPath: image diff --git a/helm-chart/images/binderhub/Dockerfile b/helm-chart/images/binderhub/Dockerfile deleted file mode 100644 index 7393827ef..000000000 --- a/helm-chart/images/binderhub/Dockerfile +++ /dev/null @@ -1,69 +0,0 @@ -# syntax = docker/dockerfile:1.3 - - -# The build stage -# --------------- -# This stage is building Python wheels for use in later stages by using a base -# image that has more pre-requisites to do so, such as a C++ compiler. -# -# NOTE: If the image version is updated, also update it in ci/refreeze! -# -FROM python:3.13-bookworm as build-stage - -# Build wheels -# -# We set pip's cache directory and expose it across build stages via an -# ephemeral docker cache (--mount=type=cache,target=${PIP_CACHE_DIR}). We use -# the same technique for the directory /tmp/wheels. -# -# assumes `python3 -m build .` has been run to create the wheel -# in the top-level dist directory -COPY helm-chart/images/binderhub/requirements.txt ./ -COPY dist . -ARG PIP_CACHE_DIR=/tmp/pip-cache -RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ - pip wheel --wheel-dir=/tmp/wheels \ - # pycurl wheels for 7.45.3 have problems finding CAs - # https://github.com/pycurl/pycurl/issues/834 - --no-binary pycurl \ - -r ./requirements.txt \ - ./binderhub*.whl - - -# The final stage -# --------------- -# This stage is built and published as quay.io/jupyterhub/k8s-binderhub. -# -FROM python:3.13-slim-bookworm - -ENV PYTHONUNBUFFERED=1 -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update \ - && apt-get upgrade --yes \ - && apt-get install --yes \ - git \ - # required by binderhub - libcurl4 \ - # required by pycurl - tini \ - # tini is used as an entrypoint to not loose track of SIGTERM - # signals as sent before SIGKILL, for example when "docker stop" - # or "kubectl delete pod" is run. By doing that the pod can - # terminate very quickly. - && rm -rf /var/lib/apt/lists/* - -# install wheels built in the build stage -COPY helm-chart/images/binderhub/requirements.txt /tmp/requirements.txt -ARG PIP_CACHE_DIR=/tmp/pip-cache -RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ - --mount=type=cache,from=build-stage,source=/tmp/wheels,target=/tmp/wheels \ - pip install --no-deps /tmp/wheels/* \ - # validate pip install since it's not resolving dependencies - && pip check \ - # verify success of previous step - && python -c "import pycurl, binderhub.app" - -EXPOSE 8585 -ENTRYPOINT ["tini", "--", "python", "-m", "binderhub"] -CMD ["--config", "/etc/binderhub/config/binderhub_config.py"] diff --git a/helm-chart/images/binderhub/README.md b/helm-chart/images/binderhub/README.md deleted file mode 100644 index b34f85924..000000000 --- a/helm-chart/images/binderhub/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# binderhub image - -The image for running binderhub itself. -Built with [chartpress][]. - -[chartpress]: https://github.com/jupyterhub/chartpress - -## Updating requirements.txt - -Use the "Run workflow" button at -https://github.com/jupyterhub/binderhub/actions/workflows/watch-dependencies.yaml. diff --git a/helm-chart/images/binderhub/requirements.in b/helm-chart/images/binderhub/requirements.in deleted file mode 100644 index 1b462744b..000000000 --- a/helm-chart/images/binderhub/requirements.in +++ /dev/null @@ -1,25 +0,0 @@ -# google-cloud-logging is an optional dependency used by mybinder.org-deploy at -# https://github.com/jupyterhub/mybinder.org-deploy/blob/e47021fe/mybinder/values.yaml#L193-L216. -# -# We pin it to avoid introducing a potentially breaking change as major versions -# are released. See: -# https://github.com/googleapis/python-logging/blob/master/UPGRADING.md -# -google-cloud-logging==3.* - -# jupyterhub's major version should be matched with the JupyterHub Helm chart's -# used version of JupyterHub. -# -jupyterhub==4.* - -# https://github.com/kubernetes-client/python -kubernetes==9.* - -# binderhub's dependencies -# -# We can't put ".[pycurl]" here directly as when we freeze this into -# requirements.txt using ci/refreeze, its declaring "binderhub @ file:///io" -# which is a problem as its an absolute path. -# -pycurl --r ../../../requirements.txt diff --git a/helm-chart/images/binderhub/requirements.txt b/helm-chart/images/binderhub/requirements.txt deleted file mode 100644 index d9e6749af..000000000 --- a/helm-chart/images/binderhub/requirements.txt +++ /dev/null @@ -1,210 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.13 -# by the following command: -# -# Use the "Run workflow" button at https://github.com/jupyterhub/binderhub/actions/workflows/watch-dependencies.yaml -# -alembic==1.14.1 - # via jupyterhub -async-generator==1.10 - # via jupyterhub -attrs==25.1.0 - # via - # jsonschema - # referencing -cachetools==5.5.1 - # via google-auth -certifi==2025.1.31 - # via - # kubernetes - # requests -certipy==0.2.1 - # via jupyterhub -cffi==1.17.1 - # via cryptography -charset-normalizer==3.4.1 - # via requests -cryptography==44.0.1 - # via certipy -deprecated==1.2.18 - # via opentelemetry-api -docker==7.1.0 - # via -r /io/requirements.txt -escapism==1.0.1 - # via -r /io/requirements.txt -google-api-core==2.24.1 - # via - # google-cloud-appengine-logging - # google-cloud-core - # google-cloud-logging -google-auth==2.38.0 - # via - # google-api-core - # google-cloud-appengine-logging - # google-cloud-core - # google-cloud-logging - # kubernetes -google-cloud-appengine-logging==1.6.0 - # via google-cloud-logging -google-cloud-audit-log==0.3.0 - # via google-cloud-logging -google-cloud-core==2.4.1 - # via google-cloud-logging -google-cloud-logging==3.11.4 - # via -r helm-chart/images/binderhub/requirements.in -googleapis-common-protos==1.67.0 - # via - # google-api-core - # google-cloud-audit-log - # grpc-google-iam-v1 - # grpcio-status -greenlet==3.1.1 - # via sqlalchemy -grpc-google-iam-v1==0.14.0 - # via google-cloud-logging -grpcio==1.70.0 - # via - # google-api-core - # googleapis-common-protos - # grpc-google-iam-v1 - # grpcio-status -grpcio-status==1.70.0 - # via google-api-core -idna==3.10 - # via requests -importlib-metadata==8.5.0 - # via opentelemetry-api -jinja2==3.1.5 - # via - # -r /io/requirements.txt - # jupyterhub -jsonschema==4.23.0 - # via - # -r /io/requirements.txt - # jupyter-telemetry -jsonschema-specifications==2024.10.1 - # via jsonschema -jupyter-telemetry==0.1.0 - # via jupyterhub -jupyterhub==4.1.6 - # via - # -r /io/requirements.txt - # -r helm-chart/images/binderhub/requirements.in -kubernetes==9.0.1 - # via - # -r /io/requirements.txt - # -r helm-chart/images/binderhub/requirements.in -mako==1.3.9 - # via alembic -markupsafe==3.0.2 - # via - # jinja2 - # mako -oauthlib==3.2.2 - # via - # jupyterhub - # requests-oauthlib -opentelemetry-api==1.30.0 - # via google-cloud-logging -packaging==24.2 - # via jupyterhub -pamela==1.2.0 - # via jupyterhub -prometheus-client==0.21.1 - # via - # -r /io/requirements.txt - # jupyterhub -proto-plus==1.26.0 - # via - # google-api-core - # google-cloud-appengine-logging - # google-cloud-logging -protobuf==5.29.3 - # via - # google-api-core - # google-cloud-appengine-logging - # google-cloud-audit-log - # google-cloud-logging - # googleapis-common-protos - # grpc-google-iam-v1 - # grpcio-status - # proto-plus -pyasn1==0.6.1 - # via - # pyasn1-modules - # rsa -pyasn1-modules==0.4.1 - # via google-auth -pycparser==2.22 - # via cffi -pycurl==7.45.4 - # via -r helm-chart/images/binderhub/requirements.in -pyjwt==2.10.1 - # via -r /io/requirements.txt -python-dateutil==2.9.0.post0 - # via - # jupyterhub - # kubernetes -python-json-logger==3.2.1 - # via - # -r /io/requirements.txt - # jupyter-telemetry -pyyaml==6.0.2 - # via kubernetes -referencing==0.36.2 - # via - # jsonschema - # jsonschema-specifications -requests==2.32.3 - # via - # docker - # google-api-core - # jupyterhub - # kubernetes - # requests-oauthlib -requests-oauthlib==2.0.0 - # via kubernetes -rpds-py==0.22.3 - # via - # jsonschema - # referencing -rsa==4.9 - # via google-auth -ruamel-yaml==0.18.10 - # via jupyter-telemetry -six==1.17.0 - # via - # kubernetes - # python-dateutil -sqlalchemy==2.0.38 - # via - # alembic - # jupyterhub -tornado==6.4.2 - # via - # -r /io/requirements.txt - # jupyterhub -traitlets==5.14.3 - # via - # -r /io/requirements.txt - # jupyter-telemetry - # jupyterhub -typing-extensions==4.12.2 - # via - # alembic - # sqlalchemy -urllib3==2.3.0 - # via - # docker - # kubernetes - # requests -websocket-client==1.8.0 - # via kubernetes -wrapt==1.17.2 - # via deprecated -zipp==3.21.0 - # via importlib-metadata - -# The following packages are considered to be unsafe in a requirements file: -setuptools==75.8.0 - # via kubernetes From 4136c2b0d704f64055306a492101320d63791787 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Thu, 6 Mar 2025 19:30:35 -0800 Subject: [PATCH 193/200] Prepare for merging upstream --- .github/dependabot.yaml | 16 -- .github/workflows/watch-dependencies.yaml | 120 ------------ .gitignore | 175 ------------------ .pre-commit-config.yaml | 67 ------- .readthedocs.yaml | 17 -- LICENSE | 28 --- README.md | 64 ------- RELEASE.md | 48 ----- ci/publish | 42 ----- ci/refreeze | 16 -- dev-requirements.txt | 22 --- docs/Makefile | 38 ---- docs/README.md | 22 --- docs/requirements.txt | 6 - .../images/binderhub-service-diagram.png | Bin 84156 -> 0 bytes docs/source/_static/images/logo/favicon.ico | Bin 1439 -> 0 bytes docs/source/_static/images/logo/logo.png | Bin 15025 -> 0 bytes docs/source/_static/images/logo/logo.svg | 36 ---- docs/source/conf.py | 85 --------- docs/source/contributing.md | 8 - docs/source/explanation/architecture.md | 69 ------- docs/source/explanation/index.md | 20 -- docs/source/howto/index.md | 19 -- docs/source/index.md | 48 ----- docs/source/reference/changelog.md | 9 - docs/source/reference/index.md | 20 -- .../connect-with-jupyterhub-fancy-profiles.md | 83 --------- .../tutorials/connect-with-jupyterhub.md | 103 ----------- docs/source/tutorials/index.md | 22 --- docs/source/tutorials/install.md | 134 -------------- .../binderhub}/.helmignore | 0 .../binderhub}/Chart.yaml | 0 .../mounted-files/binderhub_config.py | 0 .../binderhub}/templates/NOTES.txt | 0 .../templates/_helpers-extra-files.tpl | 0 .../binderhub}/templates/_helpers-labels.tpl | 0 .../binderhub}/templates/_helpers-names.tpl | 0 .../binderhub}/templates/_helpers.tpl | 0 .../build-pods-docker-config/secret.yaml | 0 .../binderhub}/templates/deployment.yaml | 0 .../templates/docker-api/daemonset.yaml | 0 .../templates/docker-api/secret.yaml | 0 .../binderhub}/templates/ingress.yaml | 0 .../binderhub}/templates/role.yaml | 0 .../binderhub}/templates/rolebinding.yaml | 0 .../binderhub}/templates/secret.yaml | 0 .../binderhub}/templates/service.yaml | 0 .../binderhub}/templates/serviceaccount.yaml | 0 .../binderhub}/values.schema.yaml | 0 .../binderhub}/values.yaml | 0 chartpress.yaml => helm-chart/chartpress.yaml | 0 .../images}/binderhub-service/Dockerfile | 0 .../images}/binderhub-service/README.md | 0 .../images}/binderhub-service/requirements.in | 0 .../binderhub-service/requirements.txt | 0 pyproject.toml | 83 --------- tests/conftest.py | 6 - tools/generate-json-schema.py | 66 ------- tools/templates/lint-and-validate-values.yaml | 95 ---------- tools/templates/lint-and-validate.py | 114 ------------ tools/templates/yamllint-config.yaml | 8 - tools/validate-against-schema.py | 32 ---- 62 files changed, 1741 deletions(-) delete mode 100644 .github/dependabot.yaml delete mode 100644 .github/workflows/watch-dependencies.yaml delete mode 100644 .gitignore delete mode 100644 .pre-commit-config.yaml delete mode 100644 .readthedocs.yaml delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 RELEASE.md delete mode 100755 ci/publish delete mode 100755 ci/refreeze delete mode 100644 dev-requirements.txt delete mode 100644 docs/Makefile delete mode 100644 docs/README.md delete mode 100644 docs/requirements.txt delete mode 100644 docs/source/_static/images/binderhub-service-diagram.png delete mode 100644 docs/source/_static/images/logo/favicon.ico delete mode 100644 docs/source/_static/images/logo/logo.png delete mode 100644 docs/source/_static/images/logo/logo.svg delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/contributing.md delete mode 100644 docs/source/explanation/architecture.md delete mode 100644 docs/source/explanation/index.md delete mode 100644 docs/source/howto/index.md delete mode 100644 docs/source/index.md delete mode 100644 docs/source/reference/changelog.md delete mode 100644 docs/source/reference/index.md delete mode 100644 docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md delete mode 100644 docs/source/tutorials/connect-with-jupyterhub.md delete mode 100644 docs/source/tutorials/index.md delete mode 100644 docs/source/tutorials/install.md rename {binderhub-service => helm-chart/binderhub}/.helmignore (100%) rename {binderhub-service => helm-chart/binderhub}/Chart.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/mounted-files/binderhub_config.py (100%) rename {binderhub-service => helm-chart/binderhub}/templates/NOTES.txt (100%) rename {binderhub-service => helm-chart/binderhub}/templates/_helpers-extra-files.tpl (100%) rename {binderhub-service => helm-chart/binderhub}/templates/_helpers-labels.tpl (100%) rename {binderhub-service => helm-chart/binderhub}/templates/_helpers-names.tpl (100%) rename {binderhub-service => helm-chart/binderhub}/templates/_helpers.tpl (100%) rename {binderhub-service => helm-chart/binderhub}/templates/build-pods-docker-config/secret.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/templates/deployment.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/templates/docker-api/daemonset.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/templates/docker-api/secret.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/templates/ingress.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/templates/role.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/templates/rolebinding.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/templates/secret.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/templates/service.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/templates/serviceaccount.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/values.schema.yaml (100%) rename {binderhub-service => helm-chart/binderhub}/values.yaml (100%) rename chartpress.yaml => helm-chart/chartpress.yaml (100%) rename {images => helm-chart/images}/binderhub-service/Dockerfile (100%) rename {images => helm-chart/images}/binderhub-service/README.md (100%) rename {images => helm-chart/images}/binderhub-service/requirements.in (100%) rename {images => helm-chart/images}/binderhub-service/requirements.txt (100%) delete mode 100644 pyproject.toml delete mode 100644 tests/conftest.py delete mode 100755 tools/generate-json-schema.py delete mode 100644 tools/templates/lint-and-validate-values.yaml delete mode 100755 tools/templates/lint-and-validate.py delete mode 100644 tools/templates/yamllint-config.yaml delete mode 100755 tools/validate-against-schema.py diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml deleted file mode 100644 index 48c8c6fb9..000000000 --- a/.github/dependabot.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# dependabot.yaml reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file -# -# Notes: -# - Status and logs from dependabot are provided at -# https://github.com/2i2c-org/binderhub-service/network/updates. -# - YAML anchors are not supported here or in GitHub Workflows. -# -version: 2 -updates: - # Maintain dependencies in our GitHub Workflows - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: monthly - time: "05:00" - timezone: "Etc/UTC" diff --git a/.github/workflows/watch-dependencies.yaml b/.github/workflows/watch-dependencies.yaml deleted file mode 100644 index 928d5afb7..000000000 --- a/.github/workflows/watch-dependencies.yaml +++ /dev/null @@ -1,120 +0,0 @@ -# This is a GitHub workflow defining a set of jobs with a set of steps. -# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions -# -# This Workflow watches dependencies and automatically creates PRs to update -# them. -# -# - Watch multiple images tags referenced in values.yaml to match the latest -# stable image tag (ignoring pre-releases). -# - Refreeze images/*/requirements.txt based on images/*/requirements.in -# -name: Watch dependencies - -on: - pull_request: - paths: - - ".github/workflows/watch-dependencies.yaml" - push: - paths: - - "images/*/requirements.in" - - ".github/workflows/watch-dependencies.yaml" - branches: ["main"] - schedule: - # Run at 05:00 on day-of-month 1, ref: https://crontab.guru/#0_5_1_*_* - - cron: "0 5 1 * *" - workflow_dispatch: - -jobs: - update-image-dependencies: - if: github.repository == '2i2c-org/binderhub-service' - runs-on: ubuntu-22.04 - permissions: - contents: write - pull-requests: write - - strategy: - fail-fast: false - matrix: - include: - - name: docker - registry: docker.io - repository: library/docker - values_path: dockerApi.image.tag - tag_prefix: "" - tag_suffix: -dind - - steps: - - uses: actions/checkout@v4 - - - name: Get values.yaml pinned tag of ${{ matrix.registry }}/${{ matrix.repository }} - id: local - run: | - local_tag=$(cat binderhub-service/values.yaml | yq e '.${{ matrix.values_path }}' -) - echo "tag=$local_tag" >> $GITHUB_OUTPUT - - - name: Get latest tag of ${{ matrix.registry }}/${{ matrix.repository }} - id: latest - # The skopeo image helps us list tags consistently from different docker - # registries. We identify the latest docker image tag based on the - # version numbers of format x.y.z included in a pattern with an optional - # prefix and suffix, like the tags "v4.5.0" (v prefix) and "23.0.5-dind" - # (-dind suffix). - run: | - latest_tag=$( - docker run --rm quay.io/skopeo/stable list-tags docker://${{ matrix.registry }}/${{ matrix.repository }} \ - | jq -r '[.Tags[] | select(. | match("^${{ matrix.tag_prefix }}\\d+\\.\\d+\\.\\d+${{ matrix.tag_suffix }}$") | .string)] | sort_by(split(".") | map(ltrimstr("${{ matrix.tag_prefix }}") | rtrimstr("${{ matrix.tag_suffix }}") | tonumber)) | last' - ) - echo "tag=$latest_tag" >> $GITHUB_OUTPUT - - - name: Update values.yaml pinned tag - if: steps.local.outputs.tag != steps.latest.outputs.tag - run: | - sed --in-place 's/tag: "${{ steps.local.outputs.tag }}"/tag: "${{ steps.latest.outputs.tag }}"/g' binderhub-service/values.yaml - - - name: git diff - if: steps.local.outputs.tag != steps.latest.outputs.tag - run: git --no-pager diff --color=always - - # ref: https://github.com/peter-evans/create-pull-request - - uses: peter-evans/create-pull-request@v7 - if: github.event_name != 'pull_request' - with: - branch: update-image-dependencies - labels: dependencies - commit-message: Update ${{ matrix.repository }} version from ${{ steps.local.outputs.tag }} to ${{ steps.latest.outputs.tag }} - title: Update ${{ matrix.repository }} version from ${{ steps.local.outputs.tag }} to ${{ steps.latest.outputs.tag }} - body: >- - A new ${{ matrix.repository }} image version has been detected, version - `${{ steps.latest.outputs.tag }}`. - - refreeze-dockerfile-requirements-txt: - if: github.repository == '2i2c-org/binderhub-service' - runs-on: ubuntu-22.04 - permissions: - contents: write - pull-requests: write - - steps: - - uses: actions/checkout@v4 - - - name: Refreeze requirements.txt based on requirements.in - run: ci/refreeze - - - name: git diff - run: git --no-pager diff --color=always - - # ref: https://github.com/peter-evans/create-pull-request - - uses: peter-evans/create-pull-request@v7 - if: github.event_name != 'pull_request' - with: - branch: update-image-requirements - labels: dependencies - commit-message: "binderhub-service image: refreeze requirements.txt" - title: "binderhub-service image: refreeze requirements.txt" - body: >- - The binderhub-service image's requirements.txt has been refrozen - based on requirements.in. - - The push to this branch was made by a bot account so all tests - aren't triggered to run. Close and re-open this PR to trigger them - manually. diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 46e573bec..000000000 --- a/.gitignore +++ /dev/null @@ -1,175 +0,0 @@ -### Repository specific files that are generated - -binderhub-service/values.schema.json -tools/templates/rendered-templates/ - -# convenience for storing production config in the repo while developing -prod-config.yaml - -# Other misc things -.vscode -*.DS_Store - - -### Python .gitignore from https://github.com/github/gitignore/blob/HEAD/Python.gitignore -# -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 25b1af8e4..000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# pre-commit is a tool to perform a predefined set of tasks manually and/or -# automatically before git commits are made. -# -# Config reference: https://pre-commit.com/#pre-commit-configyaml---top-level -# -# Common tasks -# -# - Run on all files: pre-commit run --all-files -# - Register git hooks: pre-commit install --install-hooks -# -repos: - # Autoformat: Python code, syntax patterns are modernized - - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 - hooks: - - id: pyupgrade - args: - - --py310-plus - - # Autoformat: Python code - - repo: https://github.com/PyCQA/autoflake - rev: v2.3.1 - hooks: - - id: autoflake - args: - - --in-place - - # Autoformat: Python code - - repo: https://github.com/psf/black - rev: 25.1.0 - hooks: - - id: black - args: - - --target-version=py310 - - --target-version=py311 - - # Autoformat: Python code - - repo: https://github.com/pycqa/isort - rev: 6.0.1 - hooks: - - id: isort - args: - - --profile=black - - # Autoformat: markdown, yaml (but not helm templates) - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.8 - hooks: - - id: prettier - exclude: binderhub-service/templates/.* - - # Linting: Python code (see the file .flake8) - - repo: https://github.com/PyCQA/flake8 - rev: "7.1.2" - hooks: - - id: flake8 - # Ignore style and complexity - # E: style errors - # W: style warnings - # C: complexity - # - args: - - --ignore=E,C,W - -# pre-commit.ci config reference: https://pre-commit.ci/#configuration -ci: - autoupdate_schedule: monthly diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index f92c2a20f..000000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Configuration on how ReadTheDocs (RTD) builds our documentation -# ref: https://readthedocs.org/projects/binderhub-service/ -# ref: https://docs.readthedocs.io/en/stable/config-file/v2.html -# -version: 2 - -sphinx: - configuration: docs/source/conf.py - -build: - os: ubuntu-22.04 - tools: - python: "3.11" - -python: - install: - - requirements: docs/requirements.txt diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c38d4181a..000000000 --- a/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2024, 2i2c - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md deleted file mode 100644 index 12c5dffa9..000000000 --- a/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# binderhub-service - -[![Documentation Status](https://img.shields.io/readthedocs/binderhub-service?logo=read-the-docs)](https://binderhub-service.readthedocs.io/en/latest/) -[![Latest chart development release](https://img.shields.io/badge/Helm_releases-https://2i2c.org/binderhub-service/blue?link=https://2i2c.org/binderhub-service&color=blue)](https://2i2c.org/binderhub-service/) - -The binderhub-service is a Helm chart and guide to run BinderHub (the Python -software), as a standalone service to build and push images with repo2docker, -possibly configured for use with a JupyterHub chart installation. - -## History - -The BinderHub project provides two major pieces of functionality: - -1. Building (and pushing) images via an API using content from various - providers. -2. Launch interactive sessions using the built images (via a JupyterHub). - -The current upstream [binderhub helm chart](https://github.com/jupyterhub/binderhub/tree/main/helm-chart) -is a very opinionated distribution, focusing purely on public instances of BinderHub -(such as [mybinder.org](https://mybinder.org)). It has strong opinions on how -the JupyterHub should be configured, and how it should be connected to the BinderHub. -While historically this allowed for faster iteration on mybinder.org itself, -it has major limitations when used elsewhere. - -1. It places restrictions on how the JupyterHub used to launch the interactive sessions - can be installed and configured. It required workarounds for several types - of configuration, particularly around persistence (see [persistent binderhub](https://github.com/gesiscss/persistent_binderhub) - for example). -2. It can not be deployed without the attached, opinionated JupyterHub it comes with. - This makes deployment for use with alternate frontends (such as - [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) - difficult) - -This project is designed to provide a standalone helm chart that does not have these -restrictions. - -## Restrictions - -To prevent a recurrance of the issues with the existing binderhub chart, the following -restrictions are in place for any work on this chart: - -> There will not be a _direct_ dependency on a JupyterHub. We can provide documentation on -> how to set this chart up next to a JupyterHub, but we will not provide a JupyterHub -> directly (via a [helm dependency](https://helm.sh/docs/chart_best_practices/dependencies/)) -> or otherwise. - -## Scope - -The documentation should help configure the BinderHub service to: - -- run behind JupyterHub authentication and authorization -- in one or more ways be able to launch built images -- in one or more ways handle the issue repo2docker building an image with data - put where JupyterHub user home folders typically is mounted - -## Installation - -Checkout this project's documentation for installation guide https://binderhub-service.readthedocs.io/en/latest. - -## Funding - -Funded in part by [GESIS](http://notebooks.gesis.org) in cooperation with -NFDI4DS [460234259](https://gepris.dfg.de/gepris/projekt/460234259?context=projekt&task=showDetail&id=460234259&) -and [CESSDA](https://www.cessda.eu). diff --git a/RELEASE.md b/RELEASE.md deleted file mode 100644 index 6d5d38832..000000000 --- a/RELEASE.md +++ /dev/null @@ -1,48 +0,0 @@ -# How to make a release - -`binderhub-service` is a Helm chart available in the Helm chart repository -`https://2i2c.org/binderhub-service`. - -## Pre-requisites - -- Push rights to [2i2c-org/binderhub-service] - -## Steps to make a release - -1. Create a PR updating `docs/source/changelog.md` with [github-activity] and - continue only when its merged. - - ```shell - pip install github-activity - - github-activity --heading-level=3 2i2c-org/binderhub-service - ``` - -1. Checkout main and make sure it is up to date. - - ```shell - git checkout main - git fetch origin main - git reset --hard origin/main - ``` - -1. Update the version, make commits, and push a git tag with `tbump`. - - ```shell - pip install tbump - tbump --dry-run ${VERSION} - - tbump ${VERSION} - ``` - - Following this, the [CI system] will build and publish a release. - -1. Reset the version back to dev, e.g. `1.1.0-0.dev` after releasing `1.0.0` - - ```shell - tbump --no-tag ${NEXT_VERSION}-0.dev - ``` - -[2i2c-org/binderhub-service]: https://github.com/2i2c-org/binderhub-service -[github-activity]: https://github.com/executablebooks/github-activity -[ci system]: https://github.com/2i2c-org/binderhub-service/actions/workflows/release.yaml diff --git a/ci/publish b/ci/publish deleted file mode 100755 index ba9678234..000000000 --- a/ci/publish +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# This script publishes the Helm chart to the GitHub Pages based Helm chart repo -# in and pushes associated built docker images to Docker hub using chartpress. -# -------------------------------------------------------------------------- - -# Exit on errors, assert env vars, log commands -set -eux - -PUBLISH_ARGS="--push --publish-chart \ - --builder docker-buildx \ - --platform linux/amd64 --platform linux/arm64 \ -" - -# chartpress use git to push to our Helm chart repository, which is the gh-pages -# branch of 2i2c-org/binderhub-service. -if [[ $GITHUB_REF != refs/tags/* ]]; then - # Using --extra-message, we help readers of merged PRs to know what version - # they need to bump to in order to make use of the PR. This is enabled by a - # GitHub notificaiton in the PR like "Github Action user pushed a commit to - # 2i2c-org/binderhub-service that referenced this pull request..." - # - # ref: https://github.com/jupyterhub/chartpress#usage - # - # NOTE: GitHub merge commits contain a PR reference like #123. `sed` looks - # to extract either a PR reference like #123 or fall back to create a - # commit hash reference like @123abcd. Combined with GITHUB_REPOSITORY - # we craft a commit message like 2i2c-org/binderhub-service#123 or - # 2i2c-org/binderhub-service@123abcd which will be understood as a - # reference by GitHub. - PR_OR_HASH=$(git log -1 --pretty=%h-%B | head -n1 | sed 's/^.*\(#[0-9]*\).*/\1/' | sed 's/^\([0-9a-f]*\)-.*/@\1/') - LATEST_COMMIT_TITLE=$(git log -1 --pretty=%B | head -n1) - EXTRA_MESSAGE="${GITHUB_REPOSITORY}${PR_OR_HASH} ${LATEST_COMMIT_TITLE}" - chartpress $PUBLISH_ARGS --extra-message "${EXTRA_MESSAGE}" -else - # Setting a tag explicitly enforces a rebuild if this tag had already been - # built and we wanted to override it. - chartpress $PUBLISH_ARGS --tag "${GITHUB_REF:10}" -fi - -# Let us log the changes chartpress did, it should include replacements for -# fields in values.yaml, such as what tag for various images we are using. -git --no-pager diff --color=always diff --git a/ci/refreeze b/ci/refreeze deleted file mode 100755 index 1cf62524f..000000000 --- a/ci/refreeze +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -set -xeuo pipefail - -# Because `pip-compile` resolves `requirements.txt` with the current Python for -# the current platform, it should be run on the same Python version and platform -# as our Dockerfile as done by this script. - -cd images/binderhub-service -docker run \ - --rm \ - --env=CUSTOM_COMPILE_COMMAND='Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml' \ - --volume="$PWD:/io" \ - --workdir=/io \ - --user=root \ - python:3.11-bullseye \ - sh -c 'pip install pip-tools==7.* && pip-compile --allow-unsafe --strip-extras --upgrade' diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 02c855b72..000000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,22 +0,0 @@ -# chartpress is important for local development, CI and CD -# - builds images and can push them also (--push) -# - updates image names and tags in values.yaml -# - can publish the built Helm chart (--publish) -# -# ref: https://github.com/jupyterhub/chartpress -# -chartpress - -# pytest is used to run tests -pytest - -# pyyaml is used by script under tools/ -pyyaml - -# yamllint is used by tools/templates/lint-and-validate.py script -yamllint - -# FIXME: We pin requests as docker-py breaks when using 2.32.0, see -# https://github.com/docker/docker-py/issues/3256. -# When removing this, also remove the pin in release.yaml. -requests==2.31.0 diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index c1a96d05b..000000000 --- a/docs/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -# Makefile for Sphinx documentation generated by sphinx-quickstart -# ---------------------------------------------------------------------------- - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) - - -# Manually added commands -# ---------------------------------------------------------------------------- - -# For local development: -# - builds and rebuilds html on changes to source -# - starts a livereload enabled webserver and opens up a browser -devenv: - sphinx-autobuild -b html --open-browser "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) - -# For local development and CI: -# - verifies that links are valid -linkcheck: - $(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)/linkcheck" $(SPHINXOPTS) - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index e55a017bb..000000000 --- a/docs/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# About the documentation - -This documentation is automatically built on each commit [as configured on -ReadTheDocs](https://readthedocs.org/projects/binderhub-service/) and in the -`.readthedocs.yaml` file, and made available on -[binderhub-service.readthedocs.io](https://binderhub-service.readthedocs.io/en/latest/). - -The documentation is meant to be structured according to the Diataxis framework -as documented in https://diataxis.fr/. - -## Local documentation development - -```shell -cd docs -pip install -r requirements.txt - -# automatic build and livereload enabled web-server -make devenv - -# automatic check of links validity -make linkcheck -``` diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 63f4be100..000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -myst-parser -sphinx-autobuild -sphinx-book-theme -sphinx-copybutton -sphinxext-opengraph -sphinxext-rediraffe diff --git a/docs/source/_static/images/binderhub-service-diagram.png b/docs/source/_static/images/binderhub-service-diagram.png deleted file mode 100644 index 4c22249f89397ebc4d89dd66a1ea9dc6dea72d7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84156 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 diff --git a/docs/source/_static/images/logo/logo.png b/docs/source/_static/images/logo/logo.png deleted file mode 100644 index 47ce3453420c6b338b4ab25673dd4ce0b0562bc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15025 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 g|L+BBTVrz*xBvNtB=RyB-~}u>DX3)rTf - - - - - - - - - - - - - - - - - - - - diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index eae21d133..000000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,85 +0,0 @@ -# Configuration file for Sphinx to build our documentation to HTML. -# -# Configuration reference: https://www.sphinx-doc.org/en/master/usage/configuration.html -# -import datetime - -# -- Project information ----------------------------------------------------- -# ref: https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -# -project = "binderhub-service" -copyright = f"{datetime.date.today().year}, 2i2c.org" -author = "2i2c.org" - - -# -- General Sphinx configuration --------------------------------------------------- -# ref: https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -# -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -# -extensions = [ - "myst_parser", - "sphinx_copybutton", - "sphinxext.opengraph", - "sphinxext.rediraffe", -] -root_doc = "index" -source_suffix = [".md"] - - -# -- Options for HTML output ------------------------------------------------- -# ref: https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -# -html_logo = "_static/images/logo/logo.png" -html_favicon = "_static/images/logo/favicon.ico" -html_static_path = ["_static"] - -# sphinx_book_theme reference: https://sphinx-book-theme.readthedocs.io/en/latest/?badge=latest -html_theme = "sphinx_book_theme" -html_theme_options = { - "home_page_in_toc": True, - "repository_url": "https://github.com/2i2c-org/binderhub-service/", - "repository_branch": "main", - "path_to_docs": "docs/source", - "use_download_button": False, - "use_edit_page_button": True, - "use_issues_button": True, - "use_repository_button": True, -} - - -# -- Options for linkcheck builder ------------------------------------------- -# ref: https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-the-linkcheck-builder -# -linkcheck_ignore = [ - r"(.*)github\.com(.*)#", # javascript based anchors - r"(.*)/#%21(.*)/(.*)", # /#!forum/jupyter - encoded anchor edge case - r"https://github.com/[^/]*$", # too many github usernames / searches in changelog - "https://github.com/2i2c-org/binderhub-service/pull/", # too many PRs in changelog - "https://github.com/2i2c-org/binderhub-service/compare/", # too many comparisons in changelog -] -linkcheck_anchors_ignore = [ - "/#!", - "/#%21", -] - - -# -- Options for the opengraph extension ------------------------------------- -# ref: https://github.com/wpilibsuite/sphinxext-opengraph#options -# -# ogp_site_url is set automatically by RTD -ogp_image = "_static/logo.svg" -ogp_use_first_image = True - - -# -- Options for the rediraffe extension ------------------------------------- -# ref: https://github.com/wpilibsuite/sphinxext-rediraffe#readme -# -# This extensions help us relocated content without breaking links. If a -# document is moved internally, we should configure a redirect like below. -# -rediraffe_branch = "main" -rediraffe_redirects = { - # "old-file": "new-folder/new-file-name", -} diff --git a/docs/source/contributing.md b/docs/source/contributing.md deleted file mode 100644 index 8bcb92363..000000000 --- a/docs/source/contributing.md +++ /dev/null @@ -1,8 +0,0 @@ -(contributing)= - -# Contributing - -Hello and thank you for contributing to binderhub-service! We are really excited to have you here! -Below you'll find some useful tasks, guidelines, and instructions for working in this repository. - -Notice something that's missing? Please open an issue or file a pull request! diff --git a/docs/source/explanation/architecture.md b/docs/source/explanation/architecture.md deleted file mode 100644 index 672ed1225..000000000 --- a/docs/source/explanation/architecture.md +++ /dev/null @@ -1,69 +0,0 @@ -(architecture)= - -# Architecture - -The `binderhub-service` chart runs the [BinderHub] Python software, in [api-only mode](https://binderhub.readthedocs.io/en/latest/reference/app.html#binderhub.app.BinderHub.enable_api_only_mode) (the default), as a standalone service to build, and push [Docker] images from source code repositories, on demand, using [repo2docker]. This service can then be paired with [JupyterHub] to allow users to initiate build requests from their hubs. - -## Architecture requirements - -Thus, the architecture of this system must: - -- facilitate the building and pushing of Docker images with repo2docker -- easily integrate with a JupyterHub deployment -- but also run as a standalone service -- operate within a Kubernetes environment - -## Architecture overview - -% (This image was generated at the following URL: https://docs.google.com/presentation/d/1KC9cyXGPGBQoeZ0sLxHORyhjXDklIfn-rZ5SAdRB08Q/edit?usp=sharing) following the BinderHub architecture chart at https://docs.google.com/presentation/d/1t5W4Rnez6xBRz4YxCxWYAx8t4KRfUosbCjS4Z1or7rM/edit#slide=id.g25dbc82125_0_53 - -```{figure} ../_static/images/binderhub-service-diagram.png -:alt: Here is a high-level overview of the components that make up binderhub-service. -``` - -```{tip} -Checking out the [BinderHub's architecture diagram](https://binderhub.readthedocs.io/en/latest/overview.html) might also be helpful. -``` - -## Details on how it works - -When a build & push request is fired, the following events happen: - -1. **BinderHub creates and starts a `build pod` that runs `repo2docker`** - - The `build pods` are managed by BinderHub through [`KubernetesBuildExecutor`](https://github.com/jupyterhub/binderhub/blob/7f8b6c3137a6f8e66e6c193ee81d32bcf0826a6e/binderhub/build.py#L222-L242) and are created as a result of an image build request. - - For the image build to work, the docker client processes running on these nodes need to be able to communicate with the dockerd daemon. This communication is done via unix socket mounted on the node. - -2. **repo2docker use a docker client to build and push images** - - A running [dockerd](https://docs.docker.com/engine/reference/commandline/dockerd/) daemon will intercept the docker commands initiated by the the docker client processes running on these build pods. This dockerd daemon is setup by the `docker-api` pods. - - The `docker-api` pods are setup to start on each node matching the [`dockerApi.nodeSelector`](https://github.com/2i2c-org/binderhub-service/blob/308965029a901993293539f159c66d15b767e8c8/binderhub-service/values.yaml#L131) by the following [DaemonSet definition](https://github.com/2i2c-org/binderhub-service/blob/main/binderhub-service/templates/docker-api/daemonset.yaml). - - The daemonset also setups a [hostPath](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath) volume that mounts a [unix socket](https://man7.org/linux/man-pages/man7/unix.7.html) from this node into the `docker-api` pods. - - ```{important} - The docker-api pods and the build pods must run on the same node so they can use the unix socket on it to interact with the docker daemon listening on this socket. - ``` - -3. **the build pods will then use the configured credentials to push the image to the repository** - - The build pods mount [**a k8s Secret** with the docker config file](https://github.com/2i2c-org/binderhub-service/blob/308965029a901993293539f159c66d15b767e8c8/binderhub-service/templates/build-pods-docker-config/secret.yaml) holding the necessary registry credentials so they can push to the container registry. - -```{warning} -The `binderhub-service` chart currently only supports Docker. Checkout https://github.com/2i2c-org/binderhub-service/issues/31 for updates on Podman support. -``` - -## Technical stack - -[BinderHub]: https://binderhub.readthedocs.io/en/latest/index.html -[JupyterHub]: https://jupyterhub.readthedocs.io/en/stable/ -[jupyterhub rbac]: https://jupyterhub.readthedocs.io/en/stable/rbac/index.html -[readthedocs]: https://readthedocs.org/ -[sphinx]: https://www.sphinx-doc.org/en/master/ -[sphinx-book-theme]: https://sphinx-book-theme.readthedocs.io/en/stable/ -[myst-parser]: https://myst-parser.readthedocs.io/en/stable/ -[github actions]: https://github.com/features/actions -[repo2docker]: https://github.com/jupyterhub/repo2docker -[Docker]: https://binderhub.readthedocs.io/en/latest/index.html diff --git a/docs/source/explanation/index.md b/docs/source/explanation/index.md deleted file mode 100644 index 33d7ebfc3..000000000 --- a/docs/source/explanation/index.md +++ /dev/null @@ -1,20 +0,0 @@ -(explanation)= - -# Explanation - -The documentation in this sections aims to _explain_ and _clarify_ a particular -topic within the project in order to broaden the reader's understanding. - -```{note} -[Read more about the Explanation quadrant of the diataxis framework.](https://diataxis.fr/explanation/) -``` - -```{attention} -This documentation is a Work in Progress! -Please see our [contributing guide](contributing) if you'd like to add to it. -``` - -```{toctree} -:maxdepth: 2 -architecture.md -``` diff --git a/docs/source/howto/index.md b/docs/source/howto/index.md deleted file mode 100644 index 715e8961a..000000000 --- a/docs/source/howto/index.md +++ /dev/null @@ -1,19 +0,0 @@ -(howto)= - -# How-To Guides - -The documentation in this section is _goal-oriented_ and designed to guide the -reader through specific steps to reach that goal. - -```{note} -[Read more about the How-To quadrant of the diataxis framework.](https://diataxis.fr/how-to-guides/) -``` - -```{attention} -This documentation is a Work in Progress! -Please see our [contributing guide](contributing) if you'd like to add to it. -``` - -```{toctree} -:maxdepth: 2 -``` diff --git a/docs/source/index.md b/docs/source/index.md deleted file mode 100644 index aaac14579..000000000 --- a/docs/source/index.md +++ /dev/null @@ -1,48 +0,0 @@ -# binderhub-service - -`binderhub-service` is a [Helm chart](https://helm.sh/) and guide to run [BinderHub](https://github.com/jupyterhub/binderhub) (the Python software), as a standalone service to build and push images with [repo2docker](https://github.com/jupyterhub/repo2docker). It can be configured for use with a JupyterHub Helm chart installation. - -## How the documentation is organised - -We are currently using the [diátaxis framework](https://diataxis.fr/) to organise -our docs into four main categories: - -- [**Tutorials**](tutorials): Step-by-step guides to complete a specific task -- [**How-To guides**](howto): Directions to solve scenarios faced while using the project. Their titles often complete the sentence "How do I...?" -- [**Explanation**](explanation): More in-depth discussion of topics within the project to broaden understanding. -- [**Reference**](ref): Technical descriptions of the components within the project, and how to use them - -## Tutorials - -```{toctree} -:maxdepth: 2 -tutorials/index.md -``` - -## How-to guides - -```{toctree} -:maxdepth: 2 -howto/index.md -``` - -## Reference - -```{toctree} -:maxdepth: 2 -reference/index.md -``` - -## Explanation - -```{toctree} -:maxdepth: 2 -explanation/index.md -``` - -## Contributing - -```{toctree} -:maxdepth: 2 -contributing.md -``` diff --git a/docs/source/reference/changelog.md b/docs/source/reference/changelog.md deleted file mode 100644 index c08020f4d..000000000 --- a/docs/source/reference/changelog.md +++ /dev/null @@ -1,9 +0,0 @@ -(changelog)= - -# Changelog - -No releases made yet - -## 0.1.0 - -Test diff --git a/docs/source/reference/index.md b/docs/source/reference/index.md deleted file mode 100644 index a224472d9..000000000 --- a/docs/source/reference/index.md +++ /dev/null @@ -1,20 +0,0 @@ -(ref)= - -# Reference - -The documentation in this section provides technical descriptions of the -components used throughout the project, and how to use them. - -```{note} -[Read more about the Reference quadrant of the diataxis framework.](https://diataxis.fr/reference/) -``` - -```{attention} -This documentation is a Work in Progress! -Please see our [contributing guide](contributing) if you'd like to add to it. -``` - -```{toctree} -:maxdepth: 2 -changelog.md -``` diff --git a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md b/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md deleted file mode 100644 index 765f326b5..000000000 --- a/docs/source/tutorials/connect-with-jupyterhub-fancy-profiles.md +++ /dev/null @@ -1,83 +0,0 @@ -# Connect with jupyterhub-fancy-profiles - -The [jupyterhub-fancy-profiles](https://github.com/yuvipanda/jupyterhub-fancy-profiles) -project provides a user facing frontend for connecting the JupyterHub to BinderHub, -allowing users to build their own images the same way they would on `mybinder.org`! - -The following steps describe how to connect your `binderhub-service` [](installation) to `jupyterhub-fancy-profiles` - -1. First, we need to install the `jupyterhub-fancy-profiles` package in the container - that is running the JupyterHub process itself (not the user containers). The - easiest way to do this is to use one of the pre-built images provided by - the `jupyterhub-fancy-profiles` project. In the [list of tags](https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags), - select the latest tag that also includes the version of the z2jh chart you are - using (the `version` specified in step 4 of the previous step). This is _most likely_ - the tag on the top of the page, and looks something like `z2jh-v{{ z2jh version }}-fancy-profiles-sha-{{ some string}}`. - - Once you find the tag, _modify_ the `z2jh-config.yaml` file to enable `jupyterhub-fancy-profiles`. - While it is hidden here for clarity, make sure to preserve the `hub.services` section that - you added in step 3 of the previous section while editing this file. - - ```yaml - hub: - services: ... - image: - # from https://quay.io/repository/yuvipanda/z2jh-hub-with-fancy-profiles?tab=tags - name: quay.io/yuvipanda/z2jh-hub-with-fancy-profiles - tag: "" # example: "z2jh-v3.2.1-fancy-profiles-sha-5874628" - - extraConfig: - enable-fancy-profiles: | - from jupyterhub_fancy_profiles import setup_ui - setup_ui(c) - ``` - -2. Since `jupyterhub-fancy-profiles` adds on to the [profileList](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) - feature of KubeSpawner, we need to configure a profile list here as well. - Add this to the `z2jh-config.yaml` file: - - ```yaml - singleuser: - profileList: - - display_name: "Choose Your Environment" - profile_options: - image: - display_name: Image - dynamic_image_building: - enabled: True - unlisted_choice: - enabled: True - display_name: "Custom image" - validation_regex: "^.+:.+$" - validation_message: "Must be a publicly available docker image, of form :" - display_name_in_choices: "Specify an existing docker image" - description_in_choices: "Use a pre-existing docker image from a public docker registry (dockerhub, quay, etc)" - kubespawner_override: - image: "{value}" - choices: - pangeo: - display_name: Pangeo Notebook Image - description: "Python image with scientific, dask and geospatial tools" - kubespawner_override: - image: pangeo/pangeo-notebook:2023.09.11 - scipy: - display_name: Jupyter SciPy Notebook - slug: scipy - kubespawner_override: - image: jupyter/scipy-notebook:2023-06-26 - ``` - -3. Deploy, using the command from step 5 of the section above. - -4. Access the JupyterHub itself, using the external IP you got from step 5 of the section - above (not `{{ hub IP }}/services/binder/`). Once you log in (you can use _any_ username - and password), you should see a UI that allows you to choose two pre-existing - images (pangeo and scipy), specify your own image, or 'build' your own image. - The last option lets you access the binder functionality! Test it out :) - -From now on, you can customize this JupyterHub as you would any other JupyterHub set up -using z2jh. The [customization guide](https://z2jh.jupyter.org/en/stable/jupyterhub/customization.html) -contains many helpful examples of how you can customize your hub. In particular, -you probably want to set up more restrictive -[authentication](https://z2jh.jupyter.org/en/stable/administrator/authentication.html) -so not everyone can log in to your hub! diff --git a/docs/source/tutorials/connect-with-jupyterhub.md b/docs/source/tutorials/connect-with-jupyterhub.md deleted file mode 100644 index b1549d56d..000000000 --- a/docs/source/tutorials/connect-with-jupyterhub.md +++ /dev/null @@ -1,103 +0,0 @@ -# Connect with a JupyterHub installation - -The next steps describe how to connect the [`binderhub-service` installation](installation) to a JupyterHub set up via [z2jh](https://z2jh.jupyter.org/). While any JupyterHub that can run containers will work with this, the _most common_ setup is to use this with z2jh. - -The first few steps are lifted directly from the [install JupyterHub](https://z2jh.jupyter.org/en/stable/jupyterhub/installation.html) section of z2jh. - -1. Add the z2jh chart repository to helm: - - ``` - helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ - helm repo update - ``` - -2. We want the binderhub to be available under `http://{{hub url}}/services/binder`, because - that is what `jupyterhub-fancy-profiles` expects. Eventually we would also want authentication - to work correctly. For that, we must set up binderhub as a [JupyterHub Service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html). - This provides two things: - - a. Routing from `{{hub url }}/services/{{ service name }}` to the service, allowing us to - expose the service to the external world using JupyterHub's ingress / loadbalancer, without - needing a dedicated ingress / loadbalancer for BinderHub. - - b. (Eventually) Appropriate credentials for authenticated network calls between these two services. - - To make this connection, we need to tell JupyterHub where to find BinderHub. Eventually - this can be automatic (once [this issue](https://github.com/2i2c-org/binderhub-service/issues/57) - gets resolved). In the meantime, you can get the name of the BinderHub service by executing - the following command: - - ```bash - kubectl -n get svc -l app.kubernetes.io/name=binderhub-service - ``` - - Make a note of the name under the `NAME` column, we will use it in the next step. - -3. Create a config file, `z2jh-config.yaml`, to hold the config values for the JupyterHub. - - ```yaml - hub: - services: - binder: - # FIXME: ref https://github.com/2i2c-org/binderhub-service/issues/57 - # for something more readable and requiring less copy-pasting - url: http://{{ service name from step 2}} - ``` - -4. Find the latest version of the z2jh helm chart. The easiest way is to run the - following command: - - ```bash - helm search repo jupyterhub - ``` - - This should output a few columns. Look for the version under **CHART VERSION** (not _APP VERSION_) - for `jupyterhub/jupyterhub`. That's the latest z2jh chart version, and that is what - we will be using. - -5. Install the JupyterHub helm chart with the following command: - - ```bash - helm upgrade --cleanup-on-fail \ - --install jupyterhub/jupyterhub \ - --namespace \ - --version= \ - --values z2jh-config.yaml \ - --wait - ``` - - where: - - - `` is any name you can use to refer to this image - (like `jupyterhub`) - - - `` is the _same_ namespace used for the BinderHub install - - - `` is the latest stable version of the JupyterHub - helm chart, determined in the previous step. - -6. Find the external IP on which the JupyterHub is accessible: - - ```bash - kubectl -n get svc proxy-public - ``` - -7. Access the binder service by going to `http://{{ external ip from step 5}}/services/binder/` (the - trailing slash is _important_). You should see an unstyled, somewhat broken - 404 page. This is great and expected. Let's fix that. - -8. Change BinderHub config in `binderhub-service-config.yaml`, telling BinderHub it should now - be available under `/services/binder`. - - ```yaml - config: - BinderHub: - base_url: /services/binder - ``` - - Deploy this using the `helm upgrade` command from step 9 in the previous section. - -9. Test by going to `http://{{ external ip from step 5}}/services/binder/` (the trailing slash - is _important_!) again, and you should see a _styled_ 404 page! Success - - this means BinderHub is now connected to JupyterHub, even if the end users - can't see it yet. Let's connect them! diff --git a/docs/source/tutorials/index.md b/docs/source/tutorials/index.md deleted file mode 100644 index ce5654294..000000000 --- a/docs/source/tutorials/index.md +++ /dev/null @@ -1,22 +0,0 @@ -(tutorials)= - -# Tutorials - -The documentation in this section are step-by-step guides that lead the reader -through completing a specific task. - -```{note} -[Read more about the Tutorials quadrant of the diataxis framework.](https://diataxis.fr/tutorials/) -``` - -```{attention} -This documentation is a Work in Progress! -Please see our [contributing guide](contributing) if you'd like to add to it. -``` - -```{toctree} -:maxdepth: 2 -install.md -connect-with-jupyterhub-fancy-profiles.md -connect-with-jupyterhub.md -``` diff --git a/docs/source/tutorials/install.md b/docs/source/tutorials/install.md deleted file mode 100644 index b7d3e45bd..000000000 --- a/docs/source/tutorials/install.md +++ /dev/null @@ -1,134 +0,0 @@ -(installation)= - -# Installation - -The following steps describe how to install the `binderhub-service` helm chart. - -1. Add the `binderhub-service` chart repository to helm: - - ```bash - helm repo add binderhub-service https://2i2c.org/binderhub-service - helm repo update - ``` - - Note this URL will change eventually, as binderhub-service is designed - to be a generic service, not something for use only by 2i2c. - -2. Install the latest development version of `binderhub-service` into a - namespace. - - ```bash - helm upgrade \ - --install \ - --create-namespace \ - --devel \ - --wait \ - --namespace \ - \ - binderhub-service/binderhub-service - ``` - - This sets up a binderhub service, but not in a publicly visible way. - -3. Test that it's running by port-forwarding to the correct pod: - - ```bash - kubectl -n port-forward $(kubectl -n get pod -l app.kubernetes.io/component=binderhub -o name) 8585:8585 - ``` - - This should forward requests on port 8585 on your localhost, to the binder service running inside the pod. So if you go - to [localhost:8585](http://localhost:8585), you should see a binder styled page that says 404. If you do, _success_! - -4. Create a docker repository for binderhub to push built images to. In this tutorial, we will be using Google Artifact Registry, but [binderhub supports using other registries](https://binderhub.readthedocs.io/en/latest/zero-to-binderhub/setup-registry.html#set-up-the-container-registry). - - Create a new Artifact Registry ([via this URL](https://console.cloud.google.com/artifacts/create-repo). Make sure you're in the correct project (look at the drop - down in the top bar). If this is the first time you are using Artifact Registry, it may ask you to enable the service. - - In the repository creation page, give it a name (ideally same name you are using for - dedicated to the chart installation), select 'Docker' as the format, 'Standard' as the mode, 'Region' - as the location type and select the same region your kubernetes cluster is in. The - settings about encryption and other options can be left in their default. Click "Create". - -5. Find the full path of the repository you just created, by opening it in the list - and looking for the small 'copy' icon next to the name of the repository. If you - click it, it should copy something like `-docker.pkg.dev//`. - Save this. - -6. Create a Google Cloud Service Account that has permissions to push to this - repository ([via this URL] - (https://console.cloud.google.com/iam-admin/serviceaccounts/create) - make - sure you are in the correct project again). You may also need appropriate permissions to set this up. Give it a name (same as the name you used - for the chart installation, but with a '-pusher' suffix) and click 'Create and Continue'. - In the next step, select 'Artifact Registry Writer' as a role. Click "Continue". In the final step, just click "Done". - -7. Now that the service account is created, find it in the list and open it. You will - find a tab named 'Keys' once the informational display opens - select that. Click - 'Add Key' -> 'Create New Key'. In the dialog box that pops up, select 'JSON' as the - key type and click 'Create'. This should download a key file. **Keep this file safe**! - -8. Now that we have the appropriate permissions, let's set up our configuration! Create a - new file named `binderhub-service-config.yaml` with the following contents: - - ```yaml - config: - BinderHub: - use_registry: true - image_prefix: /binder - # Temporarily enable the binderhub UI so we can test image building and pushing - enable_api_only_mode: false - buildPodsRegistryCredentials: - server: "https://-docker.pkg.dev" - username: "_json_key" - password: | - - ``` - - where: - - 1. `` is what you copied from step 5. - - 2. `` is the JSON file you downloaded in step 7. - This is a multi-line file and will need to be indented correctly. The `|` after - `password` allows the value to be multi-line, and each line should be indented at least - 2 spaces from `password`. - - 3. `` is the region your artifact repository was created in. You can see - this in the first part of `` as well. - -9. Run a `helm upgrade` to use the new configuration you just created: - - ```bash - helm upgrade \ - --install \ - --create-namespace \ - --devel \ - --wait \ - --namespace - \ - binderhub-service/binderhub-service \ - --values binderhub-service-config.yaml - ``` - - This should set up binderhub with this custom config. If you run a `kubectl -n get pod`, - you will see that the binderhub pod has restarted - this confirms that the config has been set up! - -10. Let's verify that _image building and pushing_ works. Access the binderhub pod by following the - same instructions as step 3. But this time, you should see a binderhub page very similar to that - on [mybinder.org](https://mybinder.org). You can test build a repository here - I recommend trying - out `binder-examples/requirements`. It might take a while to build, but you should be able to see - logs in the UI. It should succeed at _pushing_ the github image, but will fail to launch. The last - lines in the log in the UI should look like: - - ``` - Successfully pushed europe-west10-docker.pkg.dev/binderhub-service-development/bh-service-test/binderbinder-2dexamples-2drequirements-55ab5c:50533eb470ee6c24e872043d30b2fee463d6943fBuilt image, launching... - Launching server... - Launch attempt 1 failed, retrying... - Launch attempt 2 failed, retrying... - ``` - - You can also go back to the Google Artifact Registry repository you created earlier to verify that the built - image is indeed there. - -11. Now that we have verified this is working, we can disable the binderhub UI as we will not be using it. - Remove the `config.BinderHub.enable_api_only_mode` configuration from the binderhub config, and redeploy - using the command from step 9. diff --git a/binderhub-service/.helmignore b/helm-chart/binderhub/.helmignore similarity index 100% rename from binderhub-service/.helmignore rename to helm-chart/binderhub/.helmignore diff --git a/binderhub-service/Chart.yaml b/helm-chart/binderhub/Chart.yaml similarity index 100% rename from binderhub-service/Chart.yaml rename to helm-chart/binderhub/Chart.yaml diff --git a/binderhub-service/mounted-files/binderhub_config.py b/helm-chart/binderhub/mounted-files/binderhub_config.py similarity index 100% rename from binderhub-service/mounted-files/binderhub_config.py rename to helm-chart/binderhub/mounted-files/binderhub_config.py diff --git a/binderhub-service/templates/NOTES.txt b/helm-chart/binderhub/templates/NOTES.txt similarity index 100% rename from binderhub-service/templates/NOTES.txt rename to helm-chart/binderhub/templates/NOTES.txt diff --git a/binderhub-service/templates/_helpers-extra-files.tpl b/helm-chart/binderhub/templates/_helpers-extra-files.tpl similarity index 100% rename from binderhub-service/templates/_helpers-extra-files.tpl rename to helm-chart/binderhub/templates/_helpers-extra-files.tpl diff --git a/binderhub-service/templates/_helpers-labels.tpl b/helm-chart/binderhub/templates/_helpers-labels.tpl similarity index 100% rename from binderhub-service/templates/_helpers-labels.tpl rename to helm-chart/binderhub/templates/_helpers-labels.tpl diff --git a/binderhub-service/templates/_helpers-names.tpl b/helm-chart/binderhub/templates/_helpers-names.tpl similarity index 100% rename from binderhub-service/templates/_helpers-names.tpl rename to helm-chart/binderhub/templates/_helpers-names.tpl diff --git a/binderhub-service/templates/_helpers.tpl b/helm-chart/binderhub/templates/_helpers.tpl similarity index 100% rename from binderhub-service/templates/_helpers.tpl rename to helm-chart/binderhub/templates/_helpers.tpl diff --git a/binderhub-service/templates/build-pods-docker-config/secret.yaml b/helm-chart/binderhub/templates/build-pods-docker-config/secret.yaml similarity index 100% rename from binderhub-service/templates/build-pods-docker-config/secret.yaml rename to helm-chart/binderhub/templates/build-pods-docker-config/secret.yaml diff --git a/binderhub-service/templates/deployment.yaml b/helm-chart/binderhub/templates/deployment.yaml similarity index 100% rename from binderhub-service/templates/deployment.yaml rename to helm-chart/binderhub/templates/deployment.yaml diff --git a/binderhub-service/templates/docker-api/daemonset.yaml b/helm-chart/binderhub/templates/docker-api/daemonset.yaml similarity index 100% rename from binderhub-service/templates/docker-api/daemonset.yaml rename to helm-chart/binderhub/templates/docker-api/daemonset.yaml diff --git a/binderhub-service/templates/docker-api/secret.yaml b/helm-chart/binderhub/templates/docker-api/secret.yaml similarity index 100% rename from binderhub-service/templates/docker-api/secret.yaml rename to helm-chart/binderhub/templates/docker-api/secret.yaml diff --git a/binderhub-service/templates/ingress.yaml b/helm-chart/binderhub/templates/ingress.yaml similarity index 100% rename from binderhub-service/templates/ingress.yaml rename to helm-chart/binderhub/templates/ingress.yaml diff --git a/binderhub-service/templates/role.yaml b/helm-chart/binderhub/templates/role.yaml similarity index 100% rename from binderhub-service/templates/role.yaml rename to helm-chart/binderhub/templates/role.yaml diff --git a/binderhub-service/templates/rolebinding.yaml b/helm-chart/binderhub/templates/rolebinding.yaml similarity index 100% rename from binderhub-service/templates/rolebinding.yaml rename to helm-chart/binderhub/templates/rolebinding.yaml diff --git a/binderhub-service/templates/secret.yaml b/helm-chart/binderhub/templates/secret.yaml similarity index 100% rename from binderhub-service/templates/secret.yaml rename to helm-chart/binderhub/templates/secret.yaml diff --git a/binderhub-service/templates/service.yaml b/helm-chart/binderhub/templates/service.yaml similarity index 100% rename from binderhub-service/templates/service.yaml rename to helm-chart/binderhub/templates/service.yaml diff --git a/binderhub-service/templates/serviceaccount.yaml b/helm-chart/binderhub/templates/serviceaccount.yaml similarity index 100% rename from binderhub-service/templates/serviceaccount.yaml rename to helm-chart/binderhub/templates/serviceaccount.yaml diff --git a/binderhub-service/values.schema.yaml b/helm-chart/binderhub/values.schema.yaml similarity index 100% rename from binderhub-service/values.schema.yaml rename to helm-chart/binderhub/values.schema.yaml diff --git a/binderhub-service/values.yaml b/helm-chart/binderhub/values.yaml similarity index 100% rename from binderhub-service/values.yaml rename to helm-chart/binderhub/values.yaml diff --git a/chartpress.yaml b/helm-chart/chartpress.yaml similarity index 100% rename from chartpress.yaml rename to helm-chart/chartpress.yaml diff --git a/images/binderhub-service/Dockerfile b/helm-chart/images/binderhub-service/Dockerfile similarity index 100% rename from images/binderhub-service/Dockerfile rename to helm-chart/images/binderhub-service/Dockerfile diff --git a/images/binderhub-service/README.md b/helm-chart/images/binderhub-service/README.md similarity index 100% rename from images/binderhub-service/README.md rename to helm-chart/images/binderhub-service/README.md diff --git a/images/binderhub-service/requirements.in b/helm-chart/images/binderhub-service/requirements.in similarity index 100% rename from images/binderhub-service/requirements.in rename to helm-chart/images/binderhub-service/requirements.in diff --git a/images/binderhub-service/requirements.txt b/helm-chart/images/binderhub-service/requirements.txt similarity index 100% rename from images/binderhub-service/requirements.txt rename to helm-chart/images/binderhub-service/requirements.txt diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index c2ffcd993..000000000 --- a/pyproject.toml +++ /dev/null @@ -1,83 +0,0 @@ -# NOTE: This github repository houses a Helm chart and some Python based scripts -# - not a python package. This file is only used to provide configuration -# for misc Python based utilities. -# - -# autoflake is used for autoformatting Python code -# -# ref: https://github.com/PyCQA/autoflake#readme -# -[tool.autoflake] -ignore-init-module-imports = true -remove-all-unused-imports = true -remove-duplicate-keys = true -remove-unused-variables = true - - -# isort is used for autoformatting Python code -# -# ref: https://pycqa.github.io/isort/ -# -[tool.isort] -profile = "black" - - -# black is used for autoformatting Python code -# -# ref: https://black.readthedocs.io/en/stable/ -# -[tool.black] -target_version = [ - "py310", - "py311", -] - - -# pytest is used for running Python based tests -# -# ref: https://docs.pytest.org/en/stable/ -# -[tool.pytest.ini_options] -addopts = "--verbose --color=yes --durations=10" -asyncio_mode = "auto" - - -# tbump is a tool to update version fields that we use -# to update baseVersion in chartpress.yaml as documented -# in RELEASE.md -# -# Config reference: https://github.com/your-tools/tbump#readme -# -[tool.tbump] -github_url = "https://github.com/2i2c-org/binderhub-service" - -[tool.tbump.version] -current = "0.1.0-0.dev" - -# match our prerelease prefixes -# -alpha.1 -# -beta.2 -# -0.dev -regex = ''' - (?P\d+) - \. - (?P\d+) - \. - (?P\d+) - (\- - (?P - ( - (alpha|beta|rc)\.\d+| - 0\.dev - ) - ) - )? -''' - -[tool.tbump.git] -message_template = "Bump to {new_version}" -tag_template = "{new_version}" - -[[tool.tbump.file]] -src = "chartpress.yaml" -search = 'baseVersion: "{current_version}"' diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index f3d5fee12..000000000 --- a/tests/conftest.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -conftest.py is read by pytest automatically and can be used to declare fixtures -referenced by test functions. - -ref: https://docs.pytest.org/en/latest/writing_plugins.html#conftest-py-plugins -""" diff --git a/tools/generate-json-schema.py b/tools/generate-json-schema.py deleted file mode 100755 index 929e36b32..000000000 --- a/tools/generate-json-schema.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -""" -This script reads values.schema.yaml and generates a values.schema.json that we -can package with the Helm chart, allowing helm the CLI perform validation. - -While we can directly generate a values.schema.json from values.schema.yaml, it -contains a lot of description text we use to generate our configuration -reference that isn't helpful to ship along the validation schema. Due to that, -we trim away everything that isn't needed. -""" - -import json -import os -from collections.abc import MutableMapping - -import yaml - -here_dir = os.path.abspath(os.path.dirname(__file__)) -schema_yaml = os.path.join( - here_dir, os.pardir, "binderhub-service", "values.schema.yaml" -) -values_schema_json = os.path.join( - here_dir, os.pardir, "binderhub-service", "values.schema.json" -) - - -def clean_jsonschema(d, parent_key=""): - """ - Modifies a dictionary representing a jsonschema in place to not contain - jsonschema keys not relevant for a values.schema.json file solely for use by - the helm CLI. - """ - JSONSCHEMA_KEYS_TO_REMOVE = {"description"} - - # start by cleaning up the current level - for k in set.intersection(JSONSCHEMA_KEYS_TO_REMOVE, set(d.keys())): - del d[k] - - # Recursively cleanup nested levels, bypassing one level where there could - # be a valid Helm chart configuration named just like the jsonschema - # specific key to remove. - if "properties" in d: - for k, v in d["properties"].items(): - if isinstance(v, MutableMapping): - clean_jsonschema(v, k) - - -def run(): - # Using these sets, we can validate further manually by printing the results - # of set operations. - with open(schema_yaml) as f: - schema = yaml.safe_load(f) - - # Drop what isn't relevant for a values.schema.json file packaged with the - # Helm chart, such as the description keys only relevant for our - # configuration reference. - clean_jsonschema(schema) - - # dump schema to values.schema.json - with open(values_schema_json, "w") as f: - json.dump(schema, f) - - print("binderhub-service/values.schema.json created") - - -run() diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml deleted file mode 100644 index 7143819e8..000000000 --- a/tools/templates/lint-and-validate-values.yaml +++ /dev/null @@ -1,95 +0,0 @@ -# General configuration -# ----------------------------------------------------------------------------- -# -nameOverride: "" -fullnameOverride: "" -global: {} -custom: {} - -# Resources for the BinderHub created build pods -# ----------------------------------------------------------------------------- -# -buildPodsDockerConfig: - dummy: dummy-value -buildPodsRegistryCredentials: - server: "quay.io" - username: "dummy-username" - password: "dummy-password" - -# Deployment resource -# ----------------------------------------------------------------------------- -# -image: - repository: quay.io/2i2c/binderhub-service - tag: "set-by-chartpress" -extraCredentials: - googleServiceAccountKey: | - { - "type": "service_account", - "project_id": "PROJECT_ID", - "private_key_id": "KEY_ID", - "private_key": "-----BEGIN PRIVATE KEY-----\nPRIVATE_KEY\n-----END PRIVATE KEY-----\n", - "client_email": "SERVICE_ACCOUNT_EMAIL", - "client_id": "CLIENT_ID", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://accounts.google.com/o/oauth2/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/SERVICE_ACCOUNT_EMAIL" - } -extraEnv: - - name: HELM_RELEASE_NAME - value: "{{ .Release.Name }}" - -# RBAC resources -# ----------------------------------------------------------------------------- -# -rbac: - create: true - -# ServiceAccount resource -# ----------------------------------------------------------------------------- -# -serviceAccount: - create: true - name: "" - -# Service resource -# ----------------------------------------------------------------------------- -# -service: - type: ClusterIP - port: 80 - -# Ingress resource -# ----------------------------------------------------------------------------- -# -ingress: - enabled: true - ingressClassName: mock-ingress-class-name - hosts: - - mocked1.domain.name - - mocked2.domain.name - pathSuffix: dummy-pathSuffix - pathType: ImplementationSpecific - tls: - - secretName: tls - hosts: - - mocked1.domain.name - - mocked2.domain.name - -# DaemonSet resource - docker-api -# ----------------------------------------------------------------------------- -# -dockerApi: - image: - repository: docker.io/library/docker - tag: "23.0.5-dind" - securityContext: - privileged: true - runAsUser: 0 - extraFiles: - daemon-json: - mountPath: /etc/docker/daemon.json - data: - debug: true - insecure-registries: [localhost:5000] diff --git a/tools/templates/lint-and-validate.py b/tools/templates/lint-and-validate.py deleted file mode 100755 index 6d574d611..000000000 --- a/tools/templates/lint-and-validate.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -""" -Lints and validates the chart's template files and their rendered output without -any cluster interaction. For this script to function, you must install yamllint. - -USAGE: - - tools/templates/lint-and-validate.py - -DEPENDENCIES: - -yamllint: https://github.com/adrienverge/yamllint - - pip install yamllint -""" - -import argparse -import os -import pipes -import subprocess -import sys - -os.chdir(os.path.dirname(sys.argv[0])) - - -def check_call(cmd, **kwargs): - """Run a subcommand and exit if it fails""" - try: - subprocess.check_call(cmd, **kwargs) - except subprocess.CalledProcessError as e: - print( - "`{}` exited with status {}".format( - " ".join(map(pipes.quote, cmd)), - e.returncode, - ), - file=sys.stderr, - ) - sys.exit(e.returncode) - - -def lint(yamllint_config, values, output_dir, strict, debug): - """Calls `helm lint`, `helm template`, and `yamllint`.""" - - print("### Clearing output directory") - check_call(["mkdir", "-p", output_dir]) - check_call(["rm", "-rf", f"{output_dir}/*"]) - - print("### Linting started") - print("### 1/3 - helm lint: lint helm templates") - helm_lint_cmd = ["helm", "lint", "../../binderhub-service", f"--values={values}"] - if strict: - helm_lint_cmd.append("--strict") - if debug: - helm_lint_cmd.append("--debug") - check_call(helm_lint_cmd) - - print("### 2/3 - helm template: generate kubernetes resources") - helm_template_cmd = [ - "helm", - "template", - "../../binderhub-service", - f"--values={values}", - f"--output-dir={output_dir}", - ] - if debug: - helm_template_cmd.append("--debug") - check_call(helm_template_cmd) - - print("### 3/3 - yamllint: yaml lint generated kubernetes resources") - check_call(["yamllint", "-c", yamllint_config, output_dir]) - - print() - print( - "### Linting and validation of helm templates and generated kubernetes resources OK!" - ) - - -if __name__ == "__main__": - argparser = argparse.ArgumentParser() - argparser.add_argument( - "--debug", - action="store_true", - help="Run helm lint and helm template with the --debug flag", - ) - argparser.add_argument( - "--strict", - action="store_true", - help="Run helm lint with the --strict flag", - ) - argparser.add_argument( - "--values", - default="lint-and-validate-values.yaml", - help="Specify Helm values in a YAML file (can specify multiple)", - ) - argparser.add_argument( - "--output-dir", - default="rendered-templates", - help="Output directory for the rendered templates. Warning: content in this will be wiped.", - ) - argparser.add_argument( - "--yamllint-config", - default="yamllint-config.yaml", - help="Specify a yamllint config", - ) - - args = argparser.parse_args() - - lint( - args.yamllint_config, - args.values, - args.output_dir, - args.strict, - args.debug, - ) diff --git a/tools/templates/yamllint-config.yaml b/tools/templates/yamllint-config.yaml deleted file mode 100644 index ade69db12..000000000 --- a/tools/templates/yamllint-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -rules: - indentation: - spaces: 2 # Default: consistent - indent-sequences: whatever # Default true (*) - line-length: disable # Default: { max: 80, ... } - -# (*) toYaml's emitted sequences/lists will have no indentation and we have no -# control over it, so we compromise by setting "whatever". diff --git a/tools/validate-against-schema.py b/tools/validate-against-schema.py deleted file mode 100755 index 4e12cf5e6..000000000 --- a/tools/validate-against-schema.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -import os - -import jsonschema -import yaml - -here_dir = os.path.abspath(os.path.dirname(__file__)) -schema_yaml = os.path.join( - here_dir, os.pardir, "binderhub-service", "values.schema.yaml" -) -values_yaml = os.path.join(here_dir, os.pardir, "binderhub-service", "values.yaml") -lint_and_validate_values_yaml = os.path.join( - here_dir, "templates", "lint-and-validate-values.yaml" -) - -with open(schema_yaml) as f: - schema = yaml.safe_load(f) -with open(values_yaml) as f: - values = yaml.safe_load(f) -with open(lint_and_validate_values_yaml) as f: - lint_and_validate_values = yaml.safe_load(f) - -# Validate values.yaml against schema -print("Validating values.yaml against values.schema.yaml...") -jsonschema.validate(values, schema) -print("OK!") -print() - -# Validate lint-and-validate-values.yaml against schema -print("Validating lint-and-validate-values.yaml against values.schema.yaml...") -jsonschema.validate(lint_and_validate_values, schema) -print("OK!") From 072008517e388413f7b12505c6308fc868d95114 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Fri, 7 Mar 2025 13:47:51 -0800 Subject: [PATCH 194/200] Rename binderhub-service to binderhub --- .github/workflows/test-chart.yaml | 16 +++--- dev-config.yaml | 2 +- helm-chart/binderhub/Chart.yaml | 12 ++--- helm-chart/binderhub/templates/NOTES.txt | 19 +++---- .../templates/_helpers-extra-files.tpl | 22 ++++---- .../binderhub/templates/_helpers-labels.tpl | 6 +-- .../binderhub/templates/_helpers-names.tpl | 32 +++++------ helm-chart/binderhub/templates/_helpers.tpl | 4 +- .../build-pods-docker-config/secret.yaml | 4 +- .../binderhub/templates/deployment.yaml | 14 ++--- .../templates/docker-api/daemonset.yaml | 10 ++-- .../templates/docker-api/secret.yaml | 8 +-- helm-chart/binderhub/templates/ingress.yaml | 6 +-- helm-chart/binderhub/templates/role.yaml | 4 +- .../binderhub/templates/rolebinding.yaml | 8 +-- helm-chart/binderhub/templates/secret.yaml | 8 +-- helm-chart/binderhub/templates/service.yaml | 6 +-- .../binderhub/templates/serviceaccount.yaml | 4 +- helm-chart/binderhub/values.schema.yaml | 2 +- helm-chart/binderhub/values.yaml | 6 +-- helm-chart/chartpress.yaml | 12 ++--- .../Dockerfile | 6 +-- .../README.md | 2 +- .../requirements.in | 11 ++-- .../requirements.txt | 53 +++++++++---------- 25 files changed, 135 insertions(+), 142 deletions(-) rename helm-chart/images/{binderhub-service => binderhub}/Dockerfile (94%) rename helm-chart/images/{binderhub-service => binderhub}/README.md (80%) rename helm-chart/images/{binderhub-service => binderhub}/requirements.in (54%) rename helm-chart/images/{binderhub-service => binderhub}/requirements.txt (82%) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index f582253a6..99f035b52 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -70,10 +70,10 @@ jobs: run: tools/generate-json-schema.py - name: Helm lint (values.yaml) - run: helm lint ./binderhub-service + run: helm lint helm-chart/binderhub - name: Helm lint (lint-and-validate-values.yaml) - run: helm lint ./binderhub-service --values tools/templates/lint-and-validate-values.yaml + run: helm lint helm-chart/binderhub --values tools/templates/lint-and-validate-values.yaml # FIXME: We can probably emit a GitHub workflow warning if these fail # instead having them show as green without a warning or similar @@ -82,11 +82,11 @@ jobs: # are several warnings that we should ignore. # - name: Helm lint --strict (values.yaml) - run: helm lint --strict ./binderhub-service + run: helm lint --strict helm-chart/binderhub continue-on-error: true - name: Helm lint --strict (lint-and-validate-values.yaml) - run: helm lint --strict ./binderhub-service + run: helm lint --strict helm-chart/binderhub continue-on-error: true test: @@ -173,14 +173,14 @@ jobs: # dedicated lint-and-validate-values.yaml config. - name: "Helm template --validate (with lint and validate config)" run: | - helm template --validate binderhub-service ./binderhub-service \ + helm template --validate binderhub helm-chart/binderhub \ --values tools/templates/lint-and-validate-values.yaml - name: Install local chart run: | - helm upgrade --install binderhub-service ./binderhub-service \ + helm upgrade --install binderhub helm-chart/binderhub \ --values dev-config.yaml \ - --set config.BinderHub.image_prefix=$LOCAL_REGISTRY_HOST/binderhub-service/ \ + --set config.BinderHub.image_prefix=$LOCAL_REGISTRY_HOST/binderhub/ \ --set config.DockerRegistry.url=http://$LOCAL_REGISTRY_HOST \ --set buildPodsRegistryCredentials.server=http://$LOCAL_REGISTRY_HOST \ --set dockerApi.extraFiles.daemon-json.data.insecure-registries[0]=$LOCAL_REGISTRY_HOST @@ -199,4 +199,4 @@ jobs: - uses: jupyterhub/action-k8s-namespace-report@v1 if: always() with: - important-workloads: deploy/binderhub-service daemonset/binderhub-service-docker-api + important-workloads: deploy/binderhub daemonset/binderhub-docker-api diff --git a/dev-config.yaml b/dev-config.yaml index 20b219a3e..7d03d2086 100644 --- a/dev-config.yaml +++ b/dev-config.yaml @@ -18,7 +18,7 @@ config: # log_level isn't enabled to avoid overwhelming the reader with info # log_level: DEBUG use_registry: true - image_prefix: localhost:5000/binderhub-service/ + image_prefix: localhost:5000/binderhub/ buildPodsRegistryCredentials: server: http://localhost:5000 username: "" diff --git a/helm-chart/binderhub/Chart.yaml b/helm-chart/binderhub/Chart.yaml index d96ca6d88..d7c6d527a 100644 --- a/helm-chart/binderhub/Chart.yaml +++ b/helm-chart/binderhub/Chart.yaml @@ -1,14 +1,14 @@ # Chart.yaml v2 reference: https://helm.sh/docs/topics/charts/#the-chartyaml-file apiVersion: v2 -name: binderhub-service -version: 0.0.1-set.by.chartpress +name: binderhub +version: 0.2.0 # FIXME: appVersion should represent the version of binderhub in -# images/binderhub-service/requirements.txt +# images/binderhub/requirements.txt # appVersion: "1.0.0" description: A BinderHub installation separate from JupyterHub -keywords: [binderhub, binderhub-service, repo2docker, jupyterhub, jupyter] -home: https://2i2c.org/binderhub-service -sources: [https://github.com/2i2c-org/binderhub-service] +keywords: [binderhub, repo2docker, jupyterhub, jupyter] +home: https://github.com/jupyterhub/binderhub +sources: [https://github.com/jupyterhub/binderhub] icon: https://binderhub.readthedocs.io/en/latest/_static/logo.png kubeVersion: ">=1.27.0-0" diff --git a/helm-chart/binderhub/templates/NOTES.txt b/helm-chart/binderhub/templates/NOTES.txt index 876e2f7a9..c362e806b 100644 --- a/helm-chart/binderhub/templates/NOTES.txt +++ b/helm-chart/binderhub/templates/NOTES.txt @@ -1,16 +1,11 @@ {{- /* Generated with https://patorjk.com/software/taag/#p=display&h=0&f=Slant&t=BinderHub */}} -. ____ _ __ __ __ __ - / __ ) (_) ____ ____/ / ___ _____ / / / / __ __ / /_ +. ____ _ __ __ __ __ + / __ ) (_) ____ ____/ / ___ _____ / / / / __ __ / /_ / __ | / / / __ \ / __ / / _ \ / ___/ / /_/ / / / / / / __ \ / /_/ / / / / / / / / /_/ / / __/ / / / __ / / /_/ / / /_/ / -/_____/ /_/ /_/ /_/ \__,_/ \___/ /_/ /_/ /_/ \__,_/ /_.___/ - _____ _ - / ___/ ___ _____ _ __ (_) _____ ___ - \__ \ / _ \ / ___/ | | / / / / / ___/ / _ \ - ___/ / / __/ / / | |/ / / / / /__ / __/ - /____/ \___/ /_/ |___/ /_/ \___/ \___/ +/_____/ /_/ /_/ /_/ \__,_/ \___/ /_/ /_/ /_/ \__,_/ /_.___/ -You have successfully installed the BinderHub Service Helm chart! +You have successfully installed the BinderHub Helm chart! ### Installation info @@ -18,12 +13,12 @@ You have successfully installed the BinderHub Service Helm chart! - Helm release name: {{ .Release.Name }} - Helm chart version: {{ .Chart.Version }} - BinderHub version: {{ .Chart.AppVersion }} - - Python packages: See https://github.com/2i2c-org/binderhub-service/blob/{{ include "binderhub-service.chart-version-to-git-ref" .Chart.Version }}/images/binderhub-service/requirements.txt + - Python packages: See https://github.com/jupyterhub/binderhub/blob/{{ include "binderhub.chart-version-to-git-ref" .Chart.Version }}/helm-chart/images/binderhub/requirements.txt ### Followup links - - Documentation: https://2i2c.org/binderhub-service - - Issue tracking: https://github.com/2i2c-org/binderhub-service/issues + - Documentation: https://binderhub.readthedocs.io/en/latest/ + - Issue tracking: https://github.com/jupyterhub/binderhub/issues ### Post-installation checklist diff --git a/helm-chart/binderhub/templates/_helpers-extra-files.tpl b/helm-chart/binderhub/templates/_helpers-extra-files.tpl index 09cdf607c..3b4262df1 100644 --- a/helm-chart/binderhub/templates/_helpers-extra-files.tpl +++ b/helm-chart/binderhub/templates/_helpers-extra-files.tpl @@ -1,28 +1,28 @@ {{- /* - binderhub-service.extraFiles.data: + binderhub.extraFiles.data: Renders content for a k8s Secret's data field, coming from extraFiles with binaryData entries. */}} -{{- define "binderhub-service.extraFiles.data.withNewLineSuffix" -}} +{{- define "binderhub.extraFiles.data.withNewLineSuffix" -}} {{- range $file_key, $file_details := . }} - {{- include "binderhub-service.extraFiles.validate-file" (list $file_key $file_details) }} + {{- include "binderhub.extraFiles.validate-file" (list $file_key $file_details) }} {{- if $file_details.binaryData }} {{- $file_key | quote }}: {{ $file_details.binaryData | nospace | quote }}{{ println }} {{- end }} {{- end }} {{- end }} -{{- define "binderhub-service.extraFiles.data" -}} - {{- include "binderhub-service.extraFiles.data.withNewLineSuffix" . | trimSuffix "\n" }} +{{- define "binderhub.extraFiles.data" -}} + {{- include "binderhub.extraFiles.data.withNewLineSuffix" . | trimSuffix "\n" }} {{- end }} {{- /* - binderhub-service.extraFiles.stringData: + binderhub.extraFiles.stringData: Renders content for a k8s Secret's stringData field, coming from extraFiles with either data or stringData entries. */}} -{{- define "binderhub-service.extraFiles.stringData.withNewLineSuffix" -}} +{{- define "binderhub.extraFiles.stringData.withNewLineSuffix" -}} {{- range $file_key, $file_details := . }} - {{- include "binderhub-service.extraFiles.validate-file" (list $file_key $file_details) }} + {{- include "binderhub.extraFiles.validate-file" (list $file_key $file_details) }} {{- $file_name := $file_details.mountPath | base }} {{- if $file_details.stringData }} {{- $file_key | quote }}: | @@ -42,11 +42,11 @@ {{- end }} {{- end }} {{- end }} -{{- define "binderhub-service.extraFiles.stringData" -}} - {{- include "binderhub-service.extraFiles.stringData.withNewLineSuffix" . | trimSuffix "\n" }} +{{- define "binderhub.extraFiles.stringData" -}} + {{- include "binderhub.extraFiles.stringData.withNewLineSuffix" . | trimSuffix "\n" }} {{- end }} -{{- define "binderhub-service.extraFiles.validate-file" -}} +{{- define "binderhub.extraFiles.validate-file" -}} {{- $file_key := index . 0 }} {{- $file_details := index . 1 }} diff --git a/helm-chart/binderhub/templates/_helpers-labels.tpl b/helm-chart/binderhub/templates/_helpers-labels.tpl index d500be365..805bfe5ce 100644 --- a/helm-chart/binderhub/templates/_helpers-labels.tpl +++ b/helm-chart/binderhub/templates/_helpers-labels.tpl @@ -1,9 +1,9 @@ {{- /* Common labels */}} -{{- define "binderhub-service.labels" -}} +{{- define "binderhub.labels" -}} helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{ include "binderhub-service.selectorLabels" . }} +{{ include "binderhub.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -13,7 +13,7 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{- /* Selector labels */}} -{{- define "binderhub-service.selectorLabels" -}} +{{- define "binderhub.selectorLabels" -}} app.kubernetes.io/name: {{ .Values.nameOverride | default .Chart.Name | trunc 63 | trimSuffix "-" }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} diff --git a/helm-chart/binderhub/templates/_helpers-names.tpl b/helm-chart/binderhub/templates/_helpers-names.tpl index c84bdd53d..ebc0d0fe0 100644 --- a/helm-chart/binderhub/templates/_helpers-names.tpl +++ b/helm-chart/binderhub/templates/_helpers-names.tpl @@ -17,7 +17,7 @@ Renders to a prefix for the chart's resource names. This prefix is assumed to make the resource name cluster unique. */}} -{{- define "binderhub-service.fullname" -}} +{{- define "binderhub.fullname" -}} {{- /* We have implemented a trick to allow a parent chart depending on this chart to call these named templates. @@ -34,7 +34,7 @@ */}} {{- $fullname_override := .Values.fullnameOverride }} {{- $name_override := .Values.nameOverride }} - {{- if ne .Chart.Name "binderhub-service" }} + {{- if ne .Chart.Name "binderhub" }} {{- if .Values.jupyterhub }} {{- $fullname_override = .Values.jupyterhub.fullnameOverride }} {{- $name_override = .Values.jupyterhub.nameOverride }} @@ -57,9 +57,9 @@ Renders to a blank string or if the fullname template is truthy renders to it with an appended dash. */}} -{{- define "binderhub-service.fullname.dash" -}} - {{- if (include "binderhub-service.fullname" .) }} - {{- include "binderhub-service.fullname" . }}- +{{- define "binderhub.fullname.dash" -}} + {{- if (include "binderhub.fullname" .) }} + {{- include "binderhub.fullname" . }}- {{- end }} {{- end }} @@ -70,36 +70,36 @@ */}} {{- /* binderhub resources' default name */}} -{{- define "binderhub-service.binderhub.fullname" -}} - {{- include "binderhub-service.fullname.dash" . }}binderhub +{{- define "binderhub.binderhub.fullname" -}} + {{- include "binderhub.fullname.dash" . }}binderhub {{- end }} {{- /* binderhub's ServiceAccount name */}} -{{- define "binderhub-service.binderhub.serviceaccount.fullname" -}} +{{- define "binderhub.binderhub.serviceaccount.fullname" -}} {{- if .Values.serviceAccount.create }} - {{- .Values.serviceAccount.name | default (include "binderhub-service.binderhub.fullname" .) }} + {{- .Values.serviceAccount.name | default (include "binderhub.binderhub.fullname" .) }} {{- else }} {{- .Values.serviceAccount.name }} {{- end }} {{- end }} {{- /* binderhub's Ingress name */}} -{{- define "binderhub-service.binderhub.ingress.fullname" -}} - {{- if (include "binderhub-service.fullname" .) }} - {{- include "binderhub-service.fullname" . }} +{{- define "binderhub.binderhub.ingress.fullname" -}} + {{- if (include "binderhub.fullname" .) }} + {{- include "binderhub.fullname" . }} {{- else -}} binderhub {{- end }} {{- end }} {{- /* docker-api resources' default name */}} -{{- define "binderhub-service.docker-api.fullname" -}} - {{- include "binderhub-service.fullname.dash" . }}docker-api +{{- define "binderhub.docker-api.fullname" -}} + {{- include "binderhub.fullname.dash" . }}docker-api {{- end }} {{- /* build-pods-docker-config name */}} -{{- define "binderhub-service.build-pods-docker-config.fullname" -}} - {{- include "binderhub-service.fullname.dash" . }}build-pods-docker-config +{{- define "binderhub.build-pods-docker-config.fullname" -}} + {{- include "binderhub.fullname.dash" . }}build-pods-docker-config {{- end }} diff --git a/helm-chart/binderhub/templates/_helpers.tpl b/helm-chart/binderhub/templates/_helpers.tpl index a5762b721..bc1584d58 100644 --- a/helm-chart/binderhub/templates/_helpers.tpl +++ b/helm-chart/binderhub/templates/_helpers.tpl @@ -1,5 +1,5 @@ {{- /* - binderhub-service.chart-version-to-git-ref: + binderhub.chart-version-to-git-ref: Renders a valid git reference from a chartpress generated version string. In practice, either a git tag or a git commit hash will be returned. @@ -13,6 +13,6 @@ - The regular expression is in golang syntax, but \d had to become \\d for example. */}} -{{- define "binderhub-service.chart-version-to-git-ref" -}} +{{- define "binderhub.chart-version-to-git-ref" -}} {{- regexReplaceAll ".*\\.git\\.\\d+\\.h(.*)" . "${1}" }} {{- end }} diff --git a/helm-chart/binderhub/templates/build-pods-docker-config/secret.yaml b/helm-chart/binderhub/templates/build-pods-docker-config/secret.yaml index 410b7a1f1..2bdc4f90f 100644 --- a/helm-chart/binderhub/templates/build-pods-docker-config/secret.yaml +++ b/helm-chart/binderhub/templates/build-pods-docker-config/secret.yaml @@ -7,9 +7,9 @@ kind: Secret apiVersion: v1 metadata: - name: {{ include "binderhub-service.build-pods-docker-config.fullname" . }} + name: {{ include "binderhub.build-pods-docker-config.fullname" . }} labels: - {{- include "binderhub-service.labels" . | nindent 4 }} + {{- include "binderhub.labels" . | nindent 4 }} type: Opaque stringData: # config.json refers to docker config that should house credentials for the diff --git a/helm-chart/binderhub/templates/deployment.yaml b/helm-chart/binderhub/templates/deployment.yaml index 895e02b6c..32a674083 100644 --- a/helm-chart/binderhub/templates/deployment.yaml +++ b/helm-chart/binderhub/templates/deployment.yaml @@ -1,14 +1,14 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "binderhub-service.binderhub.fullname" . }} + name: {{ include "binderhub.binderhub.fullname" . }} labels: - {{- include "binderhub-service.labels" . | nindent 4 }} + {{- include "binderhub.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicas }} selector: matchLabels: - {{- include "binderhub-service.selectorLabels" . | nindent 6 }} + {{- include "binderhub.selectorLabels" . | nindent 6 }} app.kubernetes.io/component: binderhub template: metadata: @@ -18,13 +18,13 @@ spec: {{- . | toYaml | nindent 8 }} {{- end }} labels: - {{- include "binderhub-service.selectorLabels" . | nindent 8 }} + {{- include "binderhub.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: binderhub spec: volumes: - name: secret secret: - secretName: {{ include "binderhub-service.binderhub.fullname" . }} + secretName: {{ include "binderhub.binderhub.fullname" . }} {{- with .Values.extraVolumes }} {{- tpl (. | toYaml) $ | nindent 8 }} {{- end }} @@ -46,7 +46,7 @@ spec: {{- end }} env: - name: PUSH_SECRET_NAME - value: {{ include "binderhub-service.build-pods-docker-config.fullname" . }} + value: {{ include "binderhub.build-pods-docker-config.fullname" . }} - name: HELM_RELEASE_NAME value: {{ .Release.Name }} # Namespace build pods should be placed in @@ -80,7 +80,7 @@ spec: imagePullSecrets: {{- . | toYaml | nindent 8 }} {{- end }} - {{- with include "binderhub-service.binderhub.serviceaccount.fullname" . }} + {{- with include "binderhub.binderhub.serviceaccount.fullname" . }} serviceAccountName: {{ . }} {{- end }} {{- with .Values.podSecurityContext }} diff --git a/helm-chart/binderhub/templates/docker-api/daemonset.yaml b/helm-chart/binderhub/templates/docker-api/daemonset.yaml index 3427b8014..91e7dfd53 100644 --- a/helm-chart/binderhub/templates/docker-api/daemonset.yaml +++ b/helm-chart/binderhub/templates/docker-api/daemonset.yaml @@ -1,19 +1,19 @@ apiVersion: apps/v1 kind: DaemonSet metadata: - name: {{ include "binderhub-service.docker-api.fullname" . }} + name: {{ include "binderhub.docker-api.fullname" . }} labels: - {{- include "binderhub-service.labels" . | nindent 4 }} + {{- include "binderhub.labels" . | nindent 4 }} app.kubernetes.io/component: docker-api spec: selector: matchLabels: - {{- include "binderhub-service.selectorLabels" . | nindent 6 }} + {{- include "binderhub.selectorLabels" . | nindent 6 }} app.kubernetes.io/component: docker-api template: metadata: labels: - {{- include "binderhub-service.selectorLabels" . | nindent 8 }} + {{- include "binderhub.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: docker-api {{- with .Values.podAnnotations }} annotations: @@ -57,7 +57,7 @@ spec: {{- if .Values.dockerApi.extraFiles }} - name: files secret: - secretName: {{ include "binderhub-service.docker-api.fullname" . }} + secretName: {{ include "binderhub.docker-api.fullname" . }} items: {{- range $file_key, $file_details := .Values.dockerApi.extraFiles }} - key: {{ $file_key | quote }} diff --git a/helm-chart/binderhub/templates/docker-api/secret.yaml b/helm-chart/binderhub/templates/docker-api/secret.yaml index 258369dd7..38570fc7d 100644 --- a/helm-chart/binderhub/templates/docker-api/secret.yaml +++ b/helm-chart/binderhub/templates/docker-api/secret.yaml @@ -2,15 +2,15 @@ kind: Secret apiVersion: v1 metadata: - name: {{ include "binderhub-service.docker-api.fullname" . }} + name: {{ include "binderhub.docker-api.fullname" . }} labels: - {{- include "binderhub-service.labels" . | nindent 4 }} + {{- include "binderhub.labels" . | nindent 4 }} type: Opaque -{{- with include "binderhub-service.extraFiles.data" .Values.dockerApi.extraFiles }} +{{- with include "binderhub.extraFiles.data" .Values.dockerApi.extraFiles }} data: {{- . | nindent 2 }} {{- end }} -{{- with include "binderhub-service.extraFiles.stringData" .Values.dockerApi.extraFiles }} +{{- with include "binderhub.extraFiles.stringData" .Values.dockerApi.extraFiles }} stringData: {{- . | nindent 2 }} {{- end }} diff --git a/helm-chart/binderhub/templates/ingress.yaml b/helm-chart/binderhub/templates/ingress.yaml index 510fbe3d6..5126b3d1f 100644 --- a/helm-chart/binderhub/templates/ingress.yaml +++ b/helm-chart/binderhub/templates/ingress.yaml @@ -2,9 +2,9 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: {{ include "binderhub-service.binderhub.ingress.fullname" . }} + name: {{ include "binderhub.binderhub.ingress.fullname" . }} labels: - {{- include "binderhub-service.labels" . | nindent 4 }} + {{- include "binderhub.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- . | toYaml | nindent 4 }} @@ -21,7 +21,7 @@ spec: pathType: {{ $.Values.ingress.pathType }} backend: service: - name: {{ include "binderhub-service.binderhub.fullname" $ }} + name: {{ include "binderhub.binderhub.fullname" $ }} port: name: http {{- if $host }} diff --git a/helm-chart/binderhub/templates/role.yaml b/helm-chart/binderhub/templates/role.yaml index 40323383c..b379c4fae 100644 --- a/helm-chart/binderhub/templates/role.yaml +++ b/helm-chart/binderhub/templates/role.yaml @@ -2,9 +2,9 @@ kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: {{ include "binderhub-service.binderhub.fullname" . }} + name: {{ include "binderhub.binderhub.fullname" . }} labels: - {{- include "binderhub-service.labels" . | nindent 4 }} + {{- include "binderhub.labels" . | nindent 4 }} rules: - apiGroups: [""] resources: ["pods"] diff --git a/helm-chart/binderhub/templates/rolebinding.yaml b/helm-chart/binderhub/templates/rolebinding.yaml index 067b1dba7..8c2387ba4 100644 --- a/helm-chart/binderhub/templates/rolebinding.yaml +++ b/helm-chart/binderhub/templates/rolebinding.yaml @@ -2,15 +2,15 @@ kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: {{ include "binderhub-service.binderhub.fullname" . }} + name: {{ include "binderhub.binderhub.fullname" . }} labels: - {{- include "binderhub-service.labels" . | nindent 4 }} + {{- include "binderhub.labels" . | nindent 4 }} subjects: - kind: ServiceAccount namespace: {{ .Release.Namespace }} - name: {{ include "binderhub-service.binderhub.serviceaccount.fullname" . }} + name: {{ include "binderhub.binderhub.serviceaccount.fullname" . }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role - name: {{ include "binderhub-service.binderhub.fullname" . }} + name: {{ include "binderhub.binderhub.fullname" . }} {{- end }} diff --git a/helm-chart/binderhub/templates/secret.yaml b/helm-chart/binderhub/templates/secret.yaml index 123d7ae7b..3fa984e5c 100644 --- a/helm-chart/binderhub/templates/secret.yaml +++ b/helm-chart/binderhub/templates/secret.yaml @@ -1,17 +1,17 @@ {{- /* - Changes to this rendered manifest triggers a restart of the binderhub-service + Changes to this rendered manifest triggers a restart of the binderhub pod as the pod specification includes an annotation with a checksum of this. */ -}} kind: Secret apiVersion: v1 metadata: - name: {{ include "binderhub-service.binderhub.fullname" . }} + name: {{ include "binderhub.binderhub.fullname" . }} labels: - {{- include "binderhub-service.labels" . | nindent 4 }} + {{- include "binderhub.labels" . | nindent 4 }} type: Opaque stringData: {{- /* - To restart the binderhub-service pod only when relevant, we pick out the + To restart the binderhub pod only when relevant, we pick out the chart configuration actually consumed in the mounted binderhub_config.py file. */}} diff --git a/helm-chart/binderhub/templates/service.yaml b/helm-chart/binderhub/templates/service.yaml index f3ba4b042..6eb3a46df 100644 --- a/helm-chart/binderhub/templates/service.yaml +++ b/helm-chart/binderhub/templates/service.yaml @@ -1,8 +1,8 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "binderhub-service.binderhub.fullname" . }} - labels: {{- include "binderhub-service.labels" . | nindent 4 }} + name: {{ include "binderhub.binderhub.fullname" . }} + labels: {{- include "binderhub.labels" . | nindent 4 }} {{- with .Values.service.annotations }} annotations: {{- . | toYaml | nindent 4 }} @@ -16,4 +16,4 @@ spec: {{- with .Values.service.nodePort }} nodePort: {{ . }} {{- end }} - selector: {{- include "binderhub-service.selectorLabels" . | nindent 4 }} + selector: {{- include "binderhub.selectorLabels" . | nindent 4 }} diff --git a/helm-chart/binderhub/templates/serviceaccount.yaml b/helm-chart/binderhub/templates/serviceaccount.yaml index 0ba471204..6280c3112 100644 --- a/helm-chart/binderhub/templates/serviceaccount.yaml +++ b/helm-chart/binderhub/templates/serviceaccount.yaml @@ -2,9 +2,9 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ include "binderhub-service.binderhub.serviceaccount.fullname" . }} + name: {{ include "binderhub.binderhub.serviceaccount.fullname" . }} labels: - {{- include "binderhub-service.labels" . | nindent 4 }} + {{- include "binderhub.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- . | toYaml | nindent 4 }} diff --git a/helm-chart/binderhub/values.schema.yaml b/helm-chart/binderhub/values.schema.yaml index 88ccd7ea2..9b8dbf8e7 100644 --- a/helm-chart/binderhub/values.schema.yaml +++ b/helm-chart/binderhub/values.schema.yaml @@ -35,7 +35,7 @@ properties: enabled: type: boolean description: | - Configuration flag for charts depending on binderhub-service to toggle installing it. + Configuration flag for charts depending on binderhub to toggle installing it. # General configuration # --------------------------------------------------------------------------- diff --git a/helm-chart/binderhub/values.yaml b/helm-chart/binderhub/values.yaml index bd688b90b..475055fbe 100644 --- a/helm-chart/binderhub/values.yaml +++ b/helm-chart/binderhub/values.yaml @@ -40,13 +40,13 @@ config: port: 8585 enable_api_only_mode: true extraConfig: - # binderhub-service creates a k8s Secret with a docker config.json file + # binderhub creates a k8s Secret with a docker config.json file # including registry credentials. - binderhub-service-01-build-pods-docker-config: | + binderhub-01-build-pods-docker-config: | import os c.KubernetesBuildExecutor.push_secret = os.environ["PUSH_SECRET_NAME"] - binderhub-service-02-set-docker-api: | + binderhub-02-set-docker-api: | import os helm_release_name = os.environ["HELM_RELEASE_NAME"] namespace = os.environ["NAMESPACE"] diff --git a/helm-chart/chartpress.yaml b/helm-chart/chartpress.yaml index 4eb593efe..fee005c6b 100644 --- a/helm-chart/chartpress.yaml +++ b/helm-chart/chartpress.yaml @@ -9,15 +9,15 @@ # https://github.com/jupyterhub/chartpress # charts: - - name: binderhub-service + - name: binderhub imagePrefix: quay.io/2i2c/ - baseVersion: "0.1.0-0.dev" + baseVersion: "0.3.0-0.dev" repo: - git: 2i2c-org/binderhub-service - published: https://2i2c.org/binderhub-service + git: jupyterhub/helm-chart + published: https://jupyterhub.github.io/helm-chart images: - # binderhub-service, the container where the binderhub the Python + # binderhub, the container where the binderhub the Python # application is running. - binderhub-service: + binderhub: valuesPath: image diff --git a/helm-chart/images/binderhub-service/Dockerfile b/helm-chart/images/binderhub/Dockerfile similarity index 94% rename from helm-chart/images/binderhub-service/Dockerfile rename to helm-chart/images/binderhub/Dockerfile index 1f8f5a4b6..6cdb697a6 100644 --- a/helm-chart/images/binderhub-service/Dockerfile +++ b/helm-chart/images/binderhub/Dockerfile @@ -8,7 +8,7 @@ # # NOTE: If the image version is updated, also update it in ci/refreeze! # -FROM python:3.11-bookworm as build-stage +FROM python:3.13-bookworm as build-stage RUN apt-get update \ && apt-get install --yes \ @@ -34,9 +34,9 @@ RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ # The final stage # --------------- -# This stage is built and published as quay.io/2i2c/binderhub-service. +# This stage is built and published as quay.io/2i2c/binderhub. # -FROM python:3.11-slim-bookworm +FROM python:3.13-slim-bookworm ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update \ diff --git a/helm-chart/images/binderhub-service/README.md b/helm-chart/images/binderhub/README.md similarity index 80% rename from helm-chart/images/binderhub-service/README.md rename to helm-chart/images/binderhub/README.md index a830d0b59..7a1471254 100644 --- a/helm-chart/images/binderhub-service/README.md +++ b/helm-chart/images/binderhub/README.md @@ -8,4 +8,4 @@ requirements.in file using [`pip-compile`](https://pip-tools.readthedocs.io). ## How to update requirements.txt Use the "Run workflow" button at -https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml. +https://github.com/jupyterhub/binderhub/actions/workflows/watch-dependencies.yaml. diff --git a/helm-chart/images/binderhub-service/requirements.in b/helm-chart/images/binderhub/requirements.in similarity index 54% rename from helm-chart/images/binderhub-service/requirements.in rename to helm-chart/images/binderhub/requirements.in index 6431bda18..76f4a047f 100644 --- a/helm-chart/images/binderhub-service/requirements.in +++ b/helm-chart/images/binderhub/requirements.in @@ -1,8 +1,11 @@ -# This file is the input to requirements.txt, which is a frozen version of this. -# To update requirements.txt, use the "Run workflow" button at -# https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml +# binderhub's dependencies # -binderhub[pycurl] @ git+https://github.com/jupyterhub/binderhub@main +# We can't put ".[pycurl]" here directly as when we freeze this into +# requirements.txt using ci/refreeze, its declaring "binderhub @ file:///io" +# which is a problem as its an absolute path. +# +pycurl +-r ../../../requirements.txt # ruamel-yaml is required by this chart's binderhub_config.py ruamel-yaml diff --git a/helm-chart/images/binderhub-service/requirements.txt b/helm-chart/images/binderhub/requirements.txt similarity index 82% rename from helm-chart/images/binderhub-service/requirements.txt rename to helm-chart/images/binderhub/requirements.txt index a6b3c6ec0..c9b6b49c6 100644 --- a/helm-chart/images/binderhub-service/requirements.txt +++ b/helm-chart/images/binderhub/requirements.txt @@ -1,10 +1,10 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# Use the "Run workflow" button at https://github.com/2i2c-org/binderhub-service/actions/workflows/watch-dependencies.yaml +# Use the "Run workflow" button at https://github.com/jupyterhub/binderhub/actions/workflows/watch-dependencies.yaml # -alembic==1.14.1 +alembic==1.15.1 # via jupyterhub annotated-types==0.7.0 # via pydantic @@ -14,8 +14,6 @@ attrs==25.1.0 # via # jsonschema # referencing -binderhub @ git+https://github.com/jupyterhub/binderhub@main - # via -r requirements.in cachetools==5.5.2 # via google-auth certifi==2025.1.31 @@ -28,16 +26,16 @@ cffi==1.17.1 # via cryptography charset-normalizer==3.4.1 # via requests -cryptography==44.0.1 +cryptography==44.0.2 # via certipy deprecated==1.2.18 # via opentelemetry-api docker==7.1.0 - # via binderhub + # via -r /io/requirements.txt durationpy==0.9 # via kubernetes escapism==1.0.1 - # via binderhub + # via -r /io/requirements.txt fqdn==1.5.1 # via jsonschema google-api-core==2.24.1 @@ -54,13 +52,13 @@ google-auth==2.38.0 # kubernetes google-cloud-appengine-logging==1.6.0 # via google-cloud-logging -google-cloud-audit-log==0.3.0 +google-cloud-audit-log==0.3.1 # via google-cloud-logging google-cloud-core==2.4.2 # via google-cloud-logging google-cloud-logging==3.11.4 - # via -r requirements.in -googleapis-common-protos==1.68.0 + # via -r helm-chart/images/binderhub/requirements.in +googleapis-common-protos==1.69.1 # via # google-api-core # google-cloud-audit-log @@ -68,7 +66,7 @@ googleapis-common-protos==1.68.0 # grpcio-status greenlet==3.1.1 # via sqlalchemy -grpc-google-iam-v1==0.14.0 +grpc-google-iam-v1==0.14.1 # via google-cloud-logging grpcio==1.70.0 # via @@ -87,24 +85,24 @@ importlib-metadata==8.5.0 # via opentelemetry-api isoduration==20.11.0 # via jsonschema -jinja2==3.1.5 +jinja2==3.1.6 # via - # binderhub + # -r /io/requirements.txt # jupyterhub jsonpointer==3.0.0 # via jsonschema jsonschema==4.23.0 # via - # binderhub + # -r /io/requirements.txt # jupyter-events jsonschema-specifications==2024.10.1 # via jsonschema jupyter-events==0.12.0 # via jupyterhub jupyterhub==5.2.1 - # via binderhub + # via -r /io/requirements.txt kubernetes==32.0.1 - # via binderhub + # via -r /io/requirements.txt mako==1.3.9 # via alembic markupsafe==3.0.2 @@ -126,7 +124,7 @@ pamela==1.2.0 # via jupyterhub prometheus-client==0.21.1 # via - # binderhub + # -r /io/requirements.txt # jupyterhub proto-plus==1.26.0 # via @@ -151,22 +149,22 @@ pyasn1-modules==0.4.1 # via google-auth pycparser==2.22 # via cffi -pycurl==7.45.4 - # via binderhub +pycurl==7.45.6 + # via -r helm-chart/images/binderhub/requirements.in pydantic==2.10.6 # via jupyterhub pydantic-core==2.27.2 # via pydantic pyjwt==2.10.1 - # via binderhub + # via -r /io/requirements.txt python-dateutil==2.9.0.post0 # via # arrow # jupyterhub # kubernetes -python-json-logger==3.2.1 +python-json-logger==3.3.0 # via - # binderhub + # -r /io/requirements.txt # jupyter-events pyyaml==6.0.2 # via @@ -201,9 +199,7 @@ rpds-py==0.23.1 rsa==4.9 # via google-auth ruamel-yaml==0.18.10 - # via -r requirements.in -ruamel-yaml-clib==0.2.12 - # via ruamel-yaml + # via -r helm-chart/images/binderhub/requirements.in six==1.17.0 # via # kubernetes @@ -215,11 +211,11 @@ sqlalchemy==2.0.38 # jupyterhub tornado==6.4.2 # via - # binderhub + # -r /io/requirements.txt # jupyterhub traitlets==5.14.3 # via - # binderhub + # -r /io/requirements.txt # jupyter-events # jupyterhub types-python-dateutil==2.9.0.20241206 @@ -229,7 +225,6 @@ typing-extensions==4.12.2 # alembic # pydantic # pydantic-core - # referencing # sqlalchemy uri-template==1.3.0 # via jsonschema From ce14ad3d96bb2701dd9a4fa53b458f4a0f25069a Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Fri, 7 Mar 2025 14:17:33 -0800 Subject: [PATCH 195/200] Fix refreeze script --- ci/refreeze | 6 +-- helm-chart/images/binderhub/requirements.in | 3 +- helm-chart/images/binderhub/requirements.txt | 50 ++++++++------------ 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/ci/refreeze b/ci/refreeze index 80a140f00..d66c2ab13 100755 --- a/ci/refreeze +++ b/ci/refreeze @@ -7,8 +7,8 @@ set -xeuo pipefail docker run --rm \ --env=CUSTOM_COMPILE_COMMAND='Use the "Run workflow" button at https://github.com/jupyterhub/binderhub/actions/workflows/watch-dependencies.yaml' \ - --volume="$PWD:/io" \ - --workdir=/io \ + --volume="$PWD:/src" \ + --workdir=/src/helm-chart/images/binderhub/ \ --user=root \ python:3.13-bookworm \ - sh -c 'pip install pip-tools==7.* && pip-compile --allow-unsafe --strip-extras --upgrade helm-chart/images/binderhub/requirements.in' + sh -c 'pip install pip-tools==7.* && pip-compile --allow-unsafe --strip-extras --upgrade requirements.in' diff --git a/helm-chart/images/binderhub/requirements.in b/helm-chart/images/binderhub/requirements.in index 76f4a047f..e9b2d5ce2 100644 --- a/helm-chart/images/binderhub/requirements.in +++ b/helm-chart/images/binderhub/requirements.in @@ -5,7 +5,8 @@ # which is a problem as its an absolute path. # pycurl --r ../../../requirements.txt +../../.. + # ruamel-yaml is required by this chart's binderhub_config.py ruamel-yaml diff --git a/helm-chart/images/binderhub/requirements.txt b/helm-chart/images/binderhub/requirements.txt index c9b6b49c6..c6935ae9d 100644 --- a/helm-chart/images/binderhub/requirements.txt +++ b/helm-chart/images/binderhub/requirements.txt @@ -14,6 +14,8 @@ attrs==25.1.0 # via # jsonschema # referencing +binderhub @ file:///src + # via -r requirements.in cachetools==5.5.2 # via google-auth certifi==2025.1.31 @@ -28,14 +30,12 @@ charset-normalizer==3.4.1 # via requests cryptography==44.0.2 # via certipy -deprecated==1.2.18 - # via opentelemetry-api docker==7.1.0 - # via -r /io/requirements.txt + # via binderhub durationpy==0.9 # via kubernetes escapism==1.0.1 - # via -r /io/requirements.txt + # via binderhub fqdn==1.5.1 # via jsonschema google-api-core==2.24.1 @@ -48,7 +48,6 @@ google-auth==2.38.0 # google-api-core # google-cloud-appengine-logging # google-cloud-core - # google-cloud-logging # kubernetes google-cloud-appengine-logging==1.6.0 # via google-cloud-logging @@ -56,8 +55,8 @@ google-cloud-audit-log==0.3.1 # via google-cloud-logging google-cloud-core==2.4.2 # via google-cloud-logging -google-cloud-logging==3.11.4 - # via -r helm-chart/images/binderhub/requirements.in +google-cloud-logging==3.0.0 + # via -r requirements.in googleapis-common-protos==1.69.1 # via # google-api-core @@ -66,7 +65,7 @@ googleapis-common-protos==1.69.1 # grpcio-status greenlet==3.1.1 # via sqlalchemy -grpc-google-iam-v1==0.14.1 +grpc-google-iam-v1==0.12.7 # via google-cloud-logging grpcio==1.70.0 # via @@ -74,35 +73,33 @@ grpcio==1.70.0 # googleapis-common-protos # grpc-google-iam-v1 # grpcio-status -grpcio-status==1.70.0 +grpcio-status==1.62.3 # via google-api-core idna==3.10 # via # jsonschema # jupyterhub # requests -importlib-metadata==8.5.0 - # via opentelemetry-api isoduration==20.11.0 # via jsonschema jinja2==3.1.6 # via - # -r /io/requirements.txt + # binderhub # jupyterhub jsonpointer==3.0.0 # via jsonschema jsonschema==4.23.0 # via - # -r /io/requirements.txt + # binderhub # jupyter-events jsonschema-specifications==2024.10.1 # via jsonschema jupyter-events==0.12.0 # via jupyterhub jupyterhub==5.2.1 - # via -r /io/requirements.txt + # via binderhub kubernetes==32.0.1 - # via -r /io/requirements.txt + # via binderhub mako==1.3.9 # via alembic markupsafe==3.0.2 @@ -114,8 +111,6 @@ oauthlib==3.2.2 # jupyterhub # kubernetes # requests-oauthlib -opentelemetry-api==1.30.0 - # via google-cloud-logging packaging==24.2 # via # jupyter-events @@ -124,19 +119,18 @@ pamela==1.2.0 # via jupyterhub prometheus-client==0.21.1 # via - # -r /io/requirements.txt + # binderhub # jupyterhub proto-plus==1.26.0 # via # google-api-core # google-cloud-appengine-logging # google-cloud-logging -protobuf==5.29.3 +protobuf==4.25.6 # via # google-api-core # google-cloud-appengine-logging # google-cloud-audit-log - # google-cloud-logging # googleapis-common-protos # grpc-google-iam-v1 # grpcio-status @@ -150,13 +144,13 @@ pyasn1-modules==0.4.1 pycparser==2.22 # via cffi pycurl==7.45.6 - # via -r helm-chart/images/binderhub/requirements.in + # via -r requirements.in pydantic==2.10.6 # via jupyterhub pydantic-core==2.27.2 # via pydantic pyjwt==2.10.1 - # via -r /io/requirements.txt + # via binderhub python-dateutil==2.9.0.post0 # via # arrow @@ -164,7 +158,7 @@ python-dateutil==2.9.0.post0 # kubernetes python-json-logger==3.3.0 # via - # -r /io/requirements.txt + # binderhub # jupyter-events pyyaml==6.0.2 # via @@ -199,7 +193,7 @@ rpds-py==0.23.1 rsa==4.9 # via google-auth ruamel-yaml==0.18.10 - # via -r helm-chart/images/binderhub/requirements.in + # via -r requirements.in six==1.17.0 # via # kubernetes @@ -211,11 +205,11 @@ sqlalchemy==2.0.38 # jupyterhub tornado==6.4.2 # via - # -r /io/requirements.txt + # binderhub # jupyterhub traitlets==5.14.3 # via - # -r /io/requirements.txt + # binderhub # jupyter-events # jupyterhub types-python-dateutil==2.9.0.20241206 @@ -237,7 +231,3 @@ webcolors==24.11.1 # via jsonschema websocket-client==1.8.0 # via kubernetes -wrapt==1.17.2 - # via deprecated -zipp==3.21.0 - # via importlib-metadata From 9423369d651f3a2cdbb890d8ef23b7c49da9dd8f Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Fri, 7 Mar 2025 14:25:29 -0800 Subject: [PATCH 196/200] Fix chartpress and refreeze script again --- ci/refreeze | 4 +- helm-chart/chartpress.yaml | 47 +++++++++++++------- helm-chart/images/binderhub/Dockerfile | 21 ++++----- helm-chart/images/binderhub/requirements.in | 3 +- helm-chart/images/binderhub/requirements.txt | 28 ++++++++---- 5 files changed, 65 insertions(+), 38 deletions(-) diff --git a/ci/refreeze b/ci/refreeze index d66c2ab13..5c45b4043 100755 --- a/ci/refreeze +++ b/ci/refreeze @@ -8,7 +8,7 @@ set -xeuo pipefail docker run --rm \ --env=CUSTOM_COMPILE_COMMAND='Use the "Run workflow" button at https://github.com/jupyterhub/binderhub/actions/workflows/watch-dependencies.yaml' \ --volume="$PWD:/src" \ - --workdir=/src/helm-chart/images/binderhub/ \ + --workdir=/src \ --user=root \ python:3.13-bookworm \ - sh -c 'pip install pip-tools==7.* && pip-compile --allow-unsafe --strip-extras --upgrade requirements.in' + sh -c 'pip install pip-tools==7.* && pip-compile --allow-unsafe --strip-extras --upgrade helm-chart/images/binderhub/requirements.in' diff --git a/helm-chart/chartpress.yaml b/helm-chart/chartpress.yaml index fee005c6b..56ba2bac2 100644 --- a/helm-chart/chartpress.yaml +++ b/helm-chart/chartpress.yaml @@ -1,23 +1,40 @@ -# This is the configuration for chartpress, a CLI for Helm chart management. -# -# chartpress is used to: -# - Build images -# - Update Chart.yaml (version) and values.yaml (image tags) -# - Package and publish Helm charts to a GitHub based Helm chart repository -# -# For more information, see the projects README.md file: -# https://github.com/jupyterhub/chartpress +# For a reference on this configuration, see the chartpress README file. +# ref: https://github.com/jupyterhub/chartpress # +# NOTE: All paths will be set relative to this file's location, which is in the +# helm-chart folder. charts: - name: binderhub - imagePrefix: quay.io/2i2c/ - baseVersion: "0.3.0-0.dev" + baseVersion: 1.0.0-0.dev + imagePrefix: quay.io/jupyterhub/k8s- repo: git: jupyterhub/helm-chart published: https://jupyterhub.github.io/helm-chart - images: - # binderhub, the container where the binderhub the Python - # application is running. binderhub: - valuesPath: image + # We will not use the default build contextPath, and must therefore + # specify the dockerfilePath explicitly. + dockerfilePath: images/binderhub/Dockerfile + # Context to send to docker build for use by the Dockerfile. We pass the + # root folder in order to allow the image to access and build the python + # package. + contextPath: .. + # To avoid chartpress to react to changes in documentation and other + # things, we ask it to not trigger on changes to the contextPath, which + # means we manually should add paths rebuild should be triggered on + rebuildOnContextPathChanges: false + # We manually specify the paths which chartpress should monitor for + # changes that should trigger a rebuild of this image. + paths: + - images/binderhub + - ../binderhub + - ../js + - ../babel.config.json + - ../MANIFEST.in + - ../package.json + - ../pyproject.toml + - ../requirements.txt + - ../setup.cfg + - ../setup.py + - ../webpack.config.js + valuesPath: image \ No newline at end of file diff --git a/helm-chart/images/binderhub/Dockerfile b/helm-chart/images/binderhub/Dockerfile index 6cdb697a6..68480b562 100644 --- a/helm-chart/images/binderhub/Dockerfile +++ b/helm-chart/images/binderhub/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update \ # ephemeral docker cache (--mount=type=cache,target=${PIP_CACHE_DIR}). We use # the same technique for the directory /tmp/wheels. # -COPY requirements.txt requirements.txt +COPY . /src ARG PIP_CACHE_DIR=/tmp/pip-cache RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ pip wheel \ @@ -29,8 +29,7 @@ RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ # pycurl wheels for 7.45.3 have problems finding CAs # https://github.com/pycurl/pycurl/issues/834 --no-binary pycurl \ - -r requirements.txt - + -r /src/helm-chart/images/binderhub/requirements.txt # The final stage # --------------- @@ -53,16 +52,14 @@ RUN apt-get update \ # terminate very quickly. && rm -rf /var/lib/apt/lists/* +COPY helm-chart/images/binderhub/requirements.txt requirements.txt +# Even though a binderhub wheel is built by the build stage, the requirements.txt +# file produced by pip-compile will still refer to `file:///src` as the source for it. +# We run this sed to tell it to just look for a package named `binderhub` instead, which +# will show up as a wheel in `/tmp/wheels` +RUN sed -i -E 's/binderhub @ file.+/binderhub/' requirements.txt + # install wheels built in the build stage -COPY requirements.txt requirements.txt -# FIXME: As long as we reference git in requirements.txt (via requirements.in), -# we need to make sure we don't end up re-installing from git here but -# using the built wheel. -# -# This sed replace operation removes the " @ git+..." stuff, remove it -# when we reference version controlled releases of binderhub. -# -RUN sed -i -E 's/binderhub @ git.+/binderhub/' requirements.txt ARG PIP_CACHE_DIR=/tmp/pip-cache # --no-index ensures _only_ wheels from the build stage are installed RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ diff --git a/helm-chart/images/binderhub/requirements.in b/helm-chart/images/binderhub/requirements.in index e9b2d5ce2..6cd668d53 100644 --- a/helm-chart/images/binderhub/requirements.in +++ b/helm-chart/images/binderhub/requirements.in @@ -5,7 +5,8 @@ # which is a problem as its an absolute path. # pycurl -../../.. +# This *must* be regenerated with `./ci/refreeze` so it handles paths correctly +. # ruamel-yaml is required by this chart's binderhub_config.py diff --git a/helm-chart/images/binderhub/requirements.txt b/helm-chart/images/binderhub/requirements.txt index c6935ae9d..603ae37f8 100644 --- a/helm-chart/images/binderhub/requirements.txt +++ b/helm-chart/images/binderhub/requirements.txt @@ -15,7 +15,7 @@ attrs==25.1.0 # jsonschema # referencing binderhub @ file:///src - # via -r requirements.in + # via -r helm-chart/images/binderhub/requirements.in cachetools==5.5.2 # via google-auth certifi==2025.1.31 @@ -30,6 +30,8 @@ charset-normalizer==3.4.1 # via requests cryptography==44.0.2 # via certipy +deprecated==1.2.18 + # via opentelemetry-api docker==7.1.0 # via binderhub durationpy==0.9 @@ -48,6 +50,7 @@ google-auth==2.38.0 # google-api-core # google-cloud-appengine-logging # google-cloud-core + # google-cloud-logging # kubernetes google-cloud-appengine-logging==1.6.0 # via google-cloud-logging @@ -55,8 +58,8 @@ google-cloud-audit-log==0.3.1 # via google-cloud-logging google-cloud-core==2.4.2 # via google-cloud-logging -google-cloud-logging==3.0.0 - # via -r requirements.in +google-cloud-logging==3.11.4 + # via -r helm-chart/images/binderhub/requirements.in googleapis-common-protos==1.69.1 # via # google-api-core @@ -65,7 +68,7 @@ googleapis-common-protos==1.69.1 # grpcio-status greenlet==3.1.1 # via sqlalchemy -grpc-google-iam-v1==0.12.7 +grpc-google-iam-v1==0.14.1 # via google-cloud-logging grpcio==1.70.0 # via @@ -73,13 +76,15 @@ grpcio==1.70.0 # googleapis-common-protos # grpc-google-iam-v1 # grpcio-status -grpcio-status==1.62.3 +grpcio-status==1.70.0 # via google-api-core idna==3.10 # via # jsonschema # jupyterhub # requests +importlib-metadata==8.5.0 + # via opentelemetry-api isoduration==20.11.0 # via jsonschema jinja2==3.1.6 @@ -111,6 +116,8 @@ oauthlib==3.2.2 # jupyterhub # kubernetes # requests-oauthlib +opentelemetry-api==1.30.0 + # via google-cloud-logging packaging==24.2 # via # jupyter-events @@ -126,11 +133,12 @@ proto-plus==1.26.0 # google-api-core # google-cloud-appengine-logging # google-cloud-logging -protobuf==4.25.6 +protobuf==5.29.3 # via # google-api-core # google-cloud-appengine-logging # google-cloud-audit-log + # google-cloud-logging # googleapis-common-protos # grpc-google-iam-v1 # grpcio-status @@ -144,7 +152,7 @@ pyasn1-modules==0.4.1 pycparser==2.22 # via cffi pycurl==7.45.6 - # via -r requirements.in + # via -r helm-chart/images/binderhub/requirements.in pydantic==2.10.6 # via jupyterhub pydantic-core==2.27.2 @@ -193,7 +201,7 @@ rpds-py==0.23.1 rsa==4.9 # via google-auth ruamel-yaml==0.18.10 - # via -r requirements.in + # via -r helm-chart/images/binderhub/requirements.in six==1.17.0 # via # kubernetes @@ -231,3 +239,7 @@ webcolors==24.11.1 # via jsonschema websocket-client==1.8.0 # via kubernetes +wrapt==1.17.2 + # via deprecated +zipp==3.21.0 + # via importlib-metadata From 055fad16a116b882196aa3b232c894c4ad90b84d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 22:48:32 +0000 Subject: [PATCH 197/200] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- helm-chart/chartpress.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm-chart/chartpress.yaml b/helm-chart/chartpress.yaml index 56ba2bac2..79526391f 100644 --- a/helm-chart/chartpress.yaml +++ b/helm-chart/chartpress.yaml @@ -37,4 +37,4 @@ charts: - ../setup.cfg - ../setup.py - ../webpack.config.js - valuesPath: image \ No newline at end of file + valuesPath: image From 5fc465677d0ee199623326b4b3a1f3de206f9606 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Fri, 7 Mar 2025 23:08:36 -0800 Subject: [PATCH 198/200] Remove some unused workflows --- .github/workflows/release.yaml | 135 --------------- .github/workflows/test-chart.yaml | 202 ----------------------- .github/workflows/test-docker-build.yaml | 65 -------- .github/workflows/test-docs.yaml | 37 ----- .github/workflows/test.yml | 3 - 5 files changed, 442 deletions(-) delete mode 100644 .github/workflows/release.yaml delete mode 100644 .github/workflows/test-chart.yaml delete mode 100644 .github/workflows/test-docker-build.yaml delete mode 100644 .github/workflows/test-docs.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index c03287fc0..000000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,135 +0,0 @@ -# This is a GitHub workflow defining a set of jobs with a set of steps. -# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions -# -name: Release - -on: - pull_request: - paths-ignore: - - "docs/**" - - "**.md" - - ".github/workflows/*" - - "!.github/workflows/release.yaml" - push: - paths-ignore: - - "docs/**" - - "**.md" - - ".github/workflows/*" - - "!.github/workflows/release.yaml" - branches-ignore: - - "dependabot/**" - - "pre-commit-ci-update-config" - - "update-*" - tags: - - "**" - -jobs: - # Builds and pushes docker images to quay.io and packages the Helm chart and - # publishes it at 2i2c-org/binderhub-service@gh-pages which is a Helm chart - # repository with a index.yaml file and packaged Helm charts. - # - # ref: https://2i2c.org/binderhub-service/index.yaml - # - release: - runs-on: ubuntu-22.04 - - steps: - - uses: actions/checkout@v4 - with: - # chartpress needs git history - fetch-depth: 0 - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Decide to publish or not - id: publishing - shell: python - run: | - import os - repo = "${{ github.repository }}" - event = "${{ github.event_name }}" - ref = "${{ github.event.ref }}" - publishing = "" - if ( - repo == "2i2c-org/binderhub-service" - and event == "push" - and ( - ref.startswith("refs/tags/") - or ref == "refs/heads/main" - ) - ): - publishing = "true" - print("Publishing chart") - with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write(f"publishing={publishing}\n") - - - name: Set up QEMU (for docker buildx) - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx (for chartpress multi-arch builds) - uses: docker/setup-buildx-action@v3 - - - name: Install chart publishing dependencies (chartpress, pyyaml, helm) - run: | - # FIXME: remove this pin, and the one in dev-requirements.txt - pip install requests==2.31.0 - - pip install chartpress pyyaml - pip list - - # helm is already installed - helm version - - - name: Generate values.schema.json from values.schema.yaml - run: ./tools/generate-json-schema.py - - # chartpress will make a commit when pushing to gh-pages, so we need to - # configure a git user. - - name: Configure a git user - run: | - git config --global user.email "github-actions@example.local" - git config --global user.name "GitHub Actions user" - - - name: Setup push rights to Helm chart repository's git repo - # This was setup by... - # - # 1. Generating a private/public key pair: - # ssh-keygen -t ed25519 -C "2i2c-org/binderhub-service" -f /tmp/id_ed25519 - # - # 2. Registering the private key (/tmp/id_ed25519) as a secret for this - # repo: https://github.com/2i2c-org/binderhub-service/settings/secrets/actions - # - # 3. Registering the public key (/tmp/id_ed25519.pub) as a deploy key - # with push rights for the Helm chart repository's git repo: - # https://github.com/2i2c-org/binderhub-service/settings/keys - # - if: steps.publishing.outputs.publishing - run: | - mkdir -p ~/.ssh - ssh-keyscan github.com >> ~/.ssh/known_hosts - echo "${{ secrets.HELM_CHART_REPO_DEPLOY_KEY }}" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - - - name: Setup docker push rights to quay.io - if: steps.publishing.outputs.publishing - run: docker login -u "${{ secrets.DOCKER_USERNAME }}" -p "${{ secrets.DOCKER_PASSWORD }}" quay.io - - - name: Publish images and chart with chartpress - if: steps.publishing.outputs.publishing - run: ./ci/publish - env: - GITHUB_REPOSITORY: "${{ github.repository }}" - - - name: Package chart for actions/upload-artifact - if: steps.publishing.outputs.publishing == '' - run: helm package binderhub-service - - # ref: https://github.com/actions/upload-artifact - - uses: actions/upload-artifact@v4 - if: steps.publishing.outputs.publishing == '' - with: - name: binderhub-service-${{ github.sha }} - path: "binderhub-service-*.tgz" - if-no-files-found: error diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml deleted file mode 100644 index 99f035b52..000000000 --- a/.github/workflows/test-chart.yaml +++ /dev/null @@ -1,202 +0,0 @@ -# This is a GitHub workflow defining a set of jobs with a set of steps. -# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions -# -name: Test chart - -on: - pull_request: - paths-ignore: - - "docs/**" - - "**.md" - - ".github/workflows/*" - - "!.github/workflows/test-chart.yaml" - push: - paths-ignore: - - "docs/**" - - "**.md" - - ".github/workflows/*" - - "!.github/workflows/test-chart.yaml" - branches-ignore: - - "dependabot/**" - - "pre-commit-ci-update-config" - workflow_dispatch: - -jobs: - lint_and_validate_rendered_templates: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install dependencies - run: pip install -r dev-requirements.txt - - - name: Lint and validate - run: tools/templates/lint-and-validate.py - - - name: Lint and validate (--strict, accept failure) - run: tools/templates/lint-and-validate.py --strict - continue-on-error: true - - lint_and_validate_templates_with_schema: - runs-on: ubuntu-22.04 - - strategy: - fail-fast: false - matrix: - # We run this job with the latest lowest helm version we support. - # - include: - - helm-version: "" # latest - - helm-version: v3.8.0 # minimal required version - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install helm ${{ matrix.helm-version }} - run: | - curl -sf https://raw.githubusercontent.com/helm/helm/HEAD/scripts/get-helm-3 | DESIRED_VERSION=${{ matrix.helm-version }} bash - - - name: Install dependencies - run: | - pip install pyyaml - - - name: Generate values.schema.json - run: tools/generate-json-schema.py - - - name: Helm lint (values.yaml) - run: helm lint helm-chart/binderhub - - - name: Helm lint (lint-and-validate-values.yaml) - run: helm lint helm-chart/binderhub --values tools/templates/lint-and-validate-values.yaml - - # FIXME: We can probably emit a GitHub workflow warning if these fail - # instead having them show as green without a warning or similar - # - # NOTE: --strict means that any warning is considered an error, and there - # are several warnings that we should ignore. - # - - name: Helm lint --strict (values.yaml) - run: helm lint --strict helm-chart/binderhub - continue-on-error: true - - - name: Helm lint --strict (lint-and-validate-values.yaml) - run: helm lint --strict helm-chart/binderhub - continue-on-error: true - - test: - runs-on: ubuntu-22.04 - timeout-minutes: 20 - - # Start a local container registry that the binderhub build pods can push - # to, and the k8s cluster can pull images from for use by pods. - services: - registry: - image: docker.io/library/registry:latest - ports: - - 5000:5000 - - strategy: - fail-fast: false - matrix: - # We run this job multiple times with different parameterization - # specified below, these parameters have no meaning on their own and - # gain meaning on how job steps use them. - # - # k3s-version: https://github.com/rancher/k3s/tags - # k3s-channel: https://update.k3s.io/v1-release/channels - # - include: - - k3s-channel: latest - - k3s-channel: stable - - k3s-channel: v1.27 - - steps: - - uses: actions/checkout@v4 - with: - # chartpress needs git history - fetch-depth: 0 - - - name: Determine how to reference local container registry - run: | - LOCAL_REGISTRY_HOST=$(hostname --all-ip-addresses | awk '{print $1}'):5000 - echo LOCAL_REGISTRY_HOST="$LOCAL_REGISTRY_HOST" >> $GITHUB_ENV - - - name: Configure k3s to pull from local container registry - run: | - # Allow k3s to pull from private registry - # https://docs.k3s.io/installation/private-registry - sudo mkdir -p /etc/rancher/k3s/ - cat << EOF | sudo tee /etc/rancher/k3s/registries.yaml - mirrors: - "$LOCAL_REGISTRY_HOST": - endpoint: - - "http://$LOCAL_REGISTRY_HOST" - EOF - - # Starts a k8s cluster with NetworkPolicy enforcement and installs both - # kubectl and helm - # - # ref: https://github.com/jupyterhub/action-k3s-helm/ - - uses: jupyterhub/action-k3s-helm@v4 - with: - k3s-channel: ${{ matrix.k3s-channel }} - metrics-enabled: false - traefik-enabled: false - docker-enabled: true - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - name: Install dependencies - run: | - pip install -r dev-requirements.txt - - name: List dependencies - run: | - pip freeze - - # Build our images if needed and update Chart.yaml and values.yaml with - # version and tags - - run: chartpress - env: - DOCKER_BUILDKIT: "1" - - - name: Generate values.schema.json from values.schema.yaml - run: tools/generate-json-schema.py - - # Validate rendered helm templates against the k8s api-server with the - # dedicated lint-and-validate-values.yaml config. - - name: "Helm template --validate (with lint and validate config)" - run: | - helm template --validate binderhub helm-chart/binderhub \ - --values tools/templates/lint-and-validate-values.yaml - - - name: Install local chart - run: | - helm upgrade --install binderhub helm-chart/binderhub \ - --values dev-config.yaml \ - --set config.BinderHub.image_prefix=$LOCAL_REGISTRY_HOST/binderhub/ \ - --set config.DockerRegistry.url=http://$LOCAL_REGISTRY_HOST \ - --set buildPodsRegistryCredentials.server=http://$LOCAL_REGISTRY_HOST \ - --set dockerApi.extraFiles.daemon-json.data.insecure-registries[0]=$LOCAL_REGISTRY_HOST - - # ref: https://github.com/jupyterhub/action-k8s-await-workloads - - uses: jupyterhub/action-k8s-await-workloads@v3 - with: - timeout: 150 - max-restarts: 1 - - - name: Test image build/push via binderhub REST API using curl - run: | - curl http://localhost:30080/build/gh/binderhub-ci-repos/cached-minimal-dockerfile/HEAD?build_only=true - - # ref: https://github.com/jupyterhub/action-k8s-namespace-report - - uses: jupyterhub/action-k8s-namespace-report@v1 - if: always() - with: - important-workloads: deploy/binderhub daemonset/binderhub-docker-api diff --git a/.github/workflows/test-docker-build.yaml b/.github/workflows/test-docker-build.yaml deleted file mode 100644 index 86af0762a..000000000 --- a/.github/workflows/test-docker-build.yaml +++ /dev/null @@ -1,65 +0,0 @@ -# This is a GitHub workflow defining a set of jobs with a set of steps. -# ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions -# -name: Test docker multiarch build - -# Trigger the workflow's on all PRs and pushes so that other contributors can -# run tests in their own forks. Avoid triggering this tests on changes not -# influencing the image notably. -on: - pull_request: - paths: - - "helm-chart/images/**" - - "helm-chart/chartpress.yaml" - - ".github/workflows/test-docker-build.yaml" - push: - paths: - - "helm-chart/images/**" - - "helm-chart/chartpress.yaml" - - ".github/workflows/test-docker-build.yaml" - branches-ignore: - - "dependabot/**" - - "pre-commit-ci-update-config" - - "update-*" - workflow_dispatch: - -jobs: - # This is a quick test to check the arm64 docker images based on: - # - https://github.com/docker/build-push-action/blob/v2.3.0/docs/advanced/local-registry.md - # - https://github.com/docker/build-push-action/blob/v2.3.0/docs/advanced/multi-platform.md - build_images: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - with: - # chartpress requires git history to set chart version and image tags - # correctly - fetch-depth: 0 - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - uses: actions/setup-node@v4 - # node required to build wheel - with: - node-version: "22" - - - name: Install chartpress - run: pip install chartpress build - - - name: Build binderhub wheel - run: python3 -m build --wheel . - - - name: Set up QEMU (for docker buildx) - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx (for chartpress multi-arch builds) - uses: docker/setup-buildx-action@v3 - - - name: Build a multiple architecture Docker image - run: | - cd helm-chart - chartpress \ - --builder docker-buildx \ - --platform linux/amd64 --platform linux/arm64 diff --git a/.github/workflows/test-docs.yaml b/.github/workflows/test-docs.yaml deleted file mode 100644 index 955727a9b..000000000 --- a/.github/workflows/test-docs.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# This is a GitHub workflow defining a set of jobs with a set of steps. -# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions -# -name: Test docs - -on: - pull_request: - paths: - - "docs/**" - - "**/schema.yaml" - - "**/test-docs.yaml" - push: - paths: - - "docs/**" - - "**/schema.yaml" - - "**/test-docs.yaml" - branches-ignore: - - "dependabot/**" - - "pre-commit-ci-update-config" - - "update-*" - workflow_dispatch: - -jobs: - linkcheck: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - run: pip install -r docs/requirements.txt - - - name: make linkcheck - run: | - cd docs - make linkcheck SPHINXOPTS='--color -W --keep-going' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4ea4c5a3..117f368d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,3 @@ -# This is a GitHub workflow defining a set of jobs with a set of steps. -# ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions -# name: Tests on: From 399b410a2ed0fbb872e291ccf1c4a7de7b31532f Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Fri, 7 Mar 2025 23:12:53 -0800 Subject: [PATCH 199/200] Remove ./ci/check_embedded_chart_code.py It's no longer used, although it hasn't been replaced with anything yet. --- .github/workflows/test.yml | 11 ------ .pre-commit-config.yaml | 16 --------- binderhub/binderspawner_mixin.py | 5 --- ci/check_embedded_chart_code.py | 62 -------------------------------- 4 files changed, 94 deletions(-) delete mode 100755 ci/check_embedded_chart_code.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 117f368d1..5d5862512 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,17 +24,6 @@ on: workflow_dispatch: jobs: - lint: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - - name: install requirements - run: pip install ruamel.yaml - - - name: check embedded chart code - run: ./ci/check_embedded_chart_code.py - # Most of the "main", "auth" and "helm" jobs are the same and only differ # in small things. Unfortunately there is no easy way to share steps between # jobs or have "template" jobs, so we use `if` conditions on steps diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d3359231..debf01855 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,22 +31,6 @@ repos: - id: isort # args are not passed, but see the config in pyproject.toml - # Generated code: - # An entry in helm-chart/binderhub/values.yaml should be generated based on - # binderhub/binderspawner_mixin.py. See ci/check_embedded_chart_code.py for - # more details. - - repo: local - hooks: - - id: update-values-based-on-binderspawner-mixin - name: Update helm-chart/binderhub/values.yaml based on binderhub/binderspawner_mixin.py - language: python - additional_dependencies: ["ruamel.yaml"] - entry: python ci/check_embedded_chart_code.py - args: - - --update - files: binderhub/binderspawner_mixin.py|helm-chart/binderhub/values.yaml - pass_filenames: false - # Autoformat: js, html, markdown, yaml, json - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 diff --git a/binderhub/binderspawner_mixin.py b/binderhub/binderspawner_mixin.py index b0af376d4..b17fb2e1b 100644 --- a/binderhub/binderspawner_mixin.py +++ b/binderhub/binderspawner_mixin.py @@ -1,11 +1,6 @@ """ Helpers for creating BinderSpawners -FIXME: -This file is defined in binderhub/binderspawner_mixin.py -and is copied to helm-chart/binderhub/values.yaml -by ci/check_embedded_chart_code.py - The BinderHub repo is just used as the distribution mechanism for this spawner, BinderHub itself doesn't require this code. diff --git a/ci/check_embedded_chart_code.py b/ci/check_embedded_chart_code.py deleted file mode 100755 index 86b7a9374..000000000 --- a/ci/check_embedded_chart_code.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python - -# FIXME: We currently have some code duplicated in -# binderhub/binderspawner_mixin.py and helm-chart/binderhub/values.yaml -# and we use a pre-commit hook to automatically update the values in -# values.yaml. -# -# We should remove the embedded code from values.yaml and install the required -# BinderSpawner code in the JupyterHub container. -# - -# For now just check that the two are in sync -import argparse -import difflib -import os -import sys - -from ruamel.yaml import YAML - -yaml = YAML() -yaml.preserve_quotes = True -yaml.indent(mapping=2, sequence=4, offset=2) - -parser = argparse.ArgumentParser(description="Check embedded chart code") -parser.add_argument( - "--update", action="store_true", help="Update binderhub code from values.yaml" -) -args = parser.parse_args() - -root = os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir) -binderspawner_mixin_py = os.path.join(root, "binderhub", "binderspawner_mixin.py") -values_yaml = os.path.join(root, "helm-chart", "binderhub", "values.yaml") - -with open(binderspawner_mixin_py) as f: - py_code = f.read() - - -if args.update: - with open(values_yaml) as f: - values = yaml.load(f) - values_code = values["jupyterhub"]["hub"]["extraConfig"]["0-binderspawnermixin"] - if values_code != py_code: - print(f"Generating {values_yaml} from {binderspawner_mixin_py}") - values["jupyterhub"]["hub"]["extraConfig"]["0-binderspawnermixin"] = py_code - with open(values_yaml, "w") as f: - yaml.dump(values, f) -else: - with open(values_yaml) as f: - values = yaml.load(f) - values_code = values["jupyterhub"]["hub"]["extraConfig"]["0-binderspawnermixin"] - - difflines = list( - difflib.context_diff(values_code.splitlines(), py_code.splitlines()) - ) - if difflines: - print("\n".join(difflines)) - print("\n") - print("Values code is not in sync with binderhub/binderspawner_mixin.py") - print( - f"Run `python {sys.argv[0]} --update` to update values.yaml from binderhub/binderspawner_mixin.py" - ) - sys.exit(1) From 7dbb914011e6f8645f28262179072cc7b3fdb7ea Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Fri, 7 Mar 2025 23:17:03 -0800 Subject: [PATCH 200/200] Don't use the helm chart's requirements.txt file elsewhere --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d5862512..e031181c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -156,7 +156,7 @@ jobs: - name: Setup Python package dependencies run: | - pip install --no-binary pycurl -r dev-requirements.txt -r helm-chart/images/binderhub/requirements.txt + pip install --no-binary pycurl -r dev-requirements.txt pip install -e . - name: Install Playwright browser