├── .all-contributorsrc ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yaml └── workflows │ ├── check-setup.yml │ ├── docker-build.yml │ ├── docker-lint.yml │ ├── docker-pull.yml │ ├── shellcheck.yml │ └── yamllint.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── az_account_list.png ├── bash_shell.png ├── blob_storage.png ├── container_events.png ├── container_logs.png ├── create-for-rbac.png ├── find_resource_group_1.png ├── find_resource_group_2.png ├── open_dns_zone.png ├── open_shell_in_azure.png ├── select_a_record.png ├── select_blob_storage.png ├── select_container_instance.png ├── select_resource_group.png ├── set_a_record.png ├── set_subscription.png └── storage_account.png ├── azure.deploy.json ├── docs ├── lets_encrypt_prod_switch.md └── manually-setting-a-records.md ├── scripts ├── docker_pull.py └── requirements.txt ├── src ├── deploy.sh ├── info.sh ├── logs.sh ├── set_a_records.sh ├── setup.sh ├── teardown.sh └── upgrade.sh ├── template-config.json └── templates ├── acr-config-template.yaml ├── acr-secret-template.yaml ├── cluster-issuer-template.yaml ├── config-template.yaml ├── https-acr-config-template.yaml ├── https-config-template.yaml ├── secret-template.yaml └── test-resources.yaml /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "binderhub-deploy", 3 | "projectOwner": "alan-turing-institute", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "gitmoji", 12 | "contributorsSortAlphabetically": true, 13 | "contributors": [ 14 | { 15 | "login": "tmbgreaves", 16 | "name": "Tim Greaves", 17 | "avatar_url": "https://avatars2.githubusercontent.com/u/7603619?v=4", 18 | "profile": "http://www.imperial.ac.uk/people/tim.greaves", 19 | "contributions": [ 20 | "bug", 21 | "code", 22 | "ideas", 23 | "infra", 24 | "platform", 25 | "tool" 26 | ] 27 | }, 28 | { 29 | "login": "ggorman", 30 | "name": "Gerard Gorman", 31 | "avatar_url": "https://avatars1.githubusercontent.com/u/5394691?v=4", 32 | "profile": "https://uk.linkedin.com/in/gerardgorman", 33 | "contributions": [ 34 | "ideas", 35 | "review" 36 | ] 37 | }, 38 | { 39 | "login": "trallard", 40 | "name": "Tania Allard", 41 | "avatar_url": "https://avatars3.githubusercontent.com/u/23552331?v=4", 42 | "profile": "https://trallard.dev", 43 | "contributions": [ 44 | "bug", 45 | "code", 46 | "ideas", 47 | "tutorial", 48 | "question" 49 | ] 50 | }, 51 | { 52 | "login": "dalonsoa", 53 | "name": "Diego", 54 | "avatar_url": "https://avatars1.githubusercontent.com/u/6095790?v=4", 55 | "profile": "https://www.imperial.ac.uk/admin-services/ict/self-service/research-support/rcs/research-software-engineering/", 56 | "contributions": [ 57 | "bug", 58 | "ideas", 59 | "review" 60 | ] 61 | }, 62 | { 63 | "login": "sgibson91", 64 | "name": "Sarah Gibson", 65 | "avatar_url": "https://avatars2.githubusercontent.com/u/44771837?v=4", 66 | "profile": "https://sgibson91.github.io/", 67 | "contributions": [ 68 | "bug", 69 | "code", 70 | "doc", 71 | "ideas", 72 | "infra", 73 | "maintenance", 74 | "platform", 75 | "projectManagement", 76 | "question", 77 | "review", 78 | "tool", 79 | "test" 80 | ] 81 | }, 82 | { 83 | "login": "casperOne", 84 | "name": "Nicholas Paldino", 85 | "avatar_url": "https://avatars1.githubusercontent.com/u/561862?v=4", 86 | "profile": "http://oneframelink.com", 87 | "contributions": [ 88 | "code" 89 | ] 90 | }, 91 | { 92 | "login": "jemrobinson", 93 | "name": "James Robinson", 94 | "avatar_url": "https://avatars2.githubusercontent.com/u/3502751?v=4", 95 | "profile": "https://github.com/jemrobinson", 96 | "contributions": [ 97 | "code" 98 | ] 99 | }, 100 | { 101 | "login": "manics", 102 | "name": "Simon Li", 103 | "avatar_url": "https://avatars2.githubusercontent.com/u/1644105?v=4", 104 | "profile": "http://www.flickr.com/photos/manicstreetpreacher/", 105 | "contributions": [ 106 | "bug" 107 | ] 108 | } 109 | ], 110 | "contributorsPerLine": 7, 111 | "skipCi": false 112 | } 113 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Folders to be ignored 2 | .git/ 3 | .github/ 4 | assets/ 5 | docs/ 6 | scripts/ 7 | 8 | # Files to be ignored 9 | # Generated files 10 | *.json 11 | *.log 12 | 13 | # Specific files 14 | config.json 15 | config.yaml 16 | secret.yaml 17 | 18 | # Repo files 19 | .all-contributorsrc 20 | azure.deploy.json 21 | template-config.json 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the bug 10 | 11 | 12 | ### To Reproduce 13 | 18 | 19 | ### Expected behaviour 20 | 21 | 22 | ### Screenshots 23 | 24 | 25 | ### Desktop (if relevant): 26 | 27 | - OS: [e.g. iOS] 28 | 29 | ### Additional context 30 | 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ### Is your feature request related to a problem? Please describe. 10 | 11 | 12 | ### Describe the solution you'd like 13 | 14 | 15 | ### Describe alternatives you've considered (Optional) 16 | 17 | 18 | ### Additional context 19 | 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 4 | 5 | fix # 6 | 7 | ### What's changed? 8 | 10 | 11 | - 12 | 13 | ### What should a reviewer concentrate their feedback on? 14 | 16 | 17 | - [ ] 18 | - [ ] Everything looks ok? 19 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "monday" 8 | time: "00:00" 9 | timezone: "Europe/London" 10 | -------------------------------------------------------------------------------- /.github/workflows/check-setup.yml: -------------------------------------------------------------------------------- 1 | name: Check Setup 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | jobs: 16 | check-setup: 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | 22 | runs-on: ${{ matrix.os }} 23 | 24 | steps: 25 | - name: Checkout repo 26 | uses: actions/checkout@v3 27 | 28 | - name: Run setup.sh 29 | working-directory: src 30 | run: | 31 | chmod +x *.sh 32 | ./setup.sh 33 | 34 | - name: Check helm major version 35 | run: | 36 | HELM_VERSION=$(helm version --short -c | cut -f1 -d".") 37 | echo "${HELM_VERSION}" 38 | [ "${HELM_VERSION}" == "v3" ] 39 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | defaults: 14 | run: 15 | shell: bash 16 | 17 | jobs: 18 | docker-build: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Docker meta 23 | id: meta 24 | uses: docker/metadata-action@v4.1.1 25 | with: 26 | images: sgibson91/binderhub-setup 27 | tags: | 28 | type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }} 29 | type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} 30 | type=ref,event=pr 31 | type=semver,pattern={{raw}} 32 | flavor: | 33 | latest=false 34 | 35 | - name: Login to DockerHub 36 | if: github.event_name != 'pull_request' 37 | uses: docker/login-action@v2 38 | with: 39 | username: sgibson91 40 | password: ${{ secrets.DOCKER_PASSWORD }} 41 | 42 | - name: Build and push 43 | uses: docker/build-push-action@v3 44 | with: 45 | push: ${{ github.event_name != 'pull_request' }} 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | -------------------------------------------------------------------------------- /.github/workflows/docker-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Dockerfile 2 | 3 | on: 4 | push: 5 | paths: 6 | - "Dockerfile" 7 | 8 | jobs: 9 | dockerlint: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v3 15 | 16 | - name: Lint Dockerfile 17 | uses: luke142367/Docker-Lint-Action@v1.1.1 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/docker-pull.yml: -------------------------------------------------------------------------------- 1 | name: Pull Docker images 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 1 * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | cron-pull: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Python 3.8 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: 3.8 20 | 21 | - name: Install dependencies 22 | working-directory: scripts 23 | run: | 24 | python -m pip install -U pip 25 | pip install -r requirements.txt 26 | 27 | - name: Pull images 28 | working-directory: scripts 29 | run: | 30 | python docker_pull.py 31 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | name: Run shellcheck and shfmt 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**/*.sh" 7 | 8 | jobs: 9 | shell-checker: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v3 15 | 16 | - name: Run shell-checker 17 | uses: luizm/action-sh-checker@master 18 | -------------------------------------------------------------------------------- /.github/workflows/yamllint.yml: -------------------------------------------------------------------------------- 1 | name: yamllint 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**/*.yaml" 7 | - "**/*.yml" 8 | 9 | jobs: 10 | yamllint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Run yamllint 18 | uses: karancode/yamllint-github-action@v2.1.0 19 | with: 20 | yamllint_file_or_dir: templates 21 | yamllint_strict: false 22 | yamllint_comment: false 23 | env: 24 | GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore config JSON file 2 | config.json 3 | 4 | # Ignore specific yaml files 5 | config.yaml 6 | secret.yaml 7 | cluster-issuer.yaml 8 | 9 | # Ignore generated log files 10 | *.log 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at sgibson@turing.ac.uk. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | :space_invader: :tada: Thank you for contributing to the project! :tada: :space_invader: 4 | 5 | The following is a set of guidelines for contributing to `binderhub-deploy` on GitHub. 6 | These are mostly guidelines, not rules. 7 | Use your best judgement and feel free to propose changes to this document in a Pull Request. 8 | 9 | **Table of Contents** 10 | 11 | - [:purple_heart: Code of Conduct](#purple_heart-code-of-conduct) 12 | - [:question: What should I know before I get started?](#question-what-should-i-know-before-i-get-started) 13 | - [:package: `binderhub-deploy`](#package-binderhub-deploy) 14 | - [:open_file_folder: Scripts](#open_file_folder-scripts) 15 | - [:whale: Dockerfile](#whale-dockerfile) 16 | - [:rocket: Deploy to Azure button](#rocket-deploy-to-azure-button) 17 | - [:white_check_mark: Tests](#white_check_mark-tests) 18 | - [:gift: How can I contribute?](#gift-how-can-i-contribute) 19 | - [:bug: Reporting Bugs](#bug-reporting-bugs) 20 | - [:sparkles: Requesting Features](#sparkles-requesting-features) 21 | - [:hatching_chick: Your First Contribution](#hatching_chick-your-first-contribution) 22 | - [:arrow_right: Pull Requests](#arrow_right-pull-requests) 23 | - [:busts_in_silhouette: Acknowledging Contributors](#busts_in_silhouette-acknowledging-contributors) 24 | - [:art: Styleguides](#art-styleguides) 25 | - [:heavy_dollar_sign: Bash Styleguide](#heavy_dollar_sign-bash-styleguide) 26 | - [:pencil: Markdown Styleguide](#pencil-markdown-styleguide) 27 | - [:notebook: Additional Notes](#notebook-additional-notes) 28 | - [:label: Issue and Pull Request Labels](#label-issue-and-pull-request-labels) 29 | 30 | --- 31 | 32 | ## :purple_heart: Code of Conduct 33 | 34 | This project and everyone participating in it is expected to abide by and uphold the [Code of Conduct](CODE_OF_CONDUCT.md). 35 | Please report any unacceptable behaviour to [drsarahlgibson@gmail.com](mailto:drsarahlgibson@gmail.com). 36 | 37 | ## :question: What should I know before I get started? 38 | 39 | ### :package: `binderhub-deploy` 40 | 41 | `binderhub-deploy` is a package designed to automatically deploy a [BinderHub](https://binderhub.readthedocs.io) to [Azure](https://azure.microsoft.com/en-gb/). 42 | 43 | BinderHub is a cloud-based, multi-server platform for sharing reproducible computational environments using a [Jupyter](https://jupyter.org) interface. 44 | [@sgibson91](https://github.com/sgibson91) has given [many talks](https://sgibson91.github.io/speaking) one what Binder/BinderHub/[mybinder.org](https://mybinder.org) is and how it works. 45 | This repository is recommended for those who wish to automate the deployment of their own, private BinderHubs. 46 | 47 | ### :open_file_folder: Scripts 48 | 49 | This tool is based on bash scripts, information about which can be found in the [Usage](README.md#children_crossing-usage) section of the [README](README.md). 50 | 51 | ### :whale: Dockerfile 52 | 53 | This repository contains a [Dockerfile](Dockerfile) that can be built to run the tool. 54 | It also serves as a back-end to the "Deploy to Azure" button. 55 | 56 | The built image of this file is hosted at: [hub.docker.com/repository/docker/sgibson91/binderhub-setup](https://hub.docker.com/repository/docker/sgibson91/binderhub-setup). 57 | The `main` branch is automatically built and tagged as `latest` whereas [GitHub releases and tags](https://github.com/alan-turing-institute/binderhub-deploy/releases) are given the matching image tag. 58 | 59 | When running the image, the parameters defined in [`template-config.json`](template-config.json) would be passed as [environment variables](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file). 60 | 61 | ### :rocket: Deploy to Azure button 62 | 63 | The Deploy to Azure button serves as a graphical user interface to the Docker image that passes information as environment variables at runtime. 64 | The configuration of the button parameters and variables is stored in the [`azure.deploy.json`](azure.deploy.json) file. 65 | 66 | ### :white_check_mark: Tests 67 | 68 | This repository uses [Continuous Integration](https://docs.microsoft.com/en-us/azure/devops/learn/what-is-continuous-integration) tests to check for breaking changes and formatting within the code base. 69 | This repository has the following tests: 70 | 71 | - **[Travis](.travis.yml):** This pipeline runs a [matrix job]() to test that [`setup.sh`](setup.sh) can run on Linux, OSX, and Windows platforms 72 | - **Shellcheck and format:** This [GitHub Action workflow](.github/workflows/shellcheck.yml) tests for formatting and linting of the shell scripts. It runs on the default branch and in Pull Requests and will leave comments on PRs if the test breaks. 73 | - **[YAML formatting](.github/workflows/yamllint.yml):** This GitHub Action workflow check that the YAML templates throughout the repository are well-formatted and readable to prevent errors running the scripts. 74 | 75 | ## :gift: How can I contribute? 76 | 77 | ### :bug: Reporting Bugs 78 | 79 | If something doesn't work the way you expect it to, please check it hasn't already been reported in the repository's [issue tracker](https://github.com/alan-turing-institute/binderhub-deploy/issues). 80 | Bug reports should have the [bug label](https://github.com/alan-turing-institute/binderhub-deploy/issues?q=is%3Aissue+is%3Aopen+label%3Abug), or have a title beginning with [`[BUG]`](https://github.com/alan-turing-institute/binderhub-deploy/issues?q=is%3Aissue+is%3Aopen+%5BBUG%5D). 81 | 82 | If you can't find an issue already reporting your bug, then please feel free to [open a new issue](https://github.com/alan-turing-institute/binderhub-deploy/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BBUG%5D). 83 | This repository has a [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) to help you be as descriptive as possible so we can squash that bug! :muscle: 84 | 85 | ### :sparkles: Requesting Features 86 | 87 | If there was something extra you wish `binderhub-deploy` could do, please check that the feature hasn't already been requested in the project's [issue tracker](https://github.com/alan-turing-institute/binderhub-deploy/issues). 88 | Feature requests should have the [enhancement label](https://github.com/alan-turing-institute/binderhub-deploy/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement). 89 | Please also check the [closed issues](https://github.com/alan-turing-institute/binderhub-deploy/issues?q=is%3Aissue+is%3Aclosed) to make sure the feature has not already been requested but the project maintainers decided against developing it. 90 | 91 | If you find an open issue describing the feature you wish for, you can "+1" the issue by giving a thumbs up reaction on the **top comment of the issue**. 92 | You may also leave any thoughts or offers for support as new comments on the issue. 93 | 94 | If you don't find an issue describing your feature, please [open a feature request](https://github.com/alan-turing-institute/binderhub-deploy/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). 95 | This repository has a [feature request template](.github/ISSUE_TEMPLATE/feature_request.md) to help you map out the feature you'd like. 96 | 97 | ### :hatching_chick: Your First Contribution 98 | 99 | Unsure where to start contributing? 100 | Check out the [good first issue](https://github.com/alan-turing-institute/binderhub-deploy/labels/good%20first%20issue) and [help wanted](https://github.com/alan-turing-institute/binderhub-deploy/labels/help%20wanted) labels to see where the project is looking for input. 101 | Spelling corrections and clarifications to documentation are also always welcome! 102 | 103 | ### :arrow_right: Pull Requests 104 | 105 | A Pull Request is a means for [people to collaboratively review and work on changes](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) before they are introduced into the base branch of the code base. 106 | 107 | To prepare your contribution for review, please follow these steps: 108 | 109 | 1. [Fork this repository](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) 110 | 2. [Create a new branch](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository) on your fork 111 | 1. Where possible and appropriate, please use the following convention when naming your branch: `//`. 112 | For example, if your contribution is fixing a a typo that was flagged in issue number 11, your branch name would be as follows: `fix/11/typo`. 113 | 3. Edit files or add new ones! 114 | 4. [Open your Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) 115 | 1. This repository has a [pull request template](.github/PULL_REQUEST_TEMPLATE.md) which will help you summarise your contribution and help reviewers know where to focus their feedback. 116 | Please complete it where possible and appropriate. 117 | 118 | Congratulations! :tada: 119 | You are now a `binderhub-deploy` developer! :space_invader: 120 | 121 | The project maintainers will then review your Pull Request and may ask for some changes. 122 | Once you and the maintainers are happy, your contribution will be merged! 123 | 124 | ### :busts_in_silhouette: Acknowledging Contributors 125 | 126 | This repository uses [all-contributors](https://allcontributors.org/) to acknowledge the time and expertise of the people who have made this tool into what it is today. 127 | Specifically, all-contributors has an [emoji key](https://allcontributors.org/docs/en/emoji-key) to show the breadth of expertise required for a project like this. 128 | 129 | ## :art: Styleguides 130 | 131 | ### :heavy_dollar_sign: Bash Styleguide 132 | 133 | This repository implements bash linting and formatting via [`shellcheck`](https://github.com/koalaman/shellcheck) and [`shfmt`](https://github.com/mvdan/sh). 134 | These checks are run in a [GitHub Action](.github/workflows/shellcheck.yml) and will leave comments on Pull Requests if issues are found. 135 | This will help us maintain readable code for future contributors. 136 | 137 | ### :pencil: Markdown Styleguide 138 | 139 | Documentation files are written in [Markdown](https://guides.github.com/features/mastering-markdown/). 140 | 141 | When writing Markdown, it is recommended to start a new sentence on a new line and define a new paragraph by leaving a single blank line. 142 | (Check out the raw version of this file for an example!) 143 | While the sentences will render as a single paragraph; when suggestions are made on Pull Requests, the GitHub User Interface will only highlight the affected sentence - not the whole paragraph. 144 | This makes reviews much easier to read! 145 | 146 | ## :notebook: Additional Notes 147 | 148 | ### :label: Issue and Pull Request Labels 149 | 150 | Issues and Pull Requests can have labels assigned to them which indicate at a glance what aspects of the project they describe. 151 | It is also possible to [sort issues by label](https://help.github.com/en/github/managing-your-work-on-github/filtering-issues-and-pull-requests-by-labels) making it easier to track down specific issues. 152 | Below is a table with the currently used labels in the repository. 153 | 154 | | Label | Description | 155 | | :--- | :--- | 156 | | [![azure-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/azure)](https://github.com/alan-turing-institute/binderhub-deploy/labels/azure) | Relating to the Azure deployment | 157 | | [![bug-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/bug)](https://github.com/alan-turing-institute/binderhub-deploy/labels/bug) | Something isn't working | 158 | | [![ci-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/ci)](https://github.com/alan-turing-institute/binderhub-deploy/labels/ci) | Relating to Continuous Integration workflows | 159 | | [![docker-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/docker)](https://github.com/alan-turing-institute/binderhub-deploy/labels/docker) | Relating to the Dockerfile or image | 160 | | [![docs-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/documentation)](https://github.com/alan-turing-institute/binderhub-deploy/labels/documentation) | Edits or improvements to the documentation | 161 | | [![enhancement-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/enhancement)](https://github.com/alan-turing-institute/binderhub-deploy/labels/enhancement) | New feature or request | 162 | | [![good-first-issue](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/good%20first%20issue)](https://github.com/alan-turing-institute/binderhub-deploy/labels/good%20first%20issue) | Good for newcomers | 163 | | [![helm-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/helm)](https://github.com/alan-turing-institute/binderhub-deploy/labels/helm) | Relating to deploying Helm charts | 164 | | [![help wanted](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/help%20wanted)](https://github.com/alan-turing-institute/binderhub-deploy/labels/help%20wanted) | Extra attention is needed | 165 | | [![k8s-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/kubernetes)](https://github.com/alan-turing-institute/binderhub-deploy/labels/kubernetes) | Related to deploying Kubernetes | 166 | | [![linux-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/linux)](https://github.com/alan-turing-institute/binderhub-deploy/labels/linux) | Related to running on Linux | 167 | | [![management](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/management)](https://github.com/alan-turing-institute/binderhub-deploy/labels/management) | Related to managing the project | 168 | | [![osx-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/osx)](https://github.com/alan-turing-institute/binderhub-deploy/labels/osx) | Related to running on MacOS | 169 | | [![windows-label](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/windows)](https://github.com/alan-turing-institute/binderhub-deploy/labels/windows) | Related to running on Windows | 170 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Formerly: https://hub.docker.com/r/microsoft/azure-cli/ 2 | # New repo: https://hub.docker.com/_/microsoft-azure-cli 3 | FROM mcr.microsoft.com/azure-cli:2.0.76 4 | 5 | RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && \ 6 | chmod +x ./kubectl && \ 7 | mv ./kubectl /usr/local/bin/kubectl && \ 8 | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 && \ 9 | chmod 700 get_helm.sh && \ 10 | ./get_helm.sh 11 | 12 | ADD . /app 13 | RUN find /app -type f -name '*.sh' -exec chmod +x {} \; 14 | 15 | WORKDIR /app 16 | 17 | CMD ["/app/src/deploy.sh"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 The Alan Turing Institute 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: This project is no longer maintained, and hence archived :warning: 2 | 3 | # Automatically deploy a BinderHub to Microsoft Azure 4 | 5 | [![mit_license_badge](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Build and Push Docker image](https://github.com/alan-turing-institute/binderhub-deploy/actions/workflows/docker-build.yml/badge.svg)](https://github.com/alan-turing-institute/binderhub-deploy/actions/workflows/docker-build.yml) [![Lint Dockerfile](https://github.com/alan-turing-institute/binderhub-deploy/workflows/Lint%20Dockerfile/badge.svg)](https://github.com/alan-turing-institute/binderhub-deploy/actions?query=workflow%3A%22Lint+Dockerfile%22+branch%3Amain) [![Check Setup](https://github.com/alan-turing-institute/binderhub-deploy/workflows/Check%20Setup/badge.svg)](https://github.com/alan-turing-institute/binderhub-deploy/actions?query=workflow%3A%22Check+Setup%22+branch%3Amain) [![Run shellcheck and shfmt](https://github.com/alan-turing-institute/binderhub-deploy/workflows/Run%20shellcheck%20and%20shfmt/badge.svg)](https://github.com/alan-turing-institute/binderhub-deploy/actions?query=workflow%3A%22Run+shellcheck+and+shfmt%22+branch%3Amain) [![yamllint](https://github.com/alan-turing-institute/binderhub-deploy/workflows/yamllint/badge.svg)](https://github.com/alan-turing-institute/binderhub-deploy/actions?query=workflow%3Ayamllint+branch%3Amain) [![Code of Conduct](https://img.shields.io/static/v1?label=Code%20of&message=Conduct&color=blueviolet)](CODE_OF_CONDUCT.md) [![Contributing Guidelines](https://img.shields.io/static/v1?label=Contributing&message=Guidelines&color=blueviolet)](CONTRIBUTING.md) [![good first issue](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/good%20first%20issue)](https://github.com/alan-turing-institute/binderhub-deploy/labels/good%20first%20issue) [![GitHub labels](https://img.shields.io/github/labels/alan-turing-institute/binderhub-deploy/help%20wanted)](https://github.com/alan-turing-institute/binderhub-deploy/labels/help%20wanted) 6 | [![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) 7 | 8 | 9 | [BinderHub](https://binderhub.readthedocs.io/en/latest/index.html) is a cloud-based, multi-server technology used for hosting repoducible computing environments and interactive Jupyter Notebooks built from code repositories. 10 | 11 | This repository contains a set of scripts to automatically deploy a BinderHub onto [Microsoft Azure](https://azure.microsoft.com/en-gb/), and connect either a [Docker Hub](https://hub.docker.com/) account/organisation or an [Azure Container Registry](https://azure.microsoft.com/en-gb/services/container-registry/), so that you can host your own [Binder](https://mybinder.readthedocs.io/en/latest/) service. 12 | 13 | You will require a Microsoft Azure account and subscription. 14 | A Free Trial subscription can be obtained [here](https://azure.microsoft.com/en-gb/free/). 15 | You will be asked to provide a credit card for verification purposes. 16 | **You will not be charged.** 17 | Your resources will be frozen once your subscription expires, then deleted if you do not reactivate your account within a given time period. 18 | If you are building a BinderHub as a service for an organisation, your institution may already have an Azure account. 19 | You should contact your IT Services for further information regarding permissions and access (see the [Service Principal Creation](#sparkles-service-principal-creation) section below). 20 | 21 | Please read our :purple_heart: [Code of Conduct](CODE_OF_CONDUCT.md) :purple_heart: and :space_invader: [Contributing Guidelines](CONTRIBUTING.md) :space_invader: 22 | 23 | **Table of Contents:** 24 | 25 | - [:children_crossing: Usage](#children_crossing-usage) 26 | - [:package: Choosing between Docker Hub and Azure Container Registry](#package-choosing-between-docker-hub-and-azure-container-registry) 27 | - [:closed_lock_with_key: Enabling HTTPS for a Domain Name](#closed_lock_with_key-enabling-https-for-a-domain-name) 28 | - [:vertical_traffic_light: `setup.sh`](#vertical_traffic_light-setupsh) 29 | - [:rocket: `deploy.sh`](#rocket-deploysh) 30 | - [:inbox_tray: `set-a-records.sh`](inbox_tray-set-a-recordssh) 31 | - [:bar_chart: `logs.sh`](#bar_chart-logssh) 32 | - [:information_source: `info.sh`](#information_source-infosh) 33 | - [:arrow_up: `upgrade.sh`](#arrow_up-upgradesh) 34 | - [:boom: `teardown.sh`](#boom-teardownsh) 35 | - [:rocket: "Deploy to Azure" Button](#rocket-deploy-to-azure-button) 36 | - [:sparkles: Service Principal Creation](#sparkles-service-principal-creation) 37 | - [:chart_with_upwards_trend: Monitoring Deployment Progress](#chart_with_upwards_trend-monitoring-deployment-progress) 38 | - [:package: Retrieving Deployment Output from Azure](#package-retrieving-deployment-output-from-azure) 39 | - [:unlock: Accessing your BinderHub after Deployment](#unlock-accessing-your-binderhub-after-deployment) 40 | - [:house_with_garden: Running the Container Locally](#house_with_garden-running-the-container-locally) 41 | - [:art: Customising your BinderHub Deployment](#art-customising-your-binderhub-deployment) 42 | - [:computer: Developers Guide](#computer-developers-guide) 43 | - [:wrench: Building the Docker image for testing](#wrench-building-the-docker-image-for-testing) 44 | - [:label: Tagging a Release](#label-tagging-a-release) 45 | - [:purple_heart: Contributors](#purple_heart-contributors) 46 | 47 | --- 48 | 49 | ## :children_crossing: Usage 50 | 51 | This repo can either be run locally or as "Platform as a Service" through the "Deploy to Azure" button in the ["Deploy to Azure" Button](#rocket-deploy-to-azure-button) section. 52 | 53 | To use these scripts locally, clone this repo and change into the directory. 54 | 55 | ```bash 56 | git clone https://github.com/alan-turing-institute/binderhub-deploy.git 57 | cd binderhub-deploy 58 | ``` 59 | 60 | To make the scripts executable and then run them, do the following: 61 | 62 | ```bash 63 | cd src 64 | chmod 700 .sh 65 | ./.sh 66 | ``` 67 | 68 | [**NOTE:** The above command is UNIX specific. If you are running Windows 10, [this blog post](https://www.windowscentral.com/how-install-bash-shell-command-line-windows-10) discusses using a bash shell in Windows.] 69 | 70 | To build the BinderHub, you should run `setup.sh` first (to install the required command line tools), then `deploy.sh` (which will build the BinderHub). 71 | Once the BinderHub is deployed, you can run `logs.sh` and `info.sh` to get the JupyterHub logs and IP addresses respectively. 72 | `teardown.sh` should _only_ be used to delete your BinderHub deployment. 73 | 74 | You need to create a file called `config.json` which has the format described in the code block below. 75 | Fill the quotation marks with your desired namespaces, etc. 76 | `config.json` is git-ignored so sensitive information, such as passwords and Service Principals, cannot not be pushed to GitHub. 77 | 78 | - For a list of available data centre regions, [see here](https://azure.microsoft.com/en-gb/global-infrastructure/locations/). 79 | This should be a _region_ and **not** a _location_, for example "West Europe" or "Central US". 80 | These can be equivalently written as `westeurope` and `centralus`, respectively. 81 | - For a list of available Linux Virtual Machines, [see here](https://docs.microsoft.com/en-gb/azure/virtual-machines/linux/sizes-general). 82 | This should be something like, for example `Standard_D2s_v3`. 83 | - The versions of the BinderHub Helm Chart can be found [here](https://jupyterhub.github.io/helm-chart/#development-releases-binderhub) and are of the form `0.2.0-`. 84 | It is advised to select the most recent version unless you specifically require an older one. 85 | - If you are deploying an Azure Container Registry, find out more about the SKU tiers [here](https://docs.microsoft.com/en-gb/azure/container-registry/container-registry-skus). 86 | 87 | ```json 88 | { 89 | "container_registry": "", // Choose Docker Hub or ACR with 'dockerhub' or 'azurecr' values, respectively. 90 | "enable_https": "false", // Choose whether to enable HTTPS with cert-manager. Boolean. 91 | "acr": { 92 | "registry_name": null, // Name to give the ACR. This must be alpha-numerical and unique to Azure. 93 | "sku": "Basic" // The SKU capacity and pricing tier for the ACR 94 | }, 95 | "azure": { 96 | "subscription": "", // Azure subscription name or ID (a hex-string) 97 | "res_grp_name": "", // Azure Resource Group name 98 | "location": "", // Azure Data Centre region 99 | "node_count": 1, // Number of nodes to deploy. 3 is preferrable for a stable cluster, but may be liable to caps. 100 | "vm_size": "Standard_D2s_v3", // Azure virtual machine type to deploy 101 | "sp_app_id": null, // Azure service principal ID (optional) 102 | "sp_app_key": null, // Azure service principal password (optional) 103 | "sp_tenant_id": null, // Azure tenant ID (optional) 104 | "log_to_blob_storage": false // Store logs in blob storage when not running from a container 105 | }, 106 | "binderhub": { 107 | "name": "", // Name of your BinderHub 108 | "version": "", // Helm chart version to deploy, should be 0.2.0- 109 | "image_prefix": "" // The prefix to preppend to Docker images (e.g. "binder-prod") 110 | }, 111 | "docker": { 112 | "username": null, // Docker username (can be supplied at runtime) 113 | "password": null, // Docker password (can be supplied at runtime) 114 | "org": null // A Docker Hub organisation to push images to (optional) 115 | }, 116 | "https:": { 117 | "certmanager_version": null, // Version of cert-manager to install 118 | "contact_email": null, // Contact email for Let's Encrypt 119 | "domain_name": null, // Domain name to issue certificates for 120 | "nginx_version": null // Version on nginx-ingress to install 121 | } 122 | } 123 | ``` 124 | 125 | You can copy [`template-config.json`](./template-config.json) should you require. 126 | 127 | **Please note that all entries in `template-config.json` must be surrounded by double quotation marks (`"`), with the exception of `node_count`.** 128 | 129 | #### Important for Free Trial subscriptions 130 | 131 | If you have signed up to an Azure Free Trial subscription, you are not allowed to deploy more than 4 **cores**. 132 | How many cores you deploy depends on your choice of `node_count` and `vm_size`. 133 | 134 | For example, a `Standard_D2s_v3` machine has 2 cores. 135 | Therefore, setting `node_count` to 2 will deploy 4 cores and you will have reached your quota for cores on your Free Trial subscription. 136 | 137 | ### :package: Choosing between Docker Hub and Azure Container Registry 138 | 139 | To select either a Docker Hub account/organisation or an Azure Container Registry (ACR), you must set the top-level `container_registry` key in `config.json` to either `dockerhub` or `azurecr` respectively. 140 | This will tell `deploy.sh` which variables and YAML templates to use. 141 | Then fill in the values under either the `dockerhub` or `acr` key as required. 142 | 143 | Using a Docker Hub account/organisation has the benefit of being relatively simple to set up. 144 | However, all the BinderHub images pushed there will be publicly available. 145 | For a few extra steps, deploying an ACR will allow the BinderHub images to be pushed to a private repository. 146 | 147 | #### Important Caveats when deploying an ACR 148 | 149 | **Service Principal:** 150 | 151 | In the [Service Principal Creation](#sparkles-service-principal-creation) section, we cover how to create a Service Principal in order to deploy a BinderHub. 152 | When following these steps, the `--role` argument of `Contributor` should be replaced with `Owner`. 153 | This is because the Service Principal will need the [`AcrPush`](https://docs.microsoft.com/en-gb/azure/role-based-access-control/built-in-roles#acrpush) role in order to push images to the ACR and the `Contributor` role does not have permission to create new role assignments. 154 | 155 | ### :vertical_traffic_light: `setup.sh` 156 | 157 | This script checks whether the required command line tools are already installed. 158 | If any are missing, the script uses the system package manager or [`curl`](https://curl.haxx.se/docs/) to install the command line interfaces (CLIs). 159 | The CLIs to be installed are: 160 | 161 | - [Microsoft Azure (`azure-cli`)](https://docs.microsoft.com/en-gb/cli/azure/install-azure-cli-linux?view=azure-cli-latest#install-or-update) 162 | - [Kubernetes (`kubectl`)](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-using-curl) 163 | - [Helm (`helm`)](https://helm.sh/docs/using_helm/#from-script) 164 | 165 | Any dependencies that are not automatically installed by these packages will also be installed. 166 | 167 | ### :closed_lock_with_key: Enabling HTTPS for a Domain Name 168 | 169 | If you have a domain name that you would like your BinderHub to be hosted at, the package can configure a [DNS Zone](https://docs.microsoft.com/en-gb/azure/dns/dns-zones-records#dns-zones) to host the records for your domain name and configure the BinderHub to use these addresses rather than raw IP addresses. 170 | HTTPS certificates will also be requested for the [DNS records](https://docs.microsoft.com/en-us/azure/dns/dns-zones-records#dns-records) using [`cert-manager`](https://cert-manager.io/docs/) and [Let's Encrypt](https://letsencrypt.org/). 171 | 172 | #### :hammer: Manual steps required 173 | 174 | While the package tries to automate as much as possible, when enabling HTTPS there are still a few steps that the user will have to do manually. 175 | 176 | 1) **Delegate the domain to the name servers** 177 | 178 | The script will return four name servers that are hosting the DNS Zone, the will be saved to the log file `name-servers.log`. 179 | Your parent domain NS records need to be updated to delegate to these name servers (see the [Azure documentation](https://docs.microsoft.com/en-us/azure/dns/dns-delegate-domain-azure-dns#delegate-the-domain)). 180 | How this is achieved will be different depending on your domain registrar. 181 | 182 | 2) **Point the A records to the Load Balancer IP Address** 183 | 184 | Two A records are created for the Binder page and the JupyterHub and these records need to be set to the public IP address of the cluster's load balancer. 185 | The package tries to complete this step automatically but often fails, due to the long-running nature of Azure's process to update the CLI. 186 | It is recommended to wait some time (overnight is best) and then run `set-a-records.sh`. 187 | Alternatively, there are [manual instructions](docs/manually-setting-a-records.md) for setting the A records in the Azure Portal. 188 | 189 | 3) **Switching from Let's Encrypt staging to production** 190 | 191 | Let's Encrypt provides a [staging platform](https://letsencrypt.org/docs/staging-environment/) to test against and this is the environment the package will request certificates from. 192 | Once you have [verified the staging certificates](https://www.cyberciti.biz/faq/test-ssl-certificates-diagnosis-ssl-certificate/) have been issued correctly, the user must switch to requesting certificates from Let's Encrypt's production environment to receive trusted certificates. 193 | [Instructions for switching environments](docs/lets_encrypt_prod_switch.md). 194 | 195 | ### :rocket: `deploy.sh` 196 | 197 | This script reads in values from `config.json` and deploys a Kubernetes cluster. 198 | It then creates `config.yaml` and `secret.yaml` files which are used to install the BinderHub using the templates in the [`templates` folder](./templates/). 199 | 200 | If you have chosen a Docker Hub account/organisation, the script will ask for your Docker ID and password if you haven't supplied them in the config file. 201 | The ID is your Docker username, **NOT** the associated email. 202 | If you have provided a Docker organisation in `config.json`, then Docker ID **MUST** be a member of this organisation. 203 | 204 | If you have chosen an ACR, the script will create one and assign the `AcrPush` role to your Service Principal. 205 | The registry server and Service Principal credentials will then be parsed into `config.yaml` and `secret.yaml` so that the BinderHub can connect to the ACR. 206 | 207 | If you have requested HTTPS to be enabled, the script will create a DNS Zone and A records for the Binder and JupyterHub endpoints. 208 | The [`nginx-ingress`](https://github.com/helm/charts/tree/master/stable/nginx-ingress) and [`cert-manager`](https://github.com/jetstack/cert-manager) helm charts will also be installed to provide a load balancer and automated requests for certificates from Let's Encrypt, respectively. 209 | 210 | Both a JupyterHub and BinderHub are installed via a Helm Chart onto the deployed Kubernetes cluster and the `config.yaml` file is updated with the JupyterHub IP address. 211 | 212 | `config.yaml` and `secret.yaml` are both git-ignored so that secrets cannot be pushed back to GitHub. 213 | 214 | The script also outputs log files (`.log`) for each stage of the deployment. 215 | These files are also git-ignored. 216 | 217 | If the `azure.log_to_blob_storage` value in `config.json` is set to `true` the script is running from the command line, then the log files will be stored in blob storage. 218 | 219 | ### :inbox_tray: `set-a-records.sh` 220 | 221 | :rotating_light: This script is only relevant if deploying a BinderHub with a domain name and HTTPS certificates :rotating_light: 222 | 223 | This script reads in values from `config.json` and try to set the Kubernetes public IP address to the `binder` and `hub` A records in the DNS Zone. 224 | 225 | ### :bar_chart: `logs.sh` 226 | 227 | This script will print the JupyterHub logs to the terminal to assist with debugging issues with the BinderHub. 228 | It reads from `config.json` in order to get the BinderHub name. 229 | 230 | ### :information_source: `info.sh` 231 | 232 | This script will print the pod status of the Kubernetes cluster and the IP addresses of both the JupyterHub and BinderHub to the terminal. 233 | It reads the BinderHub name from `config.json`. 234 | 235 | ### :arrow_up: `upgrade.sh` 236 | 237 | This script will automatically upgrade the Helm Chart deployment configuring the BinderHub and then prints the Kubernetes pods. 238 | It reads the BinderHub name and Helm Chart version from `config.json`. 239 | 240 | ### :boom: `teardown.sh` 241 | 242 | This script will purge the Helm Chart release, delete the Kubernetes namespace and then delete the Azure Resource Group containing the computational resources. 243 | It will read the namespaces from `config.json`. 244 | The user should check the [Azure Portal](https://portal.azure.com/) to verify the resources have been deleted. 245 | It will also purge the cluster information from your `kubectl` configuration file. 246 | 247 | ## :rocket: "Deploy to Azure" Button 248 | 249 | To deploy [BinderHub](https://binderhub.readthedocs.io/) to Azure in a single click (and some form-filling), use the deploy button below. 250 | 251 | [![Deploy to Azure](https://azuredeploy.net/deploybutton.svg)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Falan-turing-institute%2Fbinderhub-deploy%2Fmain%2Fazure.deploy.json) 252 | 253 | ### :sparkles: Service Principal Creation 254 | 255 | You will be asked to provide a [Service Principal](https://docs.microsoft.com/en-gb/azure/active-directory/develop/app-objects-and-service-principals) in the form launched when you click the "Deploy to Azure" button above. 256 | 257 | [**NOTE:** The following instructions can also be run in a local terminal session. 258 | They will require the Azure command line to be installed, so make sure to run [`setup.sh`](src/setup.sh) first.] 259 | 260 | To create a Service Principal, go to the [Azure Portal](https://portal.azure.com/) (and login!) and open the Cloud Shell: 261 | 262 | ![Open Shell in Azure](assets/open_shell_in_azure.png) 263 | 264 | You may be asked to create storage when you open the shell. 265 | This is expected, click "Create". 266 | 267 | Make sure the shell is set to Bash, not PowerShell. 268 | 269 | ![Bash Shell](assets/bash_shell.png) 270 | 271 | Set the subscription you'd like to deploy your BinderHub on. 272 | 273 | ```bash 274 | az account set --subscription 275 | ``` 276 | 277 | This image shows the command being executed for an "Azure Pass - Sponsorship" subscription. 278 | 279 | ![Set Subscription](assets/set_subscription.png) 280 | 281 | You will need the subscription ID, which you can retrieve by running: 282 | 283 | ```bash 284 | az account list --refresh --output table 285 | ``` 286 | 287 | ![List Subscriptions](assets/az_account_list.png) 288 | 289 | Next, create the Service Principal with the following command. 290 | Make sure to give it a sensible name! 291 | 292 | ```bash 293 | az ad sp create-for-rbac \ 294 | --name binderhub-sp \ 295 | --role Contributor \ 296 | --scope /subscriptions/ 297 | ``` 298 | 299 | **NOTE:** If you are deploying an ACR rather than connecting to Docker Hub, then this command should be: 300 | 301 | ```bash 302 | az ad sp create-for-rbac \ 303 | --name binder\ 304 | --scope /subscriptions/ 305 | ``` 306 | 307 | ![Create Service Principal](assets/create-for-rbac.png) 308 | 309 | The fields `appId`, `password` and `tenant` are the required pieces of information. 310 | These should be copied into the "Service Principal App ID", "Service Principal App Key" and "Service Principal Tenant ID" fields in the form, respectively. 311 | 312 | **Keep this information safe as the password cannot be recovered after this step!** 313 | 314 | ### :chart_with_upwards_trend: Monitoring Deployment Progress 315 | 316 | To monitor the progress of the blue-button deployment, go to the [Azure portal](https://portal.azure.com/) and select "Resource Groups" from the left hand pane. 317 | Then in the central pane select the resource group you chose to deploy into. 318 | 319 | ![Select Resource Group](assets/select_resource_group.png) 320 | 321 | This will give you a right hand pane containing the resources within the group. 322 | You may need to "refresh" until you see a new container instance. 323 | 324 | ![Select Container Instance](assets/select_container_instance.png) 325 | 326 | When it appears, select it and then in the new pane go to "Settings->Containers". 327 | You should see your new container listed. 328 | 329 | ![Container Events](assets/container_events.png) 330 | 331 | Select it, then in the lower right hand pane select "Logs". 332 | You may need to "refresh" this to display the logs until the container starts up. 333 | The logs are also not auto-updating, so keep refreshing them to see progress. 334 | 335 | ![Container Logs](assets/container_logs.png) 336 | 337 | ### :package: Retrieving Deployment Output from Azure 338 | 339 | When BinderHub is deployed using the "Deploy to Azure" button (or with a local container), output logs, YAML files, and ssh keys are pushed to an Azure storage account to preserve them once the container exits. 340 | The storage account is created in the same resource group as the Kubernetes cluster, and files are pushed into a storage blob within the account. 341 | 342 | Both the storage blob name and the storage account name are derived from the name you gave to your BinderHub instance, but may be modified and/or have a random seed appended. 343 | To find the storage account name, navigate to your resource group by selecting "Resource Groups" in the left-most panel of the [Azure Portal](https://portal.azure.com/), then clicking on the resource group containing your BinderHub instance. 344 | Along with any pre-existing resources (for example, if you re-used an existing resource group), you should see three new resources: a container instance, a Kubernetes service, and a storage account. 345 | Make a note of the name of the storage account (referred to in the following commands as `ACCOUNT_NAME`) then select this storage account. 346 | 347 | ![Storage Account](assets/storage_account.png) 348 | 349 | In the new pane that opens, select "Blobs" from the "Services" section. 350 | You should see a single blob listed. 351 | Make a note of the name of this blob, which will be `BLOB_NAME` in the following commands. 352 | 353 | ![Blob Storage](assets/blob_storage.png) 354 | 355 | ![Select Blob Storage](assets/select_blob_storage.png) 356 | 357 | The Azure CLI can be used to fetch files from the blob (either in the cloud shell in the [Azure Portal](https://portal.azure.com), or in a local terminal session if you've run [`setup.sh`](.setup.sh) first). 358 | Files are fetched into a local directory, **which must already exist**, referred to as `OUTPUT_DIRECTORY` in the following commands. 359 | 360 | You can run [`setup.sh`](src/setup.sh) to install the Azure CLI or use the cloud shell on the [Azure Portal](https://portal.azure.com). 361 | 362 | To fetch all files: 363 | 364 | ```bash 365 | az storage blob download-batch \ 366 | --account-name \ 367 | --source \ 368 | --pattern "*" \ 369 | --destination "" 370 | ``` 371 | 372 | The `--pattern` argument can be used to fetch particular files, for example all log files: 373 | 374 | ```bash 375 | az storage blob download-batch \ 376 | --account-name \ 377 | --source \ 378 | --pattern "*.log" \ 379 | --destination "" 380 | ``` 381 | 382 | To fetch a single file, specify `REMOTE_FILENAME` for the name of the file in blob storage, and `LOCAL_FILENAME` for the filename it will be fetched into: 383 | 384 | ```bash 385 | az storage blob download \ 386 | --account-name \ 387 | --container-name \ 388 | --name \ 389 | --file 390 | ``` 391 | 392 | For full documentation, see the [`az storage blob` documentation](https://docs.microsoft.com/en-gb/cli/azure/storage/blob?view=azure-cli-latest). 393 | 394 | ### :unlock: Accessing your BinderHub after Deployment 395 | 396 | Once the deployment has succeeded and you've downloaded the log files, visit the IP address of your Binder page to test it's working. 397 | 398 | The Binder IP address can be found by running the following: 399 | 400 | ```bash 401 | cat /binder-ip.log 402 | ``` 403 | 404 | A good repository to test your BinderHub with is [binder-examples/requirements](https://github.com/binder-examples/requirements) 405 | 406 | ## :house_with_garden: Running the Container Locally 407 | 408 | The third way to deploy BinderHub to Azure would be to pull the Docker image and run it directly, parsing the values you would have entered in `config.json` as environment variables. 409 | 410 | You will need the Docker CLI installed. 411 | Installation instructions can be found [here](https://docs.docker.com/v17.12/install/). 412 | 413 | First, pull the `binderhub-setup` image. 414 | 415 | ```bash 416 | docker pull sgibson91/binderhub-setup: 417 | ``` 418 | 419 | where `` is your chosen image tag. 420 | 421 | A list of availabe tags can be found [here](https://cloud.docker.com/repository/docker/sgibson91/binderhub-setup/tags). 422 | It is recommended to use the most recent version number. 423 | The `latest` tag is the most recent build from the default branch and may be subject fluctuations. 424 | 425 | Then, run the container with the following arguments, replacing the `<>` fields as necessary: 426 | 427 | ```bash 428 | docker run \ 429 | -e "AKS_NODE_COUNT=1" \ # Required 430 | -e "AKS_NODE_VM_SIZE=Standard_D2s_v3" \ # Required 431 | -e "AZURE_SUBSCRIPTION=" \ # Required 432 | -e "BINDERHUB_CONTAINER_MODE=true" \ # Required 433 | -e "BINDERHUB_NAME=" \ # Required 434 | -e "BINDERHUB_VERSION=" \ # Required 435 | -e "CONTAINER_REGISTRY=" \ # Required 436 | -e "DOCKER_IMAGE_PREFIX=binder-dev" \ # Required 437 | -e "DOCKERHUB_ORGANISATION=" \ 438 | -e "DOCKERHUB_PASSWORD=" \ 439 | -e "DOCKERHUB_USERNAME=" \ 440 | -e "REGISTRY_NAME=" \ 441 | -e "REGISTRY_SKU=Basic" \ 442 | -e "RESOURCE_GROUP_LOCATION=westeurope" \ # Required 443 | -e "RESOURCE_GROUP_NAME=" \ # Required 444 | -e "SP_APP_ID=" \ # Required 445 | -e "SP_APP_KEY=" \ # Required 446 | -e "SP_TENANT_ID=" \ # Required 447 | -it sgibson91/binderhub-setup: 448 | ``` 449 | 450 | The output will be printed to your terminal and the files will be pushed to blob storage, as in the button deployment. 451 | See the [Retrieving Deployment Output from Azure](#package-retrieving-deployment-output-from-azure) section for how to return these files. 452 | 453 | ## :art: Customising your BinderHub Deployment 454 | 455 | Customising your BinderHub deployment is as simple as editing `config.yaml` and/or `secret.yaml` and then upgrading the BinderHub Helm Chart. 456 | The Helm Chart can be upgraded by running [`upgrade.sh`](src/upgrade.sh) (make sure you have the CLIs installed by running [`setup.sh`](src/setup.sh) first). 457 | 458 | The Jupyter guide to customising the underlying JupyterHub can be found [here](https://zero-to-jupyterhub.readthedocs.io/en/latest/extending-jupyterhub.html). 459 | 460 | The BinderHub guide for changing the landing page logo can be found [here](https://binderhub.readthedocs.io/en/latest/customizing.html#template-customization). 461 | 462 | ## :computer: Developers Guide 463 | 464 | ### :wrench: Building the Docker image for testing 465 | 466 | The Docker image will automatically be built by Docker Hub when new pushes are made to `main`. 467 | However, a developer may wish to build the image to test deployments before merging code. 468 | 469 | Firstly, make sure `config.json` has been removed from the repository. 470 | Otherwise, secrets within the file may be built into the image. 471 | 472 | The command to build a Docker image from the root of the repo is as follows. 473 | 474 | ```bash 475 | docker build -t /binderhub-setup: . 476 | ``` 477 | 478 | It is not necessary to push this image to a container registry. 479 | But if you choose to do so, the command is as follows. 480 | 481 | ```bash 482 | docker push //binderhub-setup: 483 | ``` 484 | 485 | ### :label: Tagging a Release 486 | 487 | Docker Hub will automatically build the image from the repo with every push to `main` and tag this as `latest`. 488 | 489 | To release a specific version, update the [Azure ARM template](https://github.com/alan-turing-institute/binderhub-deploy/blob/main/azure.deploy.json) with the new/desired version on line [166](https://github.com/alan-turing-institute/binderhub-deploy/blob/main/azure.deploy.json#L166) and the block starting at line [170](https://github.com/alan-turing-institute/binderhub-deploy/blob/main/azure.deploy.json#L170). 490 | We follow [SemVer](https://semver.org/) versioning format. 491 | 492 | Once the Pull Request containing the new code/version/release has been merged, run the following commands, where `vX.Y.Z` is the new/desired version release. 493 | 494 | ```bash 495 | git checkout main 496 | git pull 497 | git tag -a vX.Y.Z # For an annotated tag 498 | git tag -m vX.Y.Z # For a lightweight tag 499 | git tag vX.Y.Z # For a tag with no extra data 500 | git push --tags 501 | ``` 502 | 503 | This will trigger Docker Hub to build an image with the SemVer version as a tag. 504 | 505 | See the following documentation for information on tagging: 506 | 507 | - 508 | - 509 | 510 | ## :purple_heart: Contributors 511 | 512 | Please read our :purple_heart: [Code of Conduct](CODE_OF_CONDUCT.md) :purple_heart: and :space_invader: [Contributing Guidelines](CONTRIBUTING.md) :space_invader: to get you started! 513 | 514 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 |

Diego

🐛 🤔 👀

Gerard Gorman

🤔 👀

James Robinson

💻

Nicholas Paldino

💻

Sarah Gibson

🐛 💻 📖 🤔 🚇 🚧 📦 📆 💬 👀 🔧 ⚠️

Simon Li

🐛

Tania Allard

🐛 💻 🤔 💬

Tim Greaves

🐛 💻 🤔 🚇 📦 🔧
533 | 534 | 535 | 536 | 537 | -------------------------------------------------------------------------------- /assets/az_account_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/az_account_list.png -------------------------------------------------------------------------------- /assets/bash_shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/bash_shell.png -------------------------------------------------------------------------------- /assets/blob_storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/blob_storage.png -------------------------------------------------------------------------------- /assets/container_events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/container_events.png -------------------------------------------------------------------------------- /assets/container_logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/container_logs.png -------------------------------------------------------------------------------- /assets/create-for-rbac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/create-for-rbac.png -------------------------------------------------------------------------------- /assets/find_resource_group_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/find_resource_group_1.png -------------------------------------------------------------------------------- /assets/find_resource_group_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/find_resource_group_2.png -------------------------------------------------------------------------------- /assets/open_dns_zone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/open_dns_zone.png -------------------------------------------------------------------------------- /assets/open_shell_in_azure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/open_shell_in_azure.png -------------------------------------------------------------------------------- /assets/select_a_record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/select_a_record.png -------------------------------------------------------------------------------- /assets/select_blob_storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/select_blob_storage.png -------------------------------------------------------------------------------- /assets/select_container_instance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/select_container_instance.png -------------------------------------------------------------------------------- /assets/select_resource_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/select_resource_group.png -------------------------------------------------------------------------------- /assets/set_a_record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/set_a_record.png -------------------------------------------------------------------------------- /assets/set_subscription.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/set_subscription.png -------------------------------------------------------------------------------- /assets/storage_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgibson91/binderhub-deploy/13c71f7ddb65af2e4ef2b51460e408e621358524/assets/storage_account.png -------------------------------------------------------------------------------- /azure.deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "servicePrincipalAppId": { 6 | "type": "securestring", 7 | "minLength": 1, 8 | "metadata": { 9 | "description": "ID of your Service Principal. Will be used by the deploy script to create Azure resources on your behalf." 10 | } 11 | }, 12 | "servicePrincipalAppKey": { 13 | "type": "securestring", 14 | "minLength": 1, 15 | "metadata": { 16 | "description": "Password of your Service Principal. Will be used by the deploy script to create Azure resources on your behalf." 17 | } 18 | }, 19 | "servicePrincipalTenantId": { 20 | "type": "securestring", 21 | "minLength": 1, 22 | "metadata": { 23 | "description": "Tenant ID of your Service Principal. Will be used by the deploy script to create Azure resources on your behalf." 24 | } 25 | }, 26 | "binderhubName": { 27 | "type": "string", 28 | "minLength": 1, 29 | "metadata": { 30 | "description": "Namespace to deploy the BinderHub into on the Kubernetes cluster. This will be used to manage the BinderHub resources. Must also be unique across Azure. Your deployment will be accessible at ..cloudapp.azure.com" 31 | } 32 | }, 33 | "binderhubVersion": { 34 | "type": "string", 35 | "minLength": 1, 36 | "metadata": { 37 | "description": "The version of the BinderHub Helm Chart to be deployed. This normally takes the form of versionnumber-hash, ie, 0.2.0-ae57d8. Refer to https://jupyterhub.github.io/helm-chart/#development-releases-binderhub for valid version numbers." 38 | } 39 | }, 40 | "aksNodeCount": { 41 | "type": "int", 42 | "minValue": 1, 43 | "defaultValue": 3, 44 | "metadata": { 45 | "description": "The initial number of nodes to create in the AKS cluster." 46 | } 47 | }, 48 | "aksNodeVmSize": { 49 | "type": "string", 50 | "minLength": 1, 51 | "defaultValue": "Standard_D2s_v3", 52 | "metadata": { 53 | "description": "The default VM size to use for the AKS cluster." 54 | } 55 | }, 56 | "imagePrefix": { 57 | "type": "string", 58 | "minLength": 1, 59 | "metadata": { 60 | "description": "Prefix to use for docker containers created by the BinderHub" 61 | } 62 | }, 63 | "containerRegistry": { 64 | "type": "string", 65 | "minLength": 1, 66 | "defaultValue": "dockerhub", 67 | "metadata": { 68 | "description": "Choose between Docker Hub or Azure Container Registry" 69 | }, 70 | "allowedValues": [ 71 | "dockerhub", 72 | "azurecr" 73 | ] 74 | }, 75 | "dockerHubUsername": { 76 | "type": "string", 77 | "minLength": 1, 78 | "defaultValue": "null", 79 | "metadata": { 80 | "description": "The name of a valid Docker Hub user account." 81 | } 82 | }, 83 | "dockerHubPassword": { 84 | "type": "securestring", 85 | "minLength": 4, 86 | "defaultValue": "null", 87 | "metadata": { 88 | "description": "The user password for Docker Hub" 89 | } 90 | }, 91 | "dockerHubOrganisation": { 92 | "type": "string", 93 | "defaultValue": "null", 94 | "metadata": { 95 | "description": "Organisation to use for Docker Hub; this is an optional value. Docker Username must be a member of this organisation if used." 96 | } 97 | }, 98 | "containerRegistryName": { 99 | "type": "string", 100 | "minLength": 4, 101 | "maxLength": 50, 102 | "defaultValue": "null", 103 | "metadata": { 104 | "description": "Name to be given to the Azure Container Registry. Must be unique to Azure." 105 | } 106 | }, 107 | "containerRegistrySku": { 108 | "type": "string", 109 | "minLength": 1, 110 | "defaultValue": "Basic", 111 | "metadata": { 112 | "description": "Capacity and pricing tier for the Azure Container Registry. See the following website for details: https://docs.microsoft.com/en-us/azure/container-registry/container-registry-skus" 113 | }, 114 | "allowedValues": [ 115 | "Basic", 116 | "Standard", 117 | "Premium" 118 | ] 119 | }, 120 | "enableHttps": { 121 | "type": "bool", 122 | "defaultValue": false, 123 | "metadata": { 124 | "description": "Choose to enable HTTPS for a domain name using cert-manager and Let's Encrypt" 125 | }, 126 | "allowedValues": [ 127 | false, 128 | true 129 | ] 130 | }, 131 | "contactEmail": { 132 | "type": "string", 133 | "defaultValue": "null", 134 | "minLength": 1, 135 | "metadata": { 136 | "description": "An email address to provide to Let's Encrypt" 137 | } 138 | }, 139 | "domainName": { 140 | "type": "string", 141 | "defaultValue": "null", 142 | "minLength": 1, 143 | "metadata": { 144 | "description": "The domain name to provide HTTPS certificates for" 145 | } 146 | }, 147 | "certManagerVersion": { 148 | "type": "string", 149 | "defaultValue": "null", 150 | "minLength": 1, 151 | "metadata": { 152 | "description": "The version of cert-manager to install. Note: must include the preceding 'v', e.g., v0.11.0. Versions can be found here: https://github.com/jetstack/cert-manager/releases" 153 | } 154 | }, 155 | "nginxVersion": { 156 | "type": "string", 157 | "defaultValue": "null", 158 | "minLength": 1, 159 | "metadata": { 160 | "description": "The version of nginx-ingress to install, e.g., 1.29.1. Versions can be found here: https://hub.helm.sh/charts/stable/nginx-ingress" 161 | } 162 | }, 163 | "setupDockerImage": { 164 | "type": "string", 165 | "minLength": 1, 166 | "defaultValue": "sgibson91/binderhub-setup:1.3.1", 167 | "metadata": { 168 | "description": "Docker image to use for the BinderHub deployment. Most recent version number is recommended, latest tag will be subject to fluctuating changes. This image must be publicly accessible." 169 | }, 170 | "allowedValues": [ 171 | "sgibson91/binderhub-setup:1.3.1", 172 | "sgibson91/binderhub-setup:1.3.0", 173 | "sgibson91/binderhub-setup:1.2.4", 174 | "sgibson91/binderhub-setup:1.2.3", 175 | "sgibson91/binderhub-setup:1.2.2", 176 | "sgibson91/binderhub-setup:1.2.1", 177 | "sgibson91/binderhub-setup:1.2.0", 178 | "sgibson91/binderhub-setup:1.1.0", 179 | "sgibson91/binderhub-setup:1.0.6", 180 | "sgibson91/binderhub-setup:1.0.5", 181 | "sgibson91/binderhub-setup:1.0.4", 182 | "sgibson91/binderhub-setup:1.0.3", 183 | "sgibson91/binderhub-setup:1.0.2", 184 | "sgibson91/binderhub-setup:1.0.1", 185 | "sgibson91/binderhub-setup:1.0.0", 186 | "sgibson91/binderhub-setup:latest" 187 | ] 188 | } 189 | }, 190 | "resources": [ 191 | { 192 | "name": "[concat(parameters('binderHubName'), 'setup')]", 193 | "type": "Microsoft.ContainerInstance/containerGroups", 194 | "apiVersion": "2018-10-01", 195 | "location": "[resourceGroup().location]", 196 | "tags": {}, 197 | "properties": { 198 | "osType": "Linux", 199 | "restartPolicy": "Never", 200 | "containers": [ 201 | { 202 | "name": "binderhub-setup", 203 | "properties": { 204 | "image": "[parameters('setupDockerImage')]", 205 | "environmentVariables": [ 206 | { 207 | "name": "BINDERHUB_CONTAINER_MODE", 208 | "value": "true" 209 | }, 210 | { 211 | "name": "SP_APP_ID", 212 | "value": "[parameters('servicePrincipalAppId')]" 213 | }, 214 | { 215 | "name": "SP_APP_KEY", 216 | "secureValue": "[parameters('servicePrincipalAppKey')]" 217 | }, 218 | { 219 | "name": "SP_TENANT_ID", 220 | "value": "[parameters('servicePrincipalTenantId')]" 221 | }, 222 | { 223 | "name": "RESOURCE_GROUP_LOCATION", 224 | "value": "[resourceGroup().location]" 225 | }, 226 | { 227 | "name": "RESOURCE_GROUP_NAME", 228 | "value": "[resourceGroup().name]" 229 | }, 230 | { 231 | "name": "AZURE_SUBSCRIPTION", 232 | "value": "[subscription().id]" 233 | }, 234 | { 235 | "name": "BINDERHUB_NAME", 236 | "value": "[parameters('binderHubName')]" 237 | }, 238 | { 239 | "name": "BINDERHUB_VERSION", 240 | "value": "[parameters('binderHubVersion')]" 241 | }, 242 | { 243 | "name": "AKS_NODE_COUNT", 244 | "value": "[parameters('aksNodeCount')]" 245 | }, 246 | { 247 | "name": "AKS_NODE_VM_SIZE", 248 | "value": "[parameters('aksNodeVmSize')]" 249 | }, 250 | { 251 | "name": "DOCKER_IMAGE_PREFIX", 252 | "value": "[parameters('imagePrefix')]" 253 | }, 254 | { 255 | "name": "CONTAINER_REGISTRY", 256 | "value": "[parameters('containerRegistry')]" 257 | }, 258 | { 259 | "name": "DOCKERHUB_USERNAME", 260 | "value": "[parameters('dockerHubUsername')]" 261 | }, 262 | { 263 | "name": "DOCKERHUB_PASSWORD", 264 | "secureValue": "[parameters('dockerHubPassword')]" 265 | }, 266 | { 267 | "name": "DOCKERHUB_ORGANISATION", 268 | "value": "[parameters('dockerHubOrganisation')]" 269 | }, 270 | { 271 | "name": "REGISTRY_NAME", 272 | "value": "[parameters('containerRegistryName')]" 273 | }, 274 | { 275 | "name": "REGISTRY_SKU", 276 | "value": "[parameters('containerRegistrySku')]" 277 | }, 278 | { 279 | "name": "ENABLE_HTTPS", 280 | "value": "[parameters('enableHttps')]" 281 | }, 282 | { 283 | "name": "CONTACT_EMAIL", 284 | "value": "[parameters('contactEmail')]" 285 | }, 286 | { 287 | "name": "DOMAIN_NAME", 288 | "value": "[parameters('domainName')]" 289 | }, 290 | { 291 | "name": "CERTMANAGER_VERSION", 292 | "value": "[parameters('certManagerVersion')]" 293 | }, 294 | { 295 | "name": "NGINX_VERSION", 296 | "value": "[parameters('nginxVersion')]" 297 | } 298 | ], 299 | "resources": { 300 | "requests": { 301 | "cpu": 1, 302 | "memoryInGB": 1.5 303 | } 304 | } 305 | } 306 | } 307 | ] 308 | } 309 | } 310 | ] 311 | } 312 | -------------------------------------------------------------------------------- /docs/lets_encrypt_prod_switch.md: -------------------------------------------------------------------------------- 1 | # Switch from Let's Encrypt Staging to Production Environment 2 | 3 | Let's Encrypt provides a [staging platform](https://letsencrypt.org/docs/staging-environment/) to test against and this is the environment the package will request certificates from. 4 | Once you have [verified the staging certificates](https://www.cyberciti.biz/faq/test-ssl-certificates-diagnosis-ssl-certificate/) have been issued correctly, the user must switch to requesting certificates from Let's Encrypt's production environment to receive trusted certificates. 5 | 6 | The package automatically installs a cluster issuer for both the staging and production environments is `cluster-issuer.yaml`, switching the issuers involves switching the `cert-manager` [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) in `config.yaml`. 7 | 8 | In `config.yaml`, you will find the following code block: 9 | 10 | ```yaml 11 | ingress: 12 | enabled: true 13 | annotations: 14 | kubernetes.io/tls-acme: "true" 15 | kubernetes.io/ingress.class: nginx 16 | cert-manager.io/cluster-issuer: "letsencrypt-staging" 17 | https: 18 | enabled: true 19 | type: nginx 20 | host: 21 | - 22 | tls: 23 | - secretName: 24 | hosts: 25 | - 26 | ``` 27 | 28 | :rotating_light: This code block will actually appear twice in `config.yaml`. 29 | Once for the `binder` ingress, and again for the `hub` ingress. :rotating_light: 30 | 31 | Update the `cert-manager.io/cluster-issuer` annotation from `letsencrypt-staging` to `letsencrypt-prod`. 32 | 33 | :rotating_light: Remember to perform this change in **both** places where the annotation appears! :rotating_light: 34 | 35 | Now upgrade your cluster using [`upgrade.sh`](../upgrade.sh) or the `helm` command: 36 | 37 | ```bash 38 | helm upgrade BINDERHUB_NAME jupyterhub/binderhub \ 39 | --version=BINDERHUB_VERSION \ 40 | -f /path/to/secret.yaml \ 41 | -f /path/to/config.yaml \ 42 | --wait 43 | ``` 44 | 45 | Congratulations, you should now be issued trusted certificates from the Let's Encrypt production environment! 46 | -------------------------------------------------------------------------------- /docs/manually-setting-a-records.md: -------------------------------------------------------------------------------- 1 | # Manually Setting A Records 2 | 3 | Two A records are created for the Binder page and the JupyterHub and these records need to be set to the public IP address of the cluster's load balancer. 4 | The package tries to complete this step automatically but often fails, due to the long-running nature of Azure's process to update the CLI. 5 | 6 | To set the A records, the user could either wait some time (overnight is best) and then run [`set-a-records.sh`](../set_a_records.sh), or follow these steps to manually set the A records in [Azure Portal](https://portal.azure.com/). 7 | 8 | --- 9 | 10 | 1. Find the resource group 11 | 12 | ![Find Resource Group 1](../assets/find_resource_group_1.png) 13 | 14 | ![Find Resource Group 2](../assets/find_resource_group_2.png) 15 | 16 | 2. Open the DNS Zone 17 | 18 | ![Open DNS Zone](../assets/open_dns_zone.png) 19 | 20 | 3. Select an A Record 21 | 22 | You can set the `hub` and `binder` records in either order. 23 | 24 | ![Select A Record](../assets/select_a_record.png) 25 | 26 | 4. Set the A record 27 | 28 | To set the A record, choose an "Alias record set" that will alias to an Azure resource. 29 | Choose the subscription you deployed your BinderHub too, and then choose the Kubernetes IP address resource (it's name will have the `kubernetes-` prefix). 30 | Then click save. 31 | 32 | ![](../assets/set_a_record.png) 33 | 34 | Repeat for the second A record. 35 | 36 | Congratulations, you've set the A records for your BinderHub! 37 | -------------------------------------------------------------------------------- /scripts/docker_pull.py: -------------------------------------------------------------------------------- 1 | import json 2 | import docker 3 | import requests 4 | 5 | 6 | def get_request(url: str, header: dict = None, params: dict = None) -> dict: 7 | """Make a GET request to a URL. If the URL returns links, step through them 8 | and concatenate the responses. Return the JSON encoded response. 9 | 10 | Args: 11 | url (str): The URL to make the request against 12 | header (dict, optional): A dictionary of headers to send with the 13 | request. Defaults to None. 14 | params (dict, optional): A dictionary of parameters to send with the 15 | request. Defaults to None. 16 | 17 | Returns: 18 | dict: The response of the request 19 | """ 20 | resp = requests.get(url, headers=header, params=params) 21 | 22 | if not resp: 23 | raise RuntimeError(f"Could not fetch response from: {url}") 24 | 25 | if resp.links: 26 | full_resp = resp.json() 27 | 28 | while "next" in resp.links.keys(): 29 | resp = requests.get( 30 | resp.links["next"]["url"], headers=header, params=params 31 | ) 32 | full_resp.extend(resp.json()) 33 | 34 | return full_resp 35 | 36 | else: 37 | return resp.json() 38 | 39 | 40 | def get_all_tags(owner: str, repo: str) -> list: 41 | """Fetch a list of tags from a GitHub repository using the GitHub REST API 42 | 43 | Args: 44 | owner (str): The owner of the repository 45 | repo (str): The name of the repository 46 | 47 | Returns: 48 | list: A list of all the tags within the repository 49 | """ 50 | tags = ["latest"] 51 | url = f"https://api.github.com/repos/{owner}/{repo}/git/matching-refs/tags" 52 | out = get_request(url) 53 | 54 | for item in out: 55 | ref = item["ref"].split("/")[-1].strip("v") 56 | tags.append(ref) 57 | 58 | return tags 59 | 60 | 61 | def pull_image(image_name: str, tag: str, client=docker.APIClient(base_url='unix://var/run/docker.sock')) -> None: 62 | """Pull a Docker image from Docker Hub 63 | 64 | Args: 65 | image_name (str): The image to be pulled 66 | tag (str): The image tag to be pulled 67 | """ 68 | for line in client.pull(image_name, tag=tag, stream=True, decode=True): 69 | print(json.dumps(line, indent=2, sort_keys=True)) 70 | 71 | 72 | def main(): 73 | """Main function""" 74 | tags = get_all_tags("alan-turing-institute", "binderhub-deploy") 75 | 76 | for tag in tags: 77 | pull_image("sgibson91/binderhub-setup", tag) 78 | 79 | 80 | if __name__ == "__main__": 81 | main() 82 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | docker 2 | requests 3 | six 4 | -------------------------------------------------------------------------------- /src/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck disable=SC2086 3 | 4 | # Exit immediately if a pipeline returns a non-zero status 5 | set -eo pipefail 6 | 7 | # Get this script's path 8 | DIR="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" >/dev/null 2>&1 && pwd)" 9 | 10 | ## Detection of the deploy mode 11 | # 12 | # This script should handle both interactive deployment when run by a user 13 | # on their local system, and also running as a container entrypoint when 14 | # used either for a container-based local deployment or when deployed via an 15 | # Azure blue button setup. 16 | # 17 | # Check whether BINDERHUB_CONTAINER_MODE is set, and if so assume running 18 | # as a container-based install, checking that all required input is present 19 | # in the form of environment variables 20 | 21 | if [[ -n $BINDERHUB_CONTAINER_MODE ]]; then 22 | echo "--> Deployment operating in container mode" 23 | echo "--> Checking required environment variables" 24 | # Set out a list of required variables for this script 25 | REQUIREDVARS=" \ 26 | AKS_NODE_COUNT \ 27 | AKS_NODE_VM_SIZE \ 28 | AZURE_SUBSCRIPTION \ 29 | BINDERHUB_NAME \ 30 | BINDERHUB_VERSION \ 31 | CONTAINER_REGISTRY \ 32 | DOCKER_IMAGE_PREFIX \ 33 | ENABLE_HTTPS \ 34 | RESOURCE_GROUP_LOCATION \ 35 | RESOURCE_GROUP_NAME \ 36 | SP_APP_ID \ 37 | SP_APP_KEY \ 38 | SP_TENANT_ID \ 39 | " 40 | for required_var in $REQUIREDVARS; do 41 | if [ -z "${!required_var}" ]; then 42 | echo "--> ${required_var} must be set for container-based setup" >&2 43 | exit 1 44 | fi 45 | done 46 | 47 | # Logs will automatically be saved for containerized 48 | # deployments. This will ensure that an environment 49 | # variable exists as if it was run from the command-line 50 | # which in turn allows the check at the end to 51 | # complete successfully. 52 | LOG_TO_BLOB_STORAGE='true' 53 | 54 | if [ "$CONTAINER_REGISTRY" == 'dockerhub' ]; then 55 | 56 | REQUIREDVARS=" \ 57 | DOCKERHUB_USERNAME \ 58 | DOCKERHUB_PASSWORD \ 59 | " 60 | 61 | for required_var in $REQUIREDVARS; do 62 | if [ -z "${!required_var}" ]; then 63 | echo "--> ${required_var} must be set for container-based setup" >&2 64 | exit 1 65 | fi 66 | done 67 | 68 | echo "--> Configuration parsed from blue button: 69 | AKS_NODE_COUNT: ${AKS_NODE_COUNT} 70 | AKS_NODE_VM_SIZE: ${AKS_NODE_VM_SIZE} 71 | AZURE_SUBSCRIPTION: ${AZURE_SUBSCRIPTION} 72 | BINDERHUB_NAME: ${BINDERHUB_NAME} 73 | BINDERHUB_VERSION: ${BINDERHUB_VERSION} 74 | CONTAINER_REGISTRY: ${CONTAINER_REGISTRY} 75 | DOCKER_IMAGE_PREFIX: ${DOCKER_IMAGE_PREFIX} 76 | DOCKERHUB_ORGANISATION: ${DOCKERHUB_ORGANISATION} 77 | DOCKERHUB_USERNAME: ${DOCKERHUB_USERNAME} 78 | LOG_TO_BLOB_STORAGE: ${LOG_TO_BLOB_STORAGE} 79 | RESOURCE_GROUP_LOCATION: ${RESOURCE_GROUP_LOCATION} 80 | RESOURCE_GROUP_NAME: ${RESOURCE_GROUP_NAME} 81 | SP_APP_ID: ${SP_APP_ID} 82 | SP_APP_KEY: ${SP_APP_KEY} 83 | SP_TENANT_ID: ${SP_TENANT_ID} 84 | " | tee read-config.log 85 | 86 | # Check if DOCKERHUB_ORGANISATION is set to null. Return empty string if true. 87 | if [ ${DOCKERHUB_ORGANISATION} == 'null' ]; then DOCKERHUB_ORGANISATION=''; fi 88 | 89 | elif [ "$CONTAINER_REGISTRY" == 'azurecr' ]; then 90 | 91 | REQUIREDVARS=" \ 92 | REGISTRY_NAME \ 93 | REGISTRY_SKU \ 94 | " 95 | 96 | for required_var in $REQUIREDVARS; do 97 | if [ -z "${!required_var}" ]; then 98 | echo "--> ${required_var} must be set for container-based setup" >&2 99 | exit 1 100 | fi 101 | done 102 | 103 | echo "--> Configuration parsed from blue button: 104 | AKS_NODE_COUNT: ${AKS_NODE_COUNT} 105 | AKS_NODE_VM_SIZE: ${AKS_NODE_VM_SIZE} 106 | AZURE_SUBSCRIPTION: ${AZURE_SUBSCRIPTION} 107 | BINDERHUB_NAME: ${BINDERHUB_NAME} 108 | BINDERHUB_VERSION: ${BINDERHUB_VERSION} 109 | CONTAINER_REGISTRY: ${CONTAINER_REGISTRY} 110 | DOCKER_IMAGE_PREFIX: ${DOCKER_IMAGE_PREFIX} 111 | LOG_TO_BLOB_STORAGE: ${LOG_TO_BLOB_STORAGE} 112 | REGISTRY_NAME: ${REGISTRY_NAME} 113 | REGISTRY_SKU: ${REGISTRY_SKU} 114 | RESOURCE_GROUP_LOCATION: ${RESOURCE_GROUP_LOCATION} 115 | RESOURCE_GROUP_NAME: ${RESOURCE_GROUP_NAME} 116 | SP_APP_ID: ${SP_APP_ID} 117 | SP_APP_KEY: ${SP_APP_KEY} 118 | SP_TENANT_ID: ${SP_TENANT_ID} 119 | " | tee read-config.log 120 | 121 | else 122 | echo "--> Please provide a valid option for CONTAINER_REGISTRY." 123 | echo " Options are 'dockerhub' or 'azurecr'." 124 | exit 1 125 | fi 126 | 127 | if [[ -n $ENABLE_HTTPS ]]; then 128 | 129 | REQUIREDVARS="\ 130 | CERTMANAGER_VERSION \ 131 | CONTACT_EMAIL \ 132 | DOMAIN_NAME \ 133 | NGINX_VERSION \ 134 | " 135 | 136 | for required_var in $REQUIREDVARS; do 137 | if [ -z "${!required_var}" ] || [ ${!required_var} == 'null' ]; then 138 | echo "--> ${required_var} must be set for container-based setup" >&2 139 | exit 1 140 | fi 141 | done 142 | 143 | # Configure URL for Custom Resource Definitions 144 | STRIPPED_VERSION=$(echo "${CERTMANAGER_VERSION}" | tr -d 'v') 145 | SHORT_VERSION=${STRIPPED_VERSION%.*} 146 | CERTMANAGER_CRDS="https://raw.githubusercontent.com/jetstack/cert-manager/release-${SHORT_VERSION}/deploy/manifests/00-crds.yaml" 147 | 148 | else 149 | if [ ${CONTACT_EMAIL} == 'null' ]; then CONTACT_EMAIL=''; fi 150 | if [ ${DOMAIN_NAME} == 'null' ]; then DOMAIN_NAME=''; fi 151 | if [ ${CERTMANAGER_VERSION} == 'null' ]; then CERTMANAGER_VERSION=''; fi 152 | if [ ${NGINX_VERSION} == 'null' ]; then NGINX_VERSION=''; fi 153 | fi 154 | 155 | # Azure blue-button prepends '/subscription/' to AZURE_SUBSCRIPTION 156 | AZURE_SUBSCRIPTION=$(echo $AZURE_SUBSCRIPTION | sed -r "s/^\/subscriptions\///") 157 | 158 | echo "--> Configuration parsed from blue button: 159 | AKS_NODE_COUNT: ${AKS_NODE_COUNT} 160 | AKS_NODE_VM_SIZE: ${AKS_NODE_VM_SIZE} 161 | AZURE_SUBSCRIPTION: ${AZURE_SUBSCRIPTION} 162 | BINDERHUB_NAME: ${BINDERHUB_NAME} 163 | BINDERHUB_VERSION: ${BINDERHUB_VERSION} 164 | CERTMANAGER_VERSION: ${CERTMANAGER_VERSION} 165 | CONTACT_EMAIL: ${CONTACT_EMAIL} 166 | CONTAINER_REGISTRY: ${CONTAINER_REGISTRY} 167 | DOCKER_IMAGE_PREFIX: ${DOCKER_IMAGE_PREFIX} 168 | DOCKERHUB_ORGANISATION: ${DOCKERHUB_ORGANISATION} 169 | DOCKERHUB_USERNAME: ${DOCKERHUB_USERNAME} 170 | DOMAIN_NAME: ${DOMAIN_NAME} 171 | ENABLE_HTTPS: ${ENABLE_HTTPS} 172 | NGINX_VERSION: ${NGINX_VERSION} 173 | REGISTRY_NAME: ${REGISTRY_NAME} 174 | REGISTRY_SKU: ${REGISTRY_SKU} 175 | RESOURCE_GROUP_LOCATION: ${RESOURCE_GROUP_LOCATION} 176 | RESOURCE_GROUP_NAME: ${RESOURCE_GROUP_NAME} 177 | SP_APP_ID: ${SP_APP_ID} 178 | SP_TENANT_ID: ${SP_TENANT_ID} 179 | " | tee read-config.log 180 | 181 | else 182 | 183 | # Read in config file and assign variables for the non-container case 184 | configFile="${DIR}/config.json" 185 | 186 | echo "--> Reading configuration from ${configFile}" 187 | 188 | AKS_NODE_COUNT=$(jq -r '.azure .node_count' ${configFile}) 189 | AKS_NODE_VM_SIZE=$(jq -r '.azure .vm_size' ${configFile}) 190 | AZURE_SUBSCRIPTION=$(jq -r '.azure .subscription' ${configFile}) 191 | BINDERHUB_NAME=$(jq -r '.binderhub .name' ${configFile}) 192 | BINDERHUB_VERSION=$(jq -r '.binderhub .version' ${configFile}) 193 | CONTAINER_REGISTRY=$(jq -r '.container_registry' ${configFile}) 194 | DOCKER_IMAGE_PREFIX=$(jq -r '.binderhub .image_prefix' ${configFile}) 195 | ENABLE_HTTPS=$(jq -r '.enable_https' ${configFile}) 196 | LOG_TO_BLOB_STORAGE=$(jq -r '.azure .log_to_blob_storage' ${configFile}) 197 | RESOURCE_GROUP_LOCATION=$(jq -r '.azure .location' ${configFile}) 198 | RESOURCE_GROUP_NAME=$(jq -r '.azure .res_grp_name' ${configFile}) 199 | SP_APP_ID=$(jq -r '.azure .sp_app_id' ${configFile}) 200 | SP_APP_KEY=$(jq -r '.azure .sp_app_key' ${configFile}) 201 | SP_TENANT_ID=$(jq -r '.azure .sp_tenant_id' ${configFile}) 202 | 203 | # Check that the variables are all set non-zero, non-null 204 | REQUIREDVARS=" \ 205 | AKS_NODE_COUNT \ 206 | AKS_NODE_VM_SIZE \ 207 | AZURE_SUBSCRIPTION \ 208 | BINDERHUB_NAME \ 209 | BINDERHUB_VERSION \ 210 | CONTAINER_REGISTRY \ 211 | DOCKER_IMAGE_PREFIX \ 212 | ENABLE_HTTPS \ 213 | RESOURCE_GROUP_LOCATION \ 214 | RESOURCE_GROUP_NAME \ 215 | " 216 | 217 | for required_var in $REQUIREDVARS; do 218 | if [ -z "${!required_var}" ] || [ ${!required_var} == 'null' ]; then 219 | echo "--> ${required_var} must be set for deployment" >&2 220 | exit 1 221 | fi 222 | done 223 | 224 | # Check if any optional variables are set null; if so, reset them to a 225 | # zero-length string for later checks. If they failed to read at all, 226 | # possibly due to an invalid json file, they will be returned as a 227 | # zero-length string -- this is attempting to make the 'not set' 228 | # value the same in either case 229 | if [ ${SP_APP_ID} == 'xnull' ]; then SP_APP_ID=''; fi 230 | if [ ${SP_APP_KEY} == 'xnull' ]; then SP_APP_KEY=''; fi 231 | if [ ${SP_TENANT_ID} == 'xnull' ]; then SP_TENANT_ID=''; fi 232 | if [ ${LOG_TO_BLOB_STORAGE} == 'xnull' ]; then LOG_TO_BLOB_STORAGE=''; fi 233 | 234 | # Test value of CONTAINER_REGISTRY. Must be either "dockerhub" or "azurecr" 235 | if [ ${CONTAINER_REGISTRY} == 'xdockerhub' ]; then 236 | echo "--> Getting DockerHub requirements" 237 | 238 | # Read Docker credentials from config file 239 | DOCKERHUB_ORGANISATION=$(jq -r '.docker .org' ${configFile}) 240 | DOCKERHUB_PASSWORD=$(jq -r '.docker .password' ${configFile}) 241 | DOCKERHUB_USERNAME=$(jq -r '.docker .username' ${configFile}) 242 | 243 | # Check that Docker Hub credentials have been set 244 | if [ ${DOCKERHUB_ORGANISATION} == 'xnull' ]; then DOCKERHUB_ORGANISATION=''; fi 245 | if [ ${DOCKERHUB_PASSWORD} == 'xnull' ]; then DOCKERHUB_PASSWORD=''; fi 246 | if [ ${DOCKERHUB_USERNAME} == 'xnull' ]; then DOCKERHUB_USERNAME=''; fi 247 | 248 | # Check/get the user's Docker Hub credentials 249 | if [ -z $DOCKERHUB_USERNAME ]; then 250 | if [ -n "$DOCKERHUB_ORGANISATION" ]; then 251 | echo "--> Your Docker ID must be a member of the ${DOCKERHUB_ORGANISATION} organisation" 252 | fi 253 | read -rp "DockerHub ID: " DOCKERHUB_USERNAME 254 | read -rsp "DockerHub password: " DOCKERHUB_PASSWORD 255 | echo 256 | else 257 | if [ -z $DOCKERHUB_PASSWORD ]; then 258 | read -rsp "Docker Hub password for ${DOCKERHUB_USERNAME}: " DOCKERHUB_PASSWORD 259 | echo 260 | fi 261 | fi 262 | 263 | elif [ ${CONTAINER_REGISTRY} == 'xazurecr' ]; then 264 | echo "--> Getting configuration for Azure Container Registry" 265 | 266 | # Read in ACR configuration 267 | REGISTRY_NAME=$(jq -r '.acr .registry_name' ${configFile}) 268 | REGISTRY_SKU=$(jq -r '.acr .sku' ${configFile}) 269 | 270 | # Checking required variables 271 | REQUIREDVARS=" \ 272 | REGISTRY_NAME \ 273 | REGISTRY_SKU \ 274 | SP_APP_ID \ 275 | SP_APP_KEY \ 276 | " 277 | 278 | for required_var in $REQUIREDVARS; do 279 | if [ -z "${!required_var}" ] || [ ${!required_var} == 'xnull' ]; then 280 | echo "--> ${required_var} must be set for deployment" >&2 281 | exit 1 282 | fi 283 | done 284 | 285 | # ACR name must be alphanumeric only and 50 characters or less 286 | REGISTRY_NAME=$(echo ${REGISTRY_NAME} | tr -cd '[:alnum:]' | cut -c -50) 287 | 288 | echo "--> Configuration read in: 289 | AKS_NODE_COUNT: ${AKS_NODE_COUNT} 290 | AKS_NODE_VM_SIZE: ${AKS_NODE_VM_SIZE} 291 | AZURE_SUBSCRIPTION: ${AZURE_SUBSCRIPTION} 292 | BINDERHUB_NAME: ${BINDERHUB_NAME} 293 | BINDERHUB_VERSION: ${BINDERHUB_VERSION} 294 | CONTAINER_REGISTRY: ${CONTAINER_REGISTRY} 295 | DOCKER_IMAGE_PREFIX: ${DOCKER_IMAGE_PREFIX} 296 | LOG_TO_BLOB_STORAGE: ${LOG_TO_BLOB_STORAGE} 297 | REGISTRY_NAME: ${REGISTRY_NAME} 298 | REGISTRY_SKU: ${REGISTRY_SKU} 299 | RESOURCE_GROUP_LOCATION: ${RESOURCE_GROUP_LOCATION} 300 | RESOURCE_GROUP_NAME: ${RESOURCE_GROUP_NAME} 301 | SP_APP_ID: ${SP_APP_ID} 302 | SP_APP_KEY: ${SP_APP_KEY} 303 | SP_TENANT_ID: ${SP_TENANT_ID} 304 | " | tee read-config.log 305 | 306 | else 307 | echo "--> Please provide a valid option for CONTAINER_REGISTRY." 308 | echo " Options are: 'dockerhub' or 'azurecr'." 309 | fi 310 | 311 | if [[ -n $ENABLE_HTTPS ]]; then 312 | 313 | # Read in cert-manager config 314 | CERTMANAGER_VERSION=$(jq -r '.https .certmanager_version' ${configFile}) 315 | CONTACT_EMAIL=$(jq -r '.https .contact_email' ${configFile}) 316 | DOMAIN_NAME=$(jq -r '.https .domain_name' ${configFile}) 317 | NGINX_VERSION=$(jq -r '.https .nginx_version' ${configFile}) 318 | 319 | # Checking required variables 320 | REQUIREDVARS="\ 321 | CERTMANAGER_VERSION \ 322 | CONTACT_EMAIL \ 323 | DOMAIN_NAME \ 324 | NGINX_VERSION \ 325 | " 326 | 327 | for required_var in $REQUIREDVARS; do 328 | if [ -z "${!required_var}" ] || [ ${!required_var} == 'xnull' ]; then 329 | echo "--> ${required_var} must be set for deployment" >&2 330 | exit 1 331 | fi 332 | done 333 | 334 | # Configure URL for Custom Resource Definitions 335 | STRIPPED_VERSION=$(echo "${CERTMANAGER_VERSION}" | tr -d 'v') 336 | SHORT_VERSION=${STRIPPED_VERSION%.*} 337 | CERTMANAGER_CRDS="https://raw.githubusercontent.com/jetstack/cert-manager/release-${SHORT_VERSION}/deploy/manifests/00-crds.yaml" 338 | 339 | else 340 | if [ ${CONTACT_EMAIL} == 'xnull' ]; then CONTACT_EMAIL=''; fi 341 | if [ ${DOMAIN_NAME} == 'xnull' ]; then DOMAIN_NAME=''; fi 342 | if [ ${CERTMANAGER_VERSION} == 'xnull' ]; then CERTMANAGER_VERSION=''; fi 343 | if [ ${NGINX_VERSION} == 'xnull' ]; then NGINX_VERSION=''; fi 344 | fi 345 | 346 | echo "--> Configuration read in: 347 | AKS_NODE_COUNT: ${AKS_NODE_COUNT} 348 | AKS_NODE_VM_SIZE: ${AKS_NODE_VM_SIZE} 349 | AZURE_SUBSCRIPTION: ${AZURE_SUBSCRIPTION} 350 | BINDERHUB_NAME: ${BINDERHUB_NAME} 351 | BINDERHUB_VERSION: ${BINDERHUB_VERSION} 352 | CERTMANAGER_VERSION: ${CERTMANAGER_VERSION} 353 | CONTACT_EMAIL: ${CONTACT_EMAIL} 354 | CONTAINER_REGISTRY: ${CONTAINER_REGISTRY} 355 | DOCKER_IMAGE_PREFIX: ${DOCKER_IMAGE_PREFIX} 356 | DOCKERHUB_ORGANISATION: ${DOCKERHUB_ORGANISATION} 357 | DOCKERHUB_USERNAME: ${DOCKERHUB_USERNAME} 358 | DOMAIN_NAME: ${DOMAIN_NAME} 359 | ENABLE_HTTPS: ${ENABLE_HTTPS} 360 | NGINX_VERSION: ${NGINX_VERSION} 361 | REGISTRY_NAME: ${REGISTRY_NAME} 362 | REGISTRY_SKU: ${REGISTRY_SKU} 363 | RESOURCE_GROUP_LOCATION: ${RESOURCE_GROUP_LOCATION} 364 | RESOURCE_GROUP_NAME: ${RESOURCE_GROUP_NAME} 365 | SP_APP_ID: ${SP_APP_ID} 366 | SP_TENANT_ID: ${SP_TENANT_ID} 367 | " | tee read-config.log 368 | 369 | fi 370 | 371 | set -eo pipefail 372 | 373 | # Normalise resource group location to remove spaces and have lowercase 374 | RESOURCE_GROUP_LOCATION=$(echo ${RESOURCE_GROUP_LOCATION//[[:blank::]]/} | tr '[:upper:]' '[:lower:]') 375 | 376 | # Generate a valid name for the AKS cluster 377 | AKS_NAME=$(echo ${BINDERHUB_NAME} | tr -cd '[:alnum:]-' | cut -c 1-59)-AKS 378 | 379 | # Format BinderHub name for Kubernetes 380 | HELM_BINDERHUB_NAME=$(echo ${BINDERHUB_NAME} | tr -cd '[:alnum:]-.' | tr '[:upper:]' '[:lower:]' | sed -E -e 's/^([.-]+)//' -e 's/([.-]+)$//') 381 | 382 | # Azure login will be different depending on whether this script is running 383 | # with or without service principal details supplied. 384 | # 385 | # If all the SP environments are set, use those. Otherwise, fall back to an 386 | # interactive login. 387 | 388 | if [ -z $SP_APP_ID ] || [ -z $SP_APP_KEY ] || [ -z $SP_TENANT_ID ]; then 389 | echo "--> Attempting to log in to Azure as a user" 390 | if ! az login -o none; then 391 | echo "--> Unable to connect to Azure" >&2 392 | exit 1 393 | else 394 | echo "--> Logged in to Azure" 395 | fi 396 | else 397 | echo "--> Attempting to log in to Azure with provided Service Principal" 398 | if ! az login --service-principal -u "${SP_APP_ID}" -p "${SP_APP_KEY}" -t "${SP_TENANT_ID}"; then 399 | echo "--> Unable to connect to Azure" >&2 400 | exit 1 401 | else 402 | echo "--> Logged in to Azure" 403 | # Use this service principal for AKS creation 404 | AKS_SP="--service-principal ${SP_APP_ID} --client-secret ${SP_APP_KEY}" 405 | fi 406 | fi 407 | 408 | # Activate chosen subscription 409 | echo "--> Activating Azure subscription: ${AZURE_SUBSCRIPTION}" 410 | az account set -s "$AZURE_SUBSCRIPTION" 411 | 412 | # Create a new resource group if necessary 413 | echo "--> Checking if resource group exists: ${RESOURCE_GROUP_NAME}" 414 | if [[ $(az group exists --name $RESOURCE_GROUP_NAME) == false ]]; then 415 | echo "--> Creating new resource group: ${RESOURCE_GROUP_NAME}" 416 | az group create -n $RESOURCE_GROUP_NAME --location $RESOURCE_GROUP_LOCATION -o table | tee rg-create.log 417 | else 418 | echo "--> Resource group ${RESOURCE_GROUP_NAME} found" 419 | fi 420 | 421 | # Create a Virtual Network to deploy the k8s cluster into 422 | echo "--> Creating a Virtual Network and subnet" 423 | az network vnet create \ 424 | -g ${RESOURCE_GROUP_NAME} \ 425 | -n ${BINDERHUB_NAME}-vnet \ 426 | --address-prefixes 10.0.0.0/8 \ 427 | --subnet-name ${BINDERHUB_NAME}-subnet \ 428 | --subnet-prefix 10.240.0.0/16 \ 429 | -o table | tee create-vnet.log 430 | echo "--> Retrieving the Virtual Network application ID" 431 | VNET_ID=$(az network vnet show -g ${RESOURCE_GROUP_NAME} -n ${BINDERHUB_NAME}-vnet --query id -o tsv) 432 | echo "--> Retrieving the subnet application ID" 433 | SUBNET_ID=$(az network vnet subnet show -g ${RESOURCE_GROUP_NAME} --vnet-name ${BINDERHUB_NAME}-vnet -n ${BINDERHUB_NAME}-subnet --query id -o tsv) 434 | 435 | # If no Service Principal is provided, create one 436 | if [ -z "${SP_APP_ID}" ] && [ -z "${SP_APP_KEY}" ]; then 437 | SP_NAME='binderhub-sp' 438 | echo "--> Creating Service Principal ${SP_NAME}" 439 | SP_APP_KEY=$(az ad sp create-for-rbac -n http://${SP_NAME} --skip-assignment --query password -o tsv) 440 | SP_APP_ID=$(az ad sp show --id http://${SP_NAME} --query appId -o tsv) 441 | echo "Waiting for Service Principal to propagate" 442 | sleep 15 443 | AKS_SP="--service-principal ${SP_APP_ID} --client-secret ${SP_APP_KEY}" 444 | fi 445 | 446 | # Assign Contributor role to Service Principal 447 | az role assignment create --assignee ${SP_APP_ID} --scope ${VNET_ID} --role Contributor -o table | tee contributor-role-assignment.log 448 | 449 | # If Azure container registry is required, create an ACR and give Service Principal AcrPush role. 450 | if [ ${CONTAINER_REGISTRY} == 'xazurecr' ]; then 451 | echo "--> Checking ACR name availability" 452 | REGISTRY_NAME_AVAIL=$(az acr check-name -n ${REGISTRY_NAME} --query nameAvailable -o tsv) 453 | while [ ${REGISTRY_NAME_AVAIL} == false ]; do 454 | echo "--> Name ${REGISTRY_NAME} not available. Appending 4 random characters." 455 | REGISTRY_NAME="$(echo ${REGISTRY_NAME} | tr -cd '[:alnum:]' | tr '[:upper:]' '[:lower:]' | cut -c -50)$(openssl rand -hex 2)" 456 | echo "--> New name: ${REGISTRY_NAME}" 457 | REGISTRY_NAME_AVAIL=$(az acr check-name -n ${REGISTRY_NAME} --query nameAvailable -o tsv) 458 | done 459 | echo "--> Name available" 460 | 461 | echo "--> Creating ACR" 462 | az acr create -n $REGISTRY_NAME -g $RESOURCE_GROUP_NAME --sku $REGISTRY_SKU -o table | tee acr-create.log 463 | 464 | # Populating some variables 465 | ACR_LOGIN_SERVER=$(az acr list -g ${RESOURCE_GROUP_NAME} --query '[].{acrLoginServer:loginServer}' -o tsv) 466 | ACR_ID=$(az acr show -n ${REGISTRY_NAME} -g ${RESOURCE_GROUP_NAME} --query 'id' -o tsv) 467 | 468 | # Assigning AcrPush role to Service Principal using AcrPush's specific object-ID 469 | echo "--> Assigning AcrPush role to Service Principal" 470 | az role assignment create --assignee ${SP_APP_ID} --role 8311e382-0749-4cb8-b61a-304f252e45ec --scope $ACR_ID -o table | tee acrpush-role-assignment.log 471 | 472 | # Reassign IMAGE_PREFIX to conform with BinderHub's expectation: 473 | # //-name:tag 474 | IMAGE_PREFIX=${BINDERHUB_NAME}/${IMAGE_PREFIX} 475 | fi 476 | 477 | # If HTTPS is required, set up a DNS zone and empty A records 478 | if [[ -n $ENABLE_HTTPS ]]; then 479 | # Create a DNS zone 480 | az network dns zone create -g $RESOURCE_GROUP_NAME -n $DOMAIN_NAME -o table | tee create-dns-zone.log 481 | 482 | # Echo name name servers 483 | NAME_SERVERS=$(az network dns zone show -g $RESOURCE_GROUP_NAME -n $DOMAIN_NAME --query nameServers -o tsv) 484 | printf "Please update your parent domain with the following name servers:\n%s" "${NAME_SERVERS}" | tee name-servers.log 485 | 486 | # Create empty A records for the binder and hub pods 487 | az network dns record-set a create -g $RESOURCE_GROUP_NAME -z $DOMAIN_NAME --ttl 300 -n binder -o table | tee create-binder-a-record.log 488 | az network dns record-set a create -g $RESOURCE_GROUP_NAME -z $DOMAIN_NAME --ttl 300 -n hub -o table | tee create-hub-a-record.log 489 | 490 | # Set some extra variables 491 | BINDER_HOST="binder.${DOMAIN_NAME}" 492 | HUB_HOST="hub.${DOMAIN_NAME}" 493 | BINDER_SECRET="${HELM_BINDERHUB_NAME}-binder-secret" 494 | HUB_SECRET="${HELM_BINDERHUB_NAME}-hub-secret" 495 | fi 496 | 497 | # Create an AKS cluster 498 | echo "--> Creating AKS cluster; this may take a few minutes to complete 499 | Resource Group: ${RESOURCE_GROUP_NAME} 500 | Cluster name: ${AKS_NAME} 501 | Node count: ${AKS_NODE_COUNT} 502 | Node VM size: ${AKS_NODE_VM_SIZE}" 503 | az aks create \ 504 | -n $AKS_NAME \ 505 | -g $RESOURCE_GROUP_NAME \ 506 | --generate-ssh-keys \ 507 | --node-count $AKS_NODE_COUNT \ 508 | --node-vm-size $AKS_NODE_VM_SIZE \ 509 | --dns-service-ip 10.0.0.10 \ 510 | --docker-bridge-address 172.17.0.1/16 \ 511 | --network-plugin azure \ 512 | --network-policy azure \ 513 | --service-cidr 10.0.0.0/16 \ 514 | --vnet-subnet-id $SUBNET_ID \ 515 | -o table ${AKS_SP} | 516 | tee aks-create.log 517 | 518 | # Get kubectl credentials from Azure 519 | echo "--> Fetching kubectl credentials from Azure" 520 | az aks get-credentials -n $AKS_NAME -g $RESOURCE_GROUP_NAME --overwrite-existing | tee get-credentials.log 521 | 522 | # Check nodes are ready 523 | nodecount="$(kubectl get node | awk '{print $2}' | grep -c Ready)" 524 | while [[ ${nodecount} -ne ${AKS_NODE_COUNT} ]]; do 525 | echo -n "$(date)" 526 | echo " : ${nodecount} of ${AKS_NODE_COUNT} nodes ready" 527 | sleep 15 528 | nodecount="$(kubectl get node | awk '{print $2}' | grep -c Ready)" 529 | done 530 | echo 531 | echo "--> Cluster node status:" 532 | kubectl get node | tee kubectl-status.log 533 | echo 534 | 535 | # Setup ServiceAccount for tiller 536 | echo "--> Setting up tiller service account" 537 | kubectl --namespace kube-system create serviceaccount tiller | tee tiller-service-account.log 538 | 539 | # Give the ServiceAccount full permissions to manage the cluster 540 | echo "--> Giving the ServiceAccount full permissions to manage the cluster" 541 | kubectl create clusterrolebinding tiller --clusterrole cluster-admin --serviceaccount=kube-system:tiller | tee cluster-role-bindings.log 542 | 543 | # Check helm installation 544 | helm=$(command -v helm3 || command -v helm) 545 | HELM_VERSION=$($helm version -c --short | cut -f1 -d".") 546 | 547 | if [ "${HELM_VERSION}" == "v3" ]; then 548 | echo "--> You are running helm v3!" 549 | elif [ "${HELM_VERSION}" == "v2" ]; then 550 | echo "--> You have helm v2 installed, but we really recommend using helm v3." 551 | echo " Please install helm v3 and rerun this script." 552 | exit 1 553 | else 554 | echo "--> Helm not found. Please run setup.sh then rerun this script." 555 | exit 1 556 | fi 557 | 558 | # Create tokens for the secrets file: 559 | apiToken=$(openssl rand -hex 32) 560 | secretToken=$(openssl rand -hex 32) 561 | 562 | # Get the latest helm chart for BinderHub: 563 | $helm repo add jupyterhub https://jupyterhub.github.io/helm-chart 564 | $helm repo update 565 | 566 | # If HTTPS is enabled, get nginx-ingress and cert-manager helm charts and 567 | # install them into the hub namespace 568 | if [[ -n $ENABLE_HTTPS ]]; then 569 | echo "--> Add nginx-ingress and cert-manager helm repos" 570 | if [ "${HELM_VERSION}" == "v2" ]; then 571 | $helm repo add stable https://charts.helm.sh/stable 572 | fi 573 | $helm repo add jetstack https://charts.jetstack.io 574 | $helm repo update 575 | kubectl apply --validate=false -f ${CERTMANAGER_CRDS} 576 | 577 | echo "--> Install nginx-ingress helm chart" 578 | $helm install nginx-ingress stable/nginx-ingress \ 579 | --namespace ${HELM_BINDERHUB_NAME} \ 580 | --version ${NGINX_VERSION} \ 581 | --create-namespace \ 582 | --timeout 10m0s \ 583 | --wait | tee nginx-chart-install.log 584 | 585 | echo "--> Install cert-manager helm chart" 586 | $helm install cert-manager jetstack/cert-manager \ 587 | --namespace ${HELM_BINDERHUB_NAME} \ 588 | --version ${CERTMANAGER_VERSION} \ 589 | --create-namespace \ 590 | --timeout 10m0s \ 591 | --wait | tee cert-manager-chart-install.log 592 | 593 | LOAD_BALANCER_IP=$(kubectl get svc nginx-ingress-controller -n ${HELM_BINDERHUB_NAME} | awk '{ print $4}' | tail -n 1) 594 | 595 | echo "--> cert-manager pods status:" 596 | kubectl get pods --namespace $HELM_BINDERHUB_NAME | tee cert-manager-get-pods.log 597 | 598 | # Create a ClusterIssuer to test deployment 599 | echo "--> Testing cert-manager webhooks" 600 | kubectl apply -f ${DIR}/templates/test-resources.yaml 601 | sleep 30 602 | kubectl describe certificate -n cert-manager-test | tee cert-manager-test.log 603 | 604 | # Clean up resources 605 | kubectl delete -f ${DIR}/templates/test-resources.yaml 606 | 607 | # Parse info to cluster issuer 608 | echo "--> Writing ClusterIssuer config" 609 | sed -e "s//${HELM_BINDERHUB_NAME}/g" \ 610 | -e "s//${CONTACT_EMAIL}/g" \ 611 | ${DIR}/templates/cluster-issuer-template.yaml >${DIR}/cluster-issuer.yaml 612 | 613 | # Install the Helm Chart using the configuration files, to deploy both a BinderHub and a JupyterHub. 614 | if [ ${CONTAINER_REGISTRY} == 'dockerhub' ]; then 615 | 616 | echo "--> Generating initial configuration file" 617 | if [ -z "${DOCKERHUB_ORGANISATION}" ]; then 618 | sed -e "s//${DOCKERHUB_USERNAME}/" \ 619 | -e "s//${DOCKER_IMAGE_PREFIX}/" \ 620 | -e "s//${HUB_HOST}/" \ 621 | -e "s//letsencrypt-staging/g" \ 622 | -e "s//${BINDER_HOST}/g" \ 623 | -e "s//${BINDER_SECRET}/" \ 624 | -e "s//${HUB_HOST}/g" \ 625 | -e "s//${HUB_SECRET}/" \ 626 | -e "s//${LOAD_BALANCER_IP}/" \ 627 | ${DIR}/templates/https-config-template.yaml >${DIR}/config.yaml 628 | else 629 | sed -e "s//${DOCKERHUB_ORGANISATION}/" \ 630 | -e "s//${DOCKER_IMAGE_PREFIX}/" \ 631 | -e "s//${HUB_HOST}/" \ 632 | -e "s//letsencrypt-staging/g" \ 633 | -e "s//${BINDER_HOST}/g" \ 634 | -e "s//${BINDER_SECRET}/" \ 635 | -e "s//${HUB_HOST}/g" \ 636 | -e "s//${HUB_SECRET}/" \ 637 | -e "s//${LOAD_BALANCER_IP}/" \ 638 | ${DIR}/templates/https-config-template.yaml >${DIR}/config.yaml 639 | fi 640 | 641 | echo "--> Generating initial secrets file" 642 | sed -e "s//${apiToken}/" \ 643 | -e "s//${secretToken}/" \ 644 | -e "s//${DOCKERHUB_USERNAME}/" \ 645 | -e "s//${DOCKERHUB_PASSWORD}/" \ 646 | ${DIR}/templates/secret-template.yaml >${DIR}/secret.yaml 647 | 648 | elif [ ${CONTAINER_REGISTRY} == 'azurecr' ]; then 649 | 650 | echo "--> Generating initial configuration file" 651 | sed -e "s@@${ACR_LOGIN_SERVER}@g" \ 652 | -e "s@@${DOCKER_IMAGE_PREFIX}@" \ 653 | -e "s//${HUB_HOST}/" \ 654 | -e "s//letsencrypt-staging/g" \ 655 | -e "s//${BINDER_HOST}/g" \ 656 | -e "s//${BINDER_SECRET}/" \ 657 | -e "s//${HUB_HOST}/g" \ 658 | -e "s//${HUB_SECRET}/" \ 659 | -e "s//${LOAD_BALANCER_IP}/" \ 660 | ${DIR}/templates/https-acr-config-template.yaml >${DIR}/config.yaml 661 | 662 | echo "--> Generating initial secrets file" 663 | sed -e "s//${apiToken}/" \ 664 | -e "s//${secretToken}/" \ 665 | -e "s@@${ACR_LOGIN_SERVER}@" \ 666 | -e "s//${SP_APP_ID}/" \ 667 | -e "s//${SP_APP_KEY}/" \ 668 | ${DIR}/templates/acr-secret-template.yaml >${DIR}/secret.yaml 669 | fi 670 | 671 | else 672 | 673 | # Install the Helm Chart using the configuration files, to deploy both a BinderHub and a JupyterHub. 674 | if [ ${CONTAINER_REGISTRY} == 'dockerhub' ]; then 675 | 676 | echo "--> Generating initial configuration file" 677 | if [ -z "${DOCKERHUB_ORGANISATION}" ]; then 678 | sed -e "s//${DOCKERHUB_USERNAME}/" \ 679 | -e "s//${DOCKER_IMAGE_PREFIX}/" \ 680 | ${DIR}/templates/config-template.yaml >${DIR}/config.yaml 681 | else 682 | sed -e "s//${DOCKERHUB_ORGANISATION}/" \ 683 | -e "s//${DOCKER_IMAGE_PREFIX}/" \ 684 | ${DIR}/templates/config-template.yaml >${DIR}/config.yaml 685 | fi 686 | 687 | echo "--> Generating initial secrets file" 688 | sed -e "s//${apiToken}/" \ 689 | -e "s//${secretToken}/" \ 690 | -e "s//${DOCKERHUB_USERNAME}/" \ 691 | -e "s//${DOCKERHUB_PASSWORD}/" \ 692 | ${DIR}/templates/secret-template.yaml >${DIR}/secret.yaml 693 | 694 | elif [ ${CONTAINER_REGISTRY} == 'azurecr' ]; then 695 | 696 | echo "--> Generating initial configuration file" 697 | sed -e "s@@${ACR_LOGIN_SERVER}@g" \ 698 | -e "s@@${DOCKER_IMAGE_PREFIX}@" \ 699 | ${DIR}/templates/acr-config-template.yaml >${DIR}/config.yaml 700 | 701 | echo "--> Generating initial secrets file" 702 | sed -e "s//${apiToken}/" \ 703 | -e "s//${secretToken}/" \ 704 | -e "s@@${ACR_LOGIN_SERVER}@" \ 705 | -e "s//${SP_APP_ID}/" \ 706 | -e "s//${SP_APP_KEY}/" \ 707 | ${DIR}/templates/acr-secret-template.yaml >${DIR}/secret.yaml 708 | fi 709 | fi 710 | 711 | echo "--> Installing Helm chart" 712 | $helm install $HELM_BINDERHUB_NAME jupyterhub/binderhub \ 713 | --version=$BINDERHUB_VERSION \ 714 | --namespace=$HELM_BINDERHUB_NAME \ 715 | -f ${DIR}/secret.yaml \ 716 | -f ${DIR}/config.yaml \ 717 | --create-namespace \ 718 | --timeout 10m0s \ 719 | --wait | tee binderhub-chart-install.log 720 | 721 | if [[ -n $ENABLE_HTTPS ]]; then 722 | # Be error tolerant for this stage 723 | set +e 724 | 725 | CLUSTER_RESOURCE_GROUP="MC_${RESOURCE_GROUP_NAME}_${AKS_NAME}_${RESOURCE_GROUP_LOCATION}" 726 | echo "--> Retrieving resources in ${CLUSTER_RESOURCE_GROUP}" 727 | 728 | IP_ADDRESS_NAME="$(az resource list -g "${CLUSTER_RESOURCE_GROUP}" --query "[?type == 'Microsoft.Network/publicIPAddresses'].name" -o tsv | grep ^kubernetes-)" 729 | echo "IP Address: ${IP_ADDRESS_NAME}" | tee ip-address-name.log 730 | 731 | ipAddressAttempts=0 732 | while [ -z "${IP_ADDRESS_NAME}" ]; do 733 | ((ipAddressAttempts++)) 734 | echo "--> IP Address Name pull attempt ${ipAddressAttempts} of 10 failed" 735 | if ((ipAddressAttempts > 9)); then 736 | echo "--> Failed to pull the IP Address name. You will have to set the A records manually. You can do this by running set_a_records.sh." 737 | break 738 | fi 739 | echo "--> Waiting 30s before trying again" 740 | sleep 30 741 | IP_ADDRESS_NAME="$(az resource list -g "${CLUSTER_RESOURCE_GROUP}" --query "[?type == 'Microsoft.Network/publicIPAddresses'].name" -o tsv | grep ^kubernetes-)" 742 | echo "IP Address: ${IP_ADDRESS_NAME}" | tee ip-address-name.log 743 | done 744 | 745 | if [ -n "${IP_ADDRESS_NAME}" ]; then 746 | IP_ADDRESS_ID="$(az resource show -g "${CLUSTER_RESOURCE_GROUP}" -n "${IP_ADDRESS_NAME}" --resource-type 'Microsoft.Network/publicIPAddresses' --query id -o tsv)" 747 | echo "IP Address ID: ${IP_ADDRESS_ID}" | tee ip-address-id.log 748 | 749 | az network dns record-set a update -n hub -g "${RESOURCE_GROUP_NAME}" -z "${DOMAIN_NAME}" --target-resource "${IP_ADDRESS_ID}" -o table | tee update-hub-a-record.log 750 | az network dns record-set a update -n binder -g "${RESOURCE_GROUP_NAME}" -z "${DOMAIN_NAME}" --target-resource "${IP_ADDRESS_ID}" -o table | tee update-binder-a-record.log 751 | fi 752 | 753 | # Revert to error-intolerance 754 | set -eo pipefail 755 | 756 | else 757 | # Wait for JupyterHub, grab its IP address, and update BinderHub to link together: 758 | echo "--> Retrieving JupyterHub IP" 759 | # shellcheck disable=SC2030 disable=SC2036 760 | JUPYTERHUB_IP=$(kubectl --namespace=$HELM_BINDERHUB_NAME get svc proxy-public | awk '{ print $4}' | tail -n 1) | tee jupyterhub-ip.log 761 | # shellcheck disable=SC2031 762 | while [ "${JUPYTERHUB_IP}" == '' ] || [ -z "${JUPYTERHUB_IP}" ]; do 763 | echo "Sleeping 30s before checking again" 764 | sleep 30 765 | JUPYTERHUB_IP=$(kubectl --namespace=$HELM_BINDERHUB_NAME get svc proxy-public | awk '{ print $4}' | tail -n 1) 766 | echo "JupyterHub IP: ${JUPYTERHUB_IP}" | tee jupyterhub-ip.log 767 | done 768 | 769 | if [ ${CONTAINER_REGISTRY} == 'dockerhub' ]; then 770 | 771 | echo "--> Finalising configurations" 772 | if [ -z "$DOCKERHUB_ORGANISATION" ]; then 773 | sed -e "s//${DOCKERHUB_USERNAME}/" \ 774 | -e "s//${DOCKER_IMAGE_PREFIX}/" \ 775 | -e "s//${JUPYTERHUB_IP}/" \ 776 | ${DIR}/templates/config-template.yaml >${DIR}/config.yaml 777 | else 778 | sed -e "s//${DOCKERHUB_ORGANISATION}/" \ 779 | -e "s//${DOCKERHUB_IMAGE_PREFIX}/" \ 780 | -e "s//${JUPYTERHUB_IP}/" \ 781 | ${DIR}/templates/config-template.yaml >${DIR}/config.yaml 782 | fi 783 | 784 | elif [ ${CONTAINER_REGISTRY} == 'azurecr' ]; then 785 | 786 | echo "--> Finalising configurations" 787 | sed -e "s@@${ACR_LOGIN_SERVER}@g" \ 788 | -e "s@@${DOCKER_IMAGE_PREFIX}@" \ 789 | -e "s//${JUPYTERHUB_IP}/" \ 790 | ${DIR}/templates/acr-config-template.yaml >${DIR}/config.yaml 791 | 792 | fi 793 | 794 | echo "--> Updating Helm chart" 795 | $helm upgrade $HELM_BINDERHUB_NAME jupyterhub/binderhub \ 796 | --namespace $HELM_BINDERHUB_NAME \ 797 | --version=$BINDERHUB_VERSION \ 798 | -f ${DIR}/secret.yaml \ 799 | -f ${DIR}/config.yaml \ 800 | --cleanup-on-fail \ 801 | --timeout 10m0s \ 802 | --wait | tee helm-upgrade.log 803 | 804 | # Print Binder IP address 805 | echo "--> Retrieving Binder IP" 806 | BINDER_IP=$(kubectl --namespace=$HELM_BINDERHUB_NAME get svc binder | awk '{ print $4}' | tail -n 1) 807 | echo "Binder IP: ${BINDER_IP}" | tee binder-ip.log 808 | while [ "${BINDER_IP}" = '' ] || [ "${BINDER_IP}" = "" ]; do 809 | echo "Sleeping 30s before checking again" 810 | sleep 30 811 | BINDER_IP=$(kubectl --namespace=$HELM_BINDERHUB_NAME get svc binder | awk '{ print $4}' | tail -n 1) 812 | echo "Binder IP: ${BINDER_IP}" | tee binder-ip.log 813 | done 814 | fi 815 | 816 | echo "BinderHub deployment completed!" 817 | 818 | if [[ -n $BINDERHUB_CONTAINER_MODE ]] || [[ "$LOG_TO_BLOB_STORAGE" == 'true' ]]; then 819 | # Finally, save outputs to blob storage 820 | # 821 | # Create a storage account 822 | echo "--> Creating storage account" 823 | CONTAINER_NAME="$(echo ${BINDERHUB_NAME}deploylogs | tr '[:upper:]' '[:lower:]')" 824 | STORAGE_ACCOUNT_NAME="$(echo ${BINDERHUB_NAME} | tr -cd '[:alnum:]' | tr '[:upper:]' '[:lower:]' | cut -c -20)$(openssl rand -hex 2)" 825 | az storage account create \ 826 | --name ${STORAGE_ACCOUNT_NAME} --resource-group ${RESOURCE_GROUP_NAME} \ 827 | --sku Standard_LRS -o table | tee storage-create.log 828 | # Create a container 829 | echo "--> Creating storage container: ${CONTAINER_NAME}" 830 | az storage container create --account-name ${STORAGE_ACCOUNT_NAME} \ 831 | --name ${CONTAINER_NAME} -o table | tee container-create.log 832 | # Push the files 833 | echo "--> Pushing log files" 834 | az storage blob upload-batch --account-name ${STORAGE_ACCOUNT_NAME} \ 835 | --destination ${CONTAINER_NAME} --source "." \ 836 | --pattern "*.log" -o table 837 | echo "--> Pushing yaml files" 838 | az storage blob upload-batch --account-name ${STORAGE_ACCOUNT_NAME} \ 839 | --destination ${CONTAINER_NAME} --source "." \ 840 | --pattern "*.yaml" -o table 841 | echo "--> Getting and pushing ssh keys" 842 | cp ~/.ssh/id_rsa ${DIR}/id_rsa_${BINDERHUB_NAME} 843 | cp ~/.ssh/id_rsa.pub ${DIR}/id_rsa_${BINDERHUB_NAME}.pub 844 | az storage blob upload-batch --account-name ${STORAGE_ACCOUNT_NAME} \ 845 | --destination ${CONTAINER_NAME} --source "." \ 846 | --pattern "id*" -o table 847 | fi 848 | -------------------------------------------------------------------------------- /src/info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get this script's path 4 | DIR="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" >/dev/null 2>&1 && pwd)" 5 | 6 | # Read in config.json and get variables 7 | configFile="${DIR}/config.json" 8 | AKS_RESOURCE_GROUP=$(jq -r '.azure .res_grp_name' "${configFile}") 9 | BINDERHUB_NAME=$(jq -r '.binderhub .name' "${configFile}") 10 | AKS_NAME=$(echo "${BINDERHUB_NAME}" | tr -cd '[:alnum:]-' | cut -c 1-59)-AKS 11 | HELM_BINDERHUB_NAME=$(echo "${BINDERHUB_NAME}" | tr -cd '[:alnum:]-.' | tr '[:upper:]' '[:lower:]' | sed -E -e 's/^([.-]+)//' -e 's/([.-]+)$//') 12 | 13 | # Get AKS cluster credentials 14 | echo "--> Get credentials for AKS cluster" 15 | az aks get-credentials -n "${AKS_NAME}" -g "${AKS_RESOURCE_GROUP}" 16 | 17 | # Print pods 18 | echo "--> Printing pods" 19 | kubectl get pods -n "${HELM_BINDERHUB_NAME}" 20 | echo 21 | 22 | # Get IP addresses of both the JupyterHub and BinderHub 23 | echo "Jupyterhub IP: $(kubectl --namespace="${HELM_BINDERHUB_NAME}" get svc proxy-public | awk '{ print $4}' | tail -n 1)" 24 | echo "Binderhub IP: $(kubectl --namespace="${HELM_BINDERHUB_NAME}" get svc binder | awk '{ print $4}' | tail -n 1)" 25 | -------------------------------------------------------------------------------- /src/logs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get this script's path 4 | DIR="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" >/dev/null 2>&1 && pwd)" 5 | 6 | # Read config.json and get BinderHub name 7 | configFile="${DIR}/config.json" 8 | AKS_RESOURCE_GROUP=$(jq -r '.azure .res_grp_name' "${configFile}") 9 | BINDERHUB_NAME=$(jq -r '.binderhub .name' "${configFile}") 10 | AKS_NAME=$(echo "${BINDERHUB_NAME}" | tr -cd '[:alnum:]-' | cut -c 1-59)-AKS 11 | HELM_BINDERHUB_NAME=$(echo "${BINDERHUB_NAME}" | tr -cd '[:alnum:]-.' | tr '[:upper:]' '[:lower:]' | sed -E -e 's/^([.-]+)//' -e 's/([.-]+)$//') 12 | 13 | # Getting credentials for AKS cluster 14 | echo "--> Getting credentials for AKS cluster" 15 | az aks get-credentials -n "${AKS_NAME}" -g "${AKS_RESOURCE_GROUP}" 16 | 17 | echo "--> Fetching JupyterHub logs" 18 | 19 | # Get pod name of the JupyterHub 20 | HUB_POD=$(kubectl get pods -n "${HELM_BINDERHUB_NAME}" -o=jsonpath='{.items[*].metadata.name}' | tr ' ' '\n' | grep "^hub-") 21 | 22 | # Print the JupyterHub logs to the terminal 23 | kubectl logs "${HUB_POD}" -n "${HELM_BINDERHUB_NAME}" 24 | -------------------------------------------------------------------------------- /src/set_a_records.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | # Get this script's path 6 | DIR="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" >/dev/null 2>&1 && pwd)" 7 | 8 | # Read config.json and get BinderHub name 9 | configFile="${DIR}/config.json" 10 | AKS_RESOURCE_GROUP=$(jq -r '.azure .res_grp_name' "${configFile}") 11 | RESOURCE_GROUP_LOCATION=$(jq -r '.azure .location' "${configFile}") 12 | BINDERHUB_NAME=$(jq -r '.binderhub .name' "${configFile}") 13 | DOMAIN_NAME=$(jq -r '.https .domain_name' "${configFile}") 14 | AKS_NAME=$(echo "${BINDERHUB_NAME}" | tr -cd '[:alnum:]-' | cut -c 1-59)-AKS 15 | 16 | CLUSTER_RESOURCE_GROUP="MC_${AKS_RESOURCE_GROUP}_${AKS_NAME}_${RESOURCE_GROUP_LOCATION}" 17 | echo "Resource Group: ${CLUSTER_RESOURCE_GROUP}" 18 | 19 | IP_ADDRESS_NAME="$(az resource list -g "${CLUSTER_RESOURCE_GROUP}" --query "[?type == 'Microsoft.Network/publicIPAddresses'].name" -o tsv | grep ^kubernetes-)" 20 | echo "IP Address: ${IP_ADDRESS_NAME}" | ip-address-name.log 21 | 22 | ipAddressAttempts=0 23 | while [ -z "${IP_ADDRESS_NAME}" ]; do 24 | ((ipAddressAttempts++)) 25 | echo "--> IP Address Name pull attempt ${ipAddressAttempts} of 10 failed" 26 | if ((ipAddressAttempts > 9)); then 27 | echo "--> Failed to pull the IP Address name. You will have to set the A records manually." 28 | break 29 | fi 30 | echo "--> Waiting 30s before trying again" 31 | sleep 30 32 | IP_ADDRESS_NAME="$(az resource list -g "${CLUSTER_RESOURCE_GROUP}" --query "[?type == 'Microsoft.Network/publicIPAddresses'].name" -o tsv | grep ^kubernetes-)" 33 | echo "IP Address Name: ${IP_ADDRESS_NAME}" | tee ip-address-name.log 34 | done 35 | 36 | if [ -n "${IP_ADDRESS_NAME}" ]; then 37 | IP_ADDRESS_ID="$(az resource show -g "${CLUSTER_RESOURCE_GROUP}" -n "${IP_ADDRESS_NAME}" --resource-type 'Microsoft.Network/publicIPAddresses' --query id -o tsv)" 38 | echo "IP Address ID: ${IP_ADDRESS_ID}" | tee ip-address-id.log 39 | 40 | az network dns record-set a update -n hub -g "${AKS_RESOURCE_GROUP}" -z "${DOMAIN_NAME}" --target-resource "${IP_ADDRESS_ID}" -o table | tee update-hub-a-record.log 41 | az network dns record-set a update -n binder -g "${AKS_RESOURCE_GROUP}" -z "${DOMAIN_NAME}" --target-resource "${IP_ADDRESS_ID}" -o table | tee update-binder-a-record.log 42 | fi 43 | -------------------------------------------------------------------------------- /src/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck disable=SC2046 disable=SC2143 3 | 4 | # Check sudo availability 5 | sudo_command=$(command -v sudo) 6 | 7 | ## Linux install cases 8 | if [[ ${OSTYPE} == 'linux'* ]]; then 9 | echo "--> This is a Linux build" 10 | 11 | ## apt-based systems 12 | if command -v apt >/dev/null 2>&1; then 13 | echo "--> Checking system packages and installing any missing packages" 14 | # Update apt before starting, in case this is a new container 15 | APTPACKAGES=" \ 16 | curl \ 17 | python \ 18 | openssl \ 19 | jq \ 20 | " 21 | 22 | for package in $APTPACKAGES; do 23 | if ! dpkg -s "$package" >/dev/null; then 24 | echo "--> Apt installing $package" 25 | (${sudo_command} apt update && ${sudo_command} apt install -y "$package") || { 26 | echo >&2 "--> $package install failed; please install manually and re-run this script." 27 | exit 1 28 | } 29 | else 30 | echo "--> $package already installed" 31 | fi 32 | done 33 | if ! command -v az >/dev/null 2>&1; then 34 | echo "--> Attempting to install Azure-CLI with deb packages" 35 | curl -sL https://aka.ms/InstallAzureCLIDeb | ${sudo_command} bash || { 36 | echo >&2 "--> Azure-CLI install failed; please install manually and re-run this script." 37 | exit 1 38 | } 39 | else 40 | echo "--> Azure-CLI already installed" 41 | fi 42 | if ! command -v kubectl >/dev/null 2>&1; then 43 | echo "--> Attempting to install kubectl with deb packages" 44 | ${sudo_command} apt-get update && ${sudo_command} apt-get install -y apt-transport-https 45 | curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | ${sudo_command} apt-key add - 46 | echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | ${sudo_command} tee -a /etc/apt/sources.list.d/kubernetes.list 47 | (${sudo_command} apt-get update && ${sudo_command} apt-get install -y kubectl) || { 48 | echo >&2 "--> kubectl install failed; please install manually and re-run this script." 49 | exit 1 50 | } 51 | else 52 | echo "--> kubectl already installed" 53 | fi 54 | 55 | ## yum-based systems 56 | elif command -v yum >/dev/null 2>&1; then 57 | if [ $(grep -iq centos /etc/redhat-release) ]; then 58 | echo "***************************************************************" 59 | echo "* You appear to be running CentOS. A required package, jq, is *" 60 | echo "* not available from core repositories but can be installed *" 61 | echo "* from the epel-release repository. If a jq install fails, *" 62 | echo "* run the following command as root (or with sudo) to enable *" 63 | echo "* the epel repository: *" 64 | echo "* *" 65 | echo "* yum -y install epel-release *" 66 | echo "* *" 67 | echo "***************************************************************" 68 | fi 69 | echo "--> Checking system packages and installing any missing packages" 70 | YUMPACKAGES=" \ 71 | jq \ 72 | curl \ 73 | python \ 74 | tar \ 75 | which \ 76 | openssl \ 77 | " 78 | for package in $YUMPACKAGES; do 79 | if ! rpm -q "$package" >/dev/null; then 80 | echo "--> Yum installing $package" 81 | ${sudo_command} yum install -y "$package" || { 82 | echo >&2 "--> $package install failed; please install manually and re-run this script." 83 | exit 1 84 | } 85 | else 86 | echo "--> $package already installed" 87 | fi 88 | done 89 | if ! command -v az >/dev/null 2>&1; then 90 | echo "--> Attempting to install Azure-CLI with yum packages" 91 | ${sudo_command} rpm --import https://packages.microsoft.com/keys/microsoft.asc 92 | ${sudo_command} sh -c 'echo -e "[azure-cli]\nname=Azure CLI\nbaseurl=https://packages.microsoft.com/yumrepos/azure-cli\nenabled=1\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" > /etc/yum.repos.d/azure-cli.repo' 93 | ${sudo_command} yum install -y azure-cli || { 94 | echo >&2 "--> Azure-CLI install failed; please install manually and re-run this script." 95 | exit 1 96 | } 97 | else 98 | echo "--> Azure-CLI already installed" 99 | fi 100 | if ! command -v kubectl >/dev/null 2>&1; then 101 | echo "--> Attempting to install kubectl with yum packages" 102 | echo "[kubernetes] 103 | name=Kubernetes 104 | baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64 105 | enabled=1 106 | gpgcheck=1 107 | repo_gpgcheck=1 108 | gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg 109 | " | ${sudo_command} tee /etc/yum.repos.d/kubernetes.repo 110 | ${sudo_command} yum install -y kubectl || { 111 | echo >&2 "--> kubectl install failed; please install manually and re-run this script." 112 | exit 1 113 | } 114 | else 115 | echo "--> kubectl already installed" 116 | fi 117 | 118 | ## zypper-based systems 119 | elif command -v zypper >/dev/null 2>&1; then 120 | echo "--> Checking system packages and installing any missing packages" 121 | ZYPPERPACKAGES=" \ 122 | curl \ 123 | python \ 124 | tar \ 125 | which \ 126 | jq \ 127 | openssl \ 128 | " 129 | for package in $ZYPPERPACKAGES; do 130 | if ! rpm -q "$package" >/dev/null; then 131 | echo "--> Zypper installing $package" 132 | ${sudo_command} zypper install -y "$package" || { 133 | echo >&2 "--> $package install failed; please install manually and re-run this script." 134 | exit 1 135 | } 136 | else 137 | echo "--> $package already installed" 138 | fi 139 | done 140 | if ! command -v az >/dev/null 2>&1; then 141 | echo "--> Attempting to install Azure-CLI with zypper packages" 142 | ${sudo_command} rpm --import https://packages.microsoft.com/keys/microsoft.asc 143 | ${sudo_command} zypper addrepo --name 'Azure CLI' --check https://packages.microsoft.com/yumrepos/azure-cli azure-cli 144 | ${sudo_command} zypper install --from azure-cli -y azure-cli || { 145 | echo >&2 "--> azure-cli install failed; please install manually and re-run this script." 146 | exit 1 147 | } 148 | # The az-cli installer misses python-xml dependency on suse 149 | ${sudo_command} zypper install -y python-xml || { 150 | echo >&2 "--> python-xml install failed; please install manually and re-run this script." 151 | exit 1 152 | } 153 | else 154 | echo "--> Azure-CLI already installed" 155 | fi 156 | if ! command -v kubectl >/dev/null 2>&1; then 157 | echo "--> Attempting to install kubectl with zypper packages" 158 | zypper ar -f https://download.opensuse.org/tumbleweed/repo/oss/ factory 159 | zypper install -y kubectl || { 160 | echo >&2 "--> kubectl install failed; please install manually and re-run this script." 161 | exit 1 162 | } 163 | else 164 | echo "--> kubectl already installed" 165 | fi 166 | 167 | ## pacman-based systems 168 | elif command -v pacman >/dev/null 2>&1; then 169 | echo "--> Checking system packages and installing any missing packages" 170 | PACMANPACKAGES=" \ 171 | curl \ 172 | python \ 173 | tar \ 174 | which \ 175 | jq \ 176 | gcc \ 177 | awk \ 178 | grep \ 179 | openssl \ 180 | kubectl \ 181 | " 182 | 183 | for package in $PACMANPACKAGES; do 184 | if ! pacman -Q "$package" 2>/dev/null; then 185 | echo "--> pacman installing $package" 186 | ${sudo_command} pacman -Sy --noconfirm "$package" || { 187 | echo >&2 "--> $package install failed; please install manually and re-run this script." 188 | exit 1 189 | } 190 | else 191 | echo "--> $package already installed" 192 | fi 193 | done 194 | if ! command -v az >/dev/null 2>&1; then 195 | echo "--> Attempting to install Azure-CLI with curl" 196 | curl -L https://aka.ms/InstallAzureCli | sh || { 197 | echo >&2 "--> Azure-CLI install failed; please install manually and re-run this script." 198 | exit 1 199 | } 200 | else 201 | echo "--> Azure-CLI already installed" 202 | fi 203 | 204 | ## Mystery linux system without any of our recognised package managers 205 | else 206 | command -v curl >/dev/null 2>&1 || { 207 | echo >&2 "curl not found; please install and re-run this script." 208 | exit 1 209 | } 210 | command -v awk >/dev/null 2>&1 || { 211 | echo >&2 "awk not found; please install and re-run this script." 212 | exit 1 213 | } 214 | command -v grep >/dev/null 2>&1 || { 215 | echo >&2 "grep not found; please install and re-run this script." 216 | exit 1 217 | } 218 | command -v python >/dev/null 2>&1 || { 219 | echo >&2 "python not found; please install and re-run this script." 220 | exit 1 221 | } 222 | command -v jq >/dev/null 2>&1 || { 223 | echo >&2 "jq not found; please install and re-run this script." 224 | exit 1 225 | } 226 | echo "--> Attempting to install Azure-CLI with curl" 227 | if ! command -v az >/dev/null 2>&1; then 228 | curl -L https://aka.ms/InstallAzureCli | sh || { 229 | echo >&2 "--> Azure-CLI install failed; please install manually and re-run this script." 230 | exit 1 231 | } 232 | else 233 | echo "--> Azure-CLI already installed" 234 | fi 235 | echo "--> Attempting to install kubectl with curl" 236 | if ! command -v kubectl >/dev/null 2>&1; then 237 | curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl || { 238 | echo >&2 "--> kubectl download failed; please install manually and re-run this script." 239 | exit 1 240 | } 241 | chmod +x ./kubectl 242 | ${sudo_command} mv ./kubectl /usr/local/bin/kubectl 243 | else 244 | echo "--> kubectl already installed" 245 | fi 246 | fi 247 | 248 | ## Helm isn't well packaged for Linux, alas 249 | command -v curl >/dev/null 2>&1 || { 250 | echo >&2 "curl not found; please install and re-run this script." 251 | exit 1 252 | } 253 | command -v awk >/dev/null 2>&1 || { 254 | echo >&2 "awk not found; please install and re-run this script." 255 | exit 1 256 | } 257 | command -v grep >/dev/null 2>&1 || { 258 | echo >&2 "grep not found; please install and re-run this script." 259 | exit 1 260 | } 261 | command -v python >/dev/null 2>&1 || { 262 | echo >&2 "python not found; please install and re-run this script." 263 | exit 1 264 | } 265 | command -v tar >/dev/null 2>&1 || { 266 | echo >&2 "tar not found; please install and re-run this script." 267 | exit 1 268 | } 269 | command -v which >/dev/null 2>&1 || { 270 | echo >&2 "which not found; please install and re-run this script." 271 | exit 1 272 | } 273 | echo "--> Helm doesn't have a system package; attempting to install with curl" 274 | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 275 | chmod 700 get_helm.sh 276 | ./get_helm.sh 277 | 278 | ## Installing on OS X 279 | elif [[ ${OSTYPE} == 'darwin'* ]]; then 280 | echo "--> This is a MacOS build" 281 | if command -v brew >/dev/null 2>&1; then 282 | echo "--> Checking brew packages and installing any missing packages" 283 | BREWPACKAGES=" \ 284 | curl \ 285 | python \ 286 | azure-cli \ 287 | kubernetes-cli \ 288 | helm \ 289 | jq \ 290 | " 291 | 292 | brew update 293 | for package in $BREWPACKAGES; do 294 | if ! brew ls --versions "$package" >/dev/null; then 295 | echo "--> Brew installing $package" 296 | brew install "$package" || { 297 | echo >&2 "--> $package install failed; please install manually and re-run this script." 298 | exit 1 299 | } 300 | else 301 | echo "--> $package is already installed" 302 | fi 303 | done 304 | else 305 | command -v curl >/dev/null 2>&1 || { 306 | echo >&2 "curl not found; please install and re-run this script." 307 | exit 1 308 | } 309 | command -v python >/dev/null 2>&1 || { 310 | echo >&2 "python not found; please install and re-run this script." 311 | exit 1 312 | } 313 | command -v tar >/dev/null 2>&1 || { 314 | echo >&2 "tar not found; please install and re-run this script." 315 | exit 1 316 | } 317 | command -v which >/dev/null 2>&1 || { 318 | echo >&2 "which not found; please install and re-run this script." 319 | exit 1 320 | } 321 | echo "--> Attempting to install Azure-CLI with curl" 322 | if ! command -v az >/dev/null 2>&1; then 323 | curl -L https://aka.ms/InstallAzureCli | sh || { 324 | echo >&2 "--> Azure-CLI install failed; please install manually and re-run this script." 325 | exit 1 326 | } 327 | else 328 | echo "--> Azure-CLI already installed" 329 | fi 330 | echo "--> Attempting to install kubectl with curl" 331 | if ! command -v kubectl >/dev/null 2>&1; then 332 | curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/darwin/amd64/kubectl || { 333 | echo >&2 "--> kubectl download failed; please install manually and re-run this script." 334 | exit 1 335 | } 336 | chmod +x ./kubectl 337 | ${sudo_command} mv ./kubectl /usr/local/bin/kubectl 338 | else 339 | echo "--> kubectl already installed" 340 | fi 341 | echo "--> Attempting to install helm with curl" 342 | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 343 | chmod 700 get_helm.sh 344 | ./get_helm.sh 345 | fi 346 | else 347 | echo "--> This is a Windows build" 348 | ## chocolatey-based systems 349 | if command -v choco >/dev/null 2>&1; then 350 | echo "--> Checking chocolatey packages and installing any missing packages" 351 | CHOCPACKAGES=" \ 352 | curl \ 353 | python \ 354 | azure-cli \ 355 | kubernetes-cli \ 356 | kubernetes-helm \ 357 | jq \ 358 | " 359 | choco upgrade chocolatey -y 360 | for package in $CHOCPACKAGES; do 361 | if ! choco search --local-only "$package" >/dev/null; then 362 | echo "--> Choco installing $package" 363 | choco install "$package" || { 364 | echo >&2 "--> $package install failed; please install manually and re-run this script." 365 | exit 1 366 | } 367 | else 368 | echo "--> $package is already installed" 369 | fi 370 | done 371 | else 372 | command -v curl >/dev/null 2>&1 || { 373 | echo >&2 "curl not found; please install and re-run this script." 374 | exit 1 375 | } 376 | command -v python >/dev/null 2>&1 || { 377 | echo >&2 "python not found; please install and re-run this script." 378 | exit 1 379 | } 380 | command -v tar >/dev/null 2>&1 || { 381 | echo >&2 "tar not found; please install and re-run this script." 382 | exit 1 383 | } 384 | command -v which >/dev/null 2>&1 || { 385 | echo >&2 "which not found; please install and re-run this script." 386 | exit 1 387 | } 388 | echo "--> Attempting to install Azure-CLI with curl" 389 | if ! command -v az >/dev/null 2>&1; then 390 | curl -L https://aka.ms/InstallAzureCli | sh || { 391 | echo >&2 "--> Azure-CLI install failed; please install manually and re-run this script." 392 | exit 1 393 | } 394 | else 395 | echo "--> Azure-CLI already installed" 396 | fi 397 | echo "--> Attempting to install kubectl with curl" 398 | if ! command -v kubectl >/dev/null 2>&1; then 399 | curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/darwin/amd64/kubectl || { 400 | echo >&2 "--> kubectl download failed; please install manually and re-run this script." 401 | exit 1 402 | } 403 | chmod +x ./kubectl 404 | ${sudo_command} mv ./kubectl /usr/local/bin/kubectl 405 | else 406 | echo "--> kubectl already installed" 407 | fi 408 | echo "--> Attempting to install helm with curl" 409 | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 410 | chmod 700 get_helm.sh 411 | ./get_helm.sh 412 | fi 413 | fi 414 | -------------------------------------------------------------------------------- /src/teardown.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get this script's path 4 | DIR="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" >/dev/null 2>&1 && pwd)" 5 | 6 | # Read in config.json 7 | configFile="${DIR}"/config.json 8 | BINDERHUB_NAME=$(jq -r '.binderhub .name' "${configFile}") 9 | RESOURCE_GROUP=$(jq -r '.azure .res_grp_name' "${configFile}") 10 | ENABLE_HTTPS=$(jq -r '.enable_https' "${configFile}") 11 | 12 | AKS_NAME=$(echo "${BINDERHUB_NAME}" | tr -cd '[:alnum:]-' | cut -c 1-59)-AKS 13 | AKS_USERNAME="users.clusterUser_${RESOURCE_GROUP}_${AKS_NAME}" 14 | HELM_BINDERHUB_NAME=$(echo "${BINDERHUB_NAME}" | tr -cd '[:alnum:]-.' | tr '[:upper:]' '[:lower:]' | sed -E -e 's/^([.-]+)//' -e 's/([.-]+)$//') 15 | 16 | # If CRDs were installed, delete them 17 | if [[ -n $ENABLE_HTTPS ]]; then 18 | echo "--> Deleting Custom Resource Definitions" 19 | kubectl delete crds --all 20 | kubectl delete apiservices v1beta1.webhook.cert-manager.io 21 | fi 22 | 23 | # Purge the Helm release and delete the Kubernetes namespace 24 | echo "--> Purging the helm chart: ${HELM_BINDERHUB_NAME}" 25 | helm delete "${HELM_BINDERHUB_NAME}" --timeout 10m0s 26 | 27 | echo "--> Deleting the namespace: ${HELM_BINDERHUB_NAME}" 28 | kubectl delete namespace "${HELM_BINDERHUB_NAME}" 29 | 30 | echo "--> Purging the kubectl config file" 31 | kubectl config unset current-context 32 | kubectl config delete-cluster "${AKS_NAME}" 33 | kubectl config delete-context "${AKS_NAME}" 34 | kubectl config unset "${AKS_USERNAME}" 35 | 36 | # Delete Azure Resource Group 37 | echo "--> Deleting the resource group: ${RESOURCE_GROUP}" 38 | az group delete -n "${RESOURCE_GROUP}" --yes --no-wait 39 | 40 | echo "--> Deleting the resource group: NetworkWatcherRG" 41 | az group delete -n NetworkWatcherRG --yes --no-wait 42 | 43 | echo "NOTE: It is a long running process to delete a resource group." 44 | echo " The groups are probably still undergoing deletion presently." 45 | echo "Double check resources are down:" 46 | echo " https://portal.azure.com/#home -> Click on Resource Groups" 47 | echo "Check your DockerHub registry:" 48 | echo " https://hub.docker.com/" 49 | echo "For more info: https://zero-to-jupyterhub.readthedocs.io/en/latest/turn-off.html#delete-the-helm-release" 50 | echo " https://zero-to-jupyterhub.readthedocs.io/en/latest/turn-off.html#microsoft-azure-aks" 51 | -------------------------------------------------------------------------------- /src/upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get this script's path 4 | DIR="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" >/dev/null 2>&1 && pwd)" 5 | 6 | # Read in config.json and get variables 7 | echo "--> Reading in config.json" 8 | configFile="${DIR}/config.json" 9 | AKS_RESOURCE_GROUP=$(jq -r '.azure .res_grp_name' "${configFile}") 10 | BINDERHUB_NAME=$(jq -r '.binderhub .name' "${configFile}") 11 | BINDERHUB_VERSION=$(jq -r '.binderhub .version' "${configFile}") 12 | 13 | # Generate a valid name for the AKS cluster 14 | AKS_NAME=$(echo "${BINDERHUB_NAME}" | tr -cd '[:alnum:]-' | cut -c 1-59)-AKS 15 | 16 | # Format BinderHub name for Kubernetes 17 | HELM_BINDERHUB_NAME=$(echo "${BINDERHUB_NAME}" | tr -cd '[:alnum:]-.' | tr '[:upper:]' '[:lower:]' | sed -E -e 's/^([.-]+)//' -e 's/([.-]+)$//') 18 | 19 | # Get cluster credentials 20 | echo "--> Getting credentials for AKS cluster" 21 | az aks get-credentials -n "${AKS_NAME}" -g "${AKS_RESOURCE_GROUP}" 22 | 23 | # Pull and update helm chart repo 24 | echo "--> Updating helm chart repo" 25 | helm repo add jupyterhub https://jupyterhub.github.io/helm-chart 26 | helm repo update 27 | 28 | # Upgrade helm chart 29 | echo "--> Upgrading ${HELM_BINDERHUB_NAME}'s helm chart with version ${BINDERHUB_VERSION}" 30 | helm upgrade "${HELM_BINDERHUB_NAME}" jupyterhub/binderhub \ 31 | --namespace "${HELM_BINDERHUB_NAME}" \ 32 | --version="${BINDERHUB_VERSION}" \ 33 | -f "${DIR}/secret.yaml" \ 34 | -f "${DIR}/config.yaml" \ 35 | --cleanup-on-fail \ 36 | --timeout 10m0s \ 37 | --wait 38 | 39 | # Print Kubernetes pods 40 | echo "--> Getting pods" 41 | kubectl get pods -n "${HELM_BINDERHUB_NAME}" 42 | -------------------------------------------------------------------------------- /template-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_registry": "", 3 | "enable_https": false, 4 | "acr": { 5 | "registry_name": null, 6 | "sku": "Basic" 7 | }, 8 | "azure": { 9 | "subscription": "", 10 | "res_grp_name": "", 11 | "location": "", 12 | "node_count": 1, 13 | "vm_size": "", 14 | "sp_app_id": null, 15 | "sp_app_key": null, 16 | "sp_tenant_id": null, 17 | "log_to_blob_storage": false 18 | }, 19 | "binderhub": { 20 | "name": "", 21 | "version": "", 22 | "image_prefix": "" 23 | }, 24 | "docker": { 25 | "username": null, 26 | "password": null, 27 | "org": null 28 | }, 29 | "https": { 30 | "certmanager_version": null, 31 | "contact_email": null, 32 | "domain_name": null, 33 | "nginx_version": null 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /templates/acr-config-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | config: 3 | BinderHub: 4 | use_registry: true 5 | image_prefix: /- 6 | hub_url: http:// 7 | DockerRegistry: 8 | token_url: 9 | "https:///oauth2/token?service=" 10 | -------------------------------------------------------------------------------- /templates/acr-secret-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | jupyterhub: 3 | hub: 4 | services: 5 | binder: 6 | apiToken: "" 7 | proxy: 8 | secretToken: "" 9 | 10 | registry: 11 | url: https:// 12 | username: 13 | password: 14 | -------------------------------------------------------------------------------- /templates/cluster-issuer-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: cert-manager.io/v1alpha2 3 | kind: ClusterIssuer 4 | metadata: 5 | name: letsencrypt-staging 6 | namespace: 7 | spec: 8 | acme: 9 | server: https://acme-staging-v02.api.letsencrypt.org/directory 10 | email: 11 | privateKeySecretRef: 12 | name: letsencrypt-staging 13 | solvers: 14 | - http01: 15 | ingress: 16 | class: nginx 17 | --- 18 | apiVersion: cert-manager.io/v1alpha2 19 | kind: ClusterIssuer 20 | metadata: 21 | name: letsencrypt-prod 22 | namespace: 23 | spec: 24 | acme: 25 | server: https://acme-v02.api.letsencrypt.org/directory 26 | email: 27 | privateKeySecretRef: 28 | name: letsencrypt-prod 29 | solvers: 30 | - http01: 31 | ingress: 32 | class: nginx 33 | -------------------------------------------------------------------------------- /templates/config-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | config: 3 | BinderHub: 4 | use_registry: true 5 | image_prefix: /- 6 | hub_url: http:// 7 | -------------------------------------------------------------------------------- /templates/https-acr-config-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | config: 3 | BinderHub: 4 | use_registry: true 5 | image_prefix: /- 6 | hub_url: https:// 7 | DockerRegistry: 8 | token_url: 9 | "https:///oauth2/token?service=" 10 | 11 | service: 12 | type: ClusterIP 13 | 14 | ingress: 15 | enabled: true 16 | annotations: 17 | kubernetes.io/tls-acme: "true" 18 | kubernetes.io/ingress.class: nginx 19 | cert-manager.io/cluster-issuer: 20 | https: 21 | enabled: true 22 | type: nginx 23 | hosts: 24 | - 25 | tls: 26 | - secretName: 27 | hosts: 28 | - 29 | 30 | jupyterhub: 31 | proxy: 32 | service: 33 | type: ClusterIP 34 | 35 | ingress: 36 | enabled: true 37 | annotations: 38 | kubernetes.io/tls-acme: "true" 39 | kubernetes.io/ingress.class: nginx 40 | cert-manager.io/cluster-issuer: 41 | https: 42 | enabled: true 43 | type: nginx 44 | hosts: 45 | - 46 | tls: 47 | - secretName: 48 | hosts: 49 | - 50 | 51 | nginx-ingress: 52 | controller: 53 | service: 54 | loadBalancerIP: 55 | config: 56 | proxy-body-size: 64m 57 | -------------------------------------------------------------------------------- /templates/https-config-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | config: 3 | BinderHub: 4 | use_registry: true 5 | image_prefix: /- 6 | hub_url: https:// 7 | 8 | service: 9 | type: ClusterIP 10 | 11 | ingress: 12 | enabled: true 13 | annotations: 14 | kubernetes.io/tls-acme: "true" 15 | kubernetes.io/ingress.class: nginx 16 | cert-manager.io/cluster-issuer: "" 17 | https: 18 | enabled: true 19 | type: nginx 20 | host: 21 | - 22 | tls: 23 | - secretName: 24 | hosts: 25 | - 26 | 27 | jupyterhub: 28 | proxy: 29 | service: 30 | type: ClusterIP 31 | 32 | ingress: 33 | enabled: true 34 | annotations: 35 | kubernetes.io/tls-acme: "true" 36 | kubernetes.io/ingress.class: nginx 37 | cert-manager.io/cluster-issuer: "" 38 | https: 39 | enabled: true 40 | type: nginx 41 | hosts: 42 | - 43 | tls: 44 | - secretName: 45 | hosts: 46 | - 47 | 48 | nginx-ingress: 49 | controller: 50 | service: 51 | loadBalancerIP: 52 | config: 53 | proxy-body-size: 64m 54 | -------------------------------------------------------------------------------- /templates/secret-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | jupyterhub: 3 | hub: 4 | services: 5 | binder: 6 | apiToken: "" 7 | proxy: 8 | secretToken: "" 9 | 10 | registry: 11 | username: 12 | password: 13 | -------------------------------------------------------------------------------- /templates/test-resources.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: cert-manager-test 6 | --- 7 | apiVersion: cert-manager.io/v1alpha2 8 | kind: Issuer 9 | metadata: 10 | name: test-selfsigned 11 | namespace: cert-manager-test 12 | spec: 13 | selfSigned: {} 14 | --- 15 | apiVersion: cert-manager.io/v1alpha2 16 | kind: Certificate 17 | metadata: 18 | name: selfsigned-cert 19 | namespace: cert-manager-test 20 | spec: 21 | commonName: example.com 22 | secretName: selfsigned-cert-tls 23 | issuerRef: 24 | name: test-selfsigned 25 | --------------------------------------------------------------------------------