diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6118699665..b133e091e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,4 +50,4 @@ repos: hooks: - id: sops-encryption # Add files here if they contain the word 'secret' but should not be encrypted - exclude: secrets\.md|helm-charts/support/templates/prometheus-ingres-auth/secret\.yaml + exclude: secrets\.md|helm-charts/support/templates/prometheus-ingres-auth/secret\.yaml|helm-charts/basehub/templates/dex/secret\.yaml|helm-charts/basehub/templates/static/secret\.yaml diff --git a/config/clusters/2i2c/cluster.yaml b/config/clusters/2i2c/cluster.yaml index aa79db158a..8da6f8b403 100644 --- a/config/clusters/2i2c/cluster.yaml +++ b/config/clusters/2i2c/cluster.yaml @@ -16,14 +16,10 @@ hubs: domain: staging.2i2c.cloud helm_chart: basehub auth0: - # connection update? Also ensure the basehub Helm chart is provided a - # matching value for jupyterhub.custom.2i2c.add_staff_user_ids_of_type! connection: google-oauth2 helm_chart_values_files: - # The order in which you list files here is the order the will be passed - # to the helm upgrade command in, and that has meaning. Please check - # that you intend for these files to be applied in this order. - staging.values.yaml + - enc-staging.secret.values.yaml - name: dask-staging display_name: "2i2c dask staging" domain: dask-staging.2i2c.cloud diff --git a/config/clusters/2i2c/enc-staging.secret.values.yaml b/config/clusters/2i2c/enc-staging.secret.values.yaml index a858020c50..6bc065c916 100644 --- a/config/clusters/2i2c/enc-staging.secret.values.yaml +++ b/config/clusters/2i2c/enc-staging.secret.values.yaml @@ -1,3 +1,8 @@ +staticWebsite: + githubAuth: + githubApp: + id: ENC[AES256_GCM,data:EDJXakop,iv:BPu3qh9zpMVJWVNxPIfScoaeA+avSsacqxA6adriU98=,tag:wHxbuCOLrjvyu8/m0bj3rg==,type:int] + privateKey: ENC[AES256_GCM,data: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,iv:iu/nD/1zYDo9OY4q+j0sd+E5ODjLHZ3Otbkd0qte8WM=,tag:wD9S4cOGUeCVZIh9mRrI/w==,type:str] jupyterhub: hub: config: @@ -7,13 +12,14 @@ jupyterhub: sops: kms: [] gcp_kms: - - resource_id: projects/two-eye-two-see/locations/global/keyRings/sops-keys/cryptoKeys/similar-hubs - created_at: '2022-03-21T10:38:36Z' - enc: CiQA4OM7eJn+A9b0ulwu64MnqKfDM1EwtoKzj7Utg4iXOccLCroSSQDm5XgWCwOR2/FDqBIOrVVltPV7nASpq8h+fiHw5dYTSaPUyAMYwQ62iytA2kwTGQcOMmtxZVhn4dpGt2b0VlEvdiHP02Cgzvo= + - resource_id: projects/two-eye-two-see/locations/global/keyRings/sops-keys/cryptoKeys/similar-hubs + created_at: "2022-03-21T10:38:36Z" + enc: CiQA4OM7eJn+A9b0ulwu64MnqKfDM1EwtoKzj7Utg4iXOccLCroSSQDm5XgWCwOR2/FDqBIOrVVltPV7nASpq8h+fiHw5dYTSaPUyAMYwQ62iytA2kwTGQcOMmtxZVhn4dpGt2b0VlEvdiHP02Cgzvo= azure_kv: [] hc_vault: [] - lastmodified: '2022-03-21T10:38:36Z' - mac: ENC[AES256_GCM,data:D50ijsF6YmlX/El96nrIgxEcEmfbJabVvKIO33zi8PfjqkQZj7L9XdGMz9FzNRvtSu2+PwhZRr+98pqWb4N2SvuVjqPfskJwigVVQifNxOtI2P3V2LvnA/rnYvvTkpfzrcwBJPHsUL8VCAeY8OjxdEpamqFsrlyFG4z2HQ0dAQg=,iv:uDkxaW/3dZZTer1iuqhfIHtZ8vvOY7TCKfFnaI2pcZM=,tag:XjF32PtqZifGwW0LsKa/8Q==,type:str] + age: [] + lastmodified: "2022-07-09T21:14:35Z" + mac: ENC[AES256_GCM,data:xfxx30hp48xu13udRZUkEBX0C4RirV6sO6jqDICdfQkvQI/EtvUWF97GCFWgfAlnOI/W26BSftpg6QW9IwyzUO55PfJy/L4TQ03Ee+thu51tXjAywHfswWvE/ovA9fKQ4fU2QcNHb72Q53qUsx912CR7sXOvQajRekMPXZSJU10=,iv:f195Wlf+Yjods46WKt40Db88J1yGiTZDBWCmQWO3kF0=,tag:PIJAjOIRaLKRVmMFkBxEkQ==,type:str] pgp: [] unencrypted_suffix: _unencrypted - version: 3.6.1 + version: 3.7.1 diff --git a/config/clusters/2i2c/staging.values.yaml b/config/clusters/2i2c/staging.values.yaml index 8cd8eced20..81c011865c 100644 --- a/config/clusters/2i2c/staging.values.yaml +++ b/config/clusters/2i2c/staging.values.yaml @@ -1,9 +1,21 @@ +dex: + enabled: true + hubHostName: staging.2i2c.cloud + +staticWebsite: + enabled: true + source: + git: + repo: https://github.com/yuvipanda/test-repo-push + branch: master + ingress: + host: staging.2i2c.cloud + path: /textbook + githubAuth: + enabled: true + jupyterhub: custom: - docs_service: - enabled: true - repo: https://github.com/jupyterhub/nbgitpuller - branch: gh-pages 2i2c: add_staff_user_ids_to_admin_users: true add_staff_user_ids_of_type: "google" @@ -23,6 +35,15 @@ jupyterhub: name: 2i2c url: https://2i2c.org hub: + services: + dex: + url: http://dex:5556 + oauth_redirect_uri: https://staging.2i2c.cloud/services/dex/callback + oauth_no_confirm: true + display: false + oauth2-proxy: + url: http://dex:9000 + display: false config: Authenticator: allowed_users: &staging_users diff --git a/config/clusters/cloudbank/staging.values.yaml b/config/clusters/cloudbank/staging.values.yaml index 60ca46718b..02f8198cb1 100644 --- a/config/clusters/cloudbank/staging.values.yaml +++ b/config/clusters/cloudbank/staging.values.yaml @@ -1,9 +1,5 @@ jupyterhub: custom: - docs_service: - enabled: true - repo: https://github.com/jupyterhub/nbgitpuller - branch: gh-pages 2i2c: add_staff_user_ids_to_admin_users: true add_staff_user_ids_of_type: "google" diff --git a/docs/howto/customize/docs-service.md b/docs/howto/customize/docs-service.md deleted file mode 100644 index 96d4d2f13b..0000000000 --- a/docs/howto/customize/docs-service.md +++ /dev/null @@ -1,34 +0,0 @@ -# Connect static web content with the hub - -The 2i2c hubs can be configured to provide static web content as a [JupyterHub service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html), available -at `https:///services/docs`. This can be a great tool to provide hub-specific documentation right from inside the hub. - -```{figure} ../../images/docs-service.png -``` - -To enable the docs service service for a hub: - -1. Mark it as *enabled* by setting `jupyterhub.custom.docs_service.enabled` to *True* in the - appropriate file under `config/clusters/`. -2. Specify the GitHub repository where the static HTML files are hosted, by setting `jupyterhub.custom.docs_service.repo`. -3. Specify the GitHub branch of the respository where the static HTML files are hosted, by setting `jupyterhub.custom.docs_service.docs_service.branch`. - -Example config: - -```yaml - jupyterhub: - custom: - docs_service: - enabled: true - repo: https://github.com/ - branch: -``` - -```{note} - -Depending on what Static Site Generator has been used to generate the website's static content, it **may** or **may not** use relative paths routing by default. -For example, [Sphinx](https://www.sphinx-doc.org/en/master/) handles relative paths by default, whereas, [Hugo](https://gohugo.io/) leaves all [relative URLs unchanged](https://gohugo.io/content-management/urls/#relative-urls). - -However, having relative URLS is a **must** in order for the hub docs service to work. Please check with the docs of your SSG of choice and enable relative URLs if they -aren't enabled already. -``` diff --git a/docs/howto/features/index.md b/docs/howto/features/index.md index 958265541f..4c4683bfff 100644 --- a/docs/howto/features/index.md +++ b/docs/howto/features/index.md @@ -10,7 +10,6 @@ See the sections below for more details: cloud-access gpu github -../customize/docs-service -../customize/configure-login-page -../operate/override-domain.md +static-sites +login-page ``` diff --git a/docs/howto/customize/configure-login-page.md b/docs/howto/features/login-page.md similarity index 100% rename from docs/howto/customize/configure-login-page.md rename to docs/howto/features/login-page.md diff --git a/docs/howto/features/static-sites.md b/docs/howto/features/static-sites.md new file mode 100644 index 0000000000..2938ae33b3 --- /dev/null +++ b/docs/howto/features/static-sites.md @@ -0,0 +1,162 @@ +# Deploy authenticated static websites along the hub + +We can deploy *authenticated* static websites on the same domain as the hub +that is only accessible to users who have access to the hub. The source +for these come from git repositories that should contain rendered HTML, +and will be updated every 5 minutes. They can be under any prefix on the +same domain as the hub (such as `/docs`, `/textbook`, etc). + +You can enable this with the following config in the `.values.yaml` +file for your hub. + +```yaml + +dex: + # Enable authentication + enabled: true + hubHostName: + +staticWebsite: + enabled: true + source: + git: + repo: + branch: + ingress: + host: + path: + +jupyterhub: + hub: + services: + dex: + url: http://dex:5556 + oauth_redirect_url: https:///services/dex/callback + oauth_no_confirm: true + display: false + oauth2-proxy: + display: false + url: http://dex:9000 + +``` + +```{note} +We manually configure the hub services instead of autogenerating +them in our deploy scripts. This leads to some additional copy-pasting and +duplication, but keeps our config explicit and simple. +``` + +## Example + +Here's a sample that hosts the data8 textbook under `https://staging.2i2c.cloud/textbook`: + +```yaml +dex: + enabled: true + hubHostName: staging.2i2c.cloud + +staticWebsite: + enabled: true + source: + git: + repo: https://github.com/inferentialthinking/inferentialthinking.github.io + branch: master + ingress: + host: staging.2i2c.cloud + path: /textbook + +jupyterhub: + hub: + services: + dex: + url: http://dex:5556 + oauth_redirect_uri: https://staging.2i2c.cloud/services/dex/callback + oauth_no_confirm: true + oauth2-proxy: + url: http://dex:9000 +``` + +This clones the [repo]( https://github.com/inferentialthinking/inferentialthinking.github.io), +checks out the `master` branch and keeps it up to date by doing a +`git pull` every 5 minutes. It is made available under `/textbook`, +and requires users be logged-in to the hub before they can access it. + +## Using private GitHub repos + +We use [git-credentials-helper](https://github.com/yuvipanda/git-credential-helpers) +to support pulling content from private repos. + +### Setup GitHub app + +`git-credentials-helper` uses a [GitHub App](https://docs.github.com/en/developers/apps) +to pull private repos. So you first need to create a GitHub app for each hub that wants +to pull private repos as static content. + +1. Create a [GitHub app in the 2i2c org](https://github.com/organizations/2i2c-org/settings/apps/new). + +2. Give it a descriptive name (such as ' static site deploy + authenticator') and description, as users will see this when authorizing + access to their private repos. + +3. Disable webhooks (uncheck the 'Active' checkbox under 'Webhooks'). All other + textboxes can be left empty. + +4. Under 'Repository permissions', select 'Read' for 'Contents'. + +5. Under 'Where can this GitHub App be installed?', select 'Any account'. This will + enable users to push to their own user repositories or other organization repositories, + rather than just the 2i2c repos. + +6. Create the application with the 'Create GitHub app' button. + +7. Copy the numeric 'App id' from the app info page you should be redirected to. + +8. Create a new private key for authentication use with the `Generate a private key` + button. This should download a private key file, that you should keep safe. + +### Helm values configuration + +Now, we can configure our static files server to make use of the GitHub app to authenticate. + +1. Enable the gitHub app in the `.values.yaml` file for the hub. + + ```yaml + staticWebsite: + gitHubAuth: + enabled: true + ``` + +2. Create a sops-encrypted file (usually in the form of + `enc-.secret.values.yaml`) to hold the secret values required to authenticate + the GitHub app. + + ```yaml + staticWebsite: + githubAuth: + githubApp: + id: + privateKey: | + + ``` + + Make sure this file is also listed under `helm_chart_values_files` for the hub in + the cluster's `cluster.yaml` so it is read during deployment. + +### Grant access to the private repo + +Finally, someone with admin rights on the private repo to be pulled needs to +grant the github app we just setup access to the private repo. **This is the only +part that hub admins rather than 2i2c engineers need to do**. + +1. Go to the 'Public page' of the GitHub app created. This usually is of the + form `https://github.com/apps/`. You can find this in the information + page of the app after you create it, under 'Public link' + +2. Install the app in the organization the private repo is in, and grant it access + *only* to the repo that needs to be pulled. + +### Do a deploy + +After all the permissions are setup, you should make sure the config under +`staticWebsite.source.git.repo` and `staticWebsite.source.git.repo` are set appropriately, and do a deployment +to pull in the private repo! \ No newline at end of file diff --git a/docs/howto/k8s/node-administration.md b/docs/howto/k8s/node-administration.md index 6087468607..446e52fa73 100644 --- a/docs/howto/k8s/node-administration.md +++ b/docs/howto/k8s/node-administration.md @@ -34,7 +34,7 @@ Sometimes you might need to delete or to perform maintenance on a node in the cl * Pods created by [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/) objects, through [Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) Once deleted, these Pods will get re-created and assigned to a different node (because the current node it's cordoned). - The [hub, proxy](https://github.com/2i2c-org/infrastructure/tree/HEAD/helm-charts/basehub/Chart.yaml) and the [docs service](https://github.com/2i2c-org/infrastructure/tree/HEAD/helm-charts/basehub/templates/docs-service-deployment.yaml) are some examples of such pods. + The [hub & proxy](https://github.com/2i2c-org/infrastructure/tree/HEAD/helm-charts/basehub/Chart.yaml) pods are some examples of such pods. * Pods created by [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) objects. These run on every node, regardless of their `cordon` status. So, they don't need to be deleted, otherwise, they will be re-created on the same cordoned node. diff --git a/docs/images/docs-service.png b/docs/images/docs-service.png deleted file mode 100644 index d5b6ec49cc..0000000000 Binary files a/docs/images/docs-service.png and /dev/null differ diff --git a/docs/topic/features.md b/docs/topic/features.md index 42f488b331..47dcf77482 100644 --- a/docs/topic/features.md +++ b/docs/topic/features.md @@ -48,4 +48,5 @@ A single bucket can also be designated as as *scratch bucket*, which will set a `SCRATCH_BUCKET` (and a deprecated `PANGEO_SCRATCH`) environment variable of the form `:///`. This can be used by individual users to store objects temporarily for their own use, although there is nothing -preventing other users from accessing these objects! \ No newline at end of file +preventing other users from accessing these objects! + diff --git a/helm-charts/basehub/templates/dex/_helpers.tpl b/helm-charts/basehub/templates/dex/_helpers.tpl new file mode 100644 index 0000000000..b44872ea82 --- /dev/null +++ b/helm-charts/basehub/templates/dex/_helpers.tpl @@ -0,0 +1,9 @@ +# Until https://github.com/Masterminds/sprig/issues/282 is fixed +{{- define "randHex" -}} + {{- $result := "" }} + {{- range $i := until . }} + {{- $rand_hex_char := mod (randNumeric 4 | atoi) 16 | printf "%x" }} + {{- $result = print $result $rand_hex_char }} + {{- end }} + {{- $result }} +{{- end }} diff --git a/helm-charts/basehub/templates/dex/configmap.yaml b/helm-charts/basehub/templates/dex/configmap.yaml new file mode 100644 index 0000000000..a46ba58685 --- /dev/null +++ b/helm-charts/basehub/templates/dex/configmap.yaml @@ -0,0 +1,67 @@ +{{- if .Values.dex.enabled -}} +kind: ConfigMap +apiVersion: v1 +metadata: + name: dex + labels: + app: dex +data: + dex.yaml: | + issuer: https://{{ .Values.dex.hubHostName }}/services/dex + storage: + type: sqlite3 + config: + # /srv/db is a PVC mounted for persistence + file: /srv/db/dex.sqlite + + web: + # Listen on all interfaces, so this is publicly visible + http: 0.0.0.0:5556 + + oauth2: + # Don't explicitly require users to grant access via the + # dex interface, for a smoother experience + skipApprovalScreen: true + + connectors: + - type: oauth + id: hub + name: hub + config: + clientID: service-dex + # Env vars are expanded via gomplate, which is present in the + # upstream dex docker image + clientSecret: {{ "{{" }} .Env.HUB_OAUTH2_CLIENT_SECRET {{ "}}" }} + redirectURI: https://{{ .Values.dex.hubHostName }}/services/dex/callback + userIDKey: name + tokenURL: http://proxy-public/hub/api/oauth2/token + authorizationURL: https://{{ .Values.dex.hubHostName }}/hub/api/oauth2/authorize + userInfoURL: http://proxy-public/hub/api/user + + staticClients: + - id: oauth2-proxy + redirectURIs: + - https://{{ .Values.dex.hubHostName }}/services/oauth2-proxy/oauth2/callback + name: oauth2-proxy + # Env vars are expanded via gomplate, which is present in the + # upstream dex docker image + secret: {{ "{{" }} .Env.OAUTH2_PROXY_CLIENT_SECRET {{ "}}" }} + oauth2-proxy.cfg: | + provider = "oidc" + # This is hardcoded in the dex config + client_id = "oauth2-proxy" + redirect_url = "https://{{ .Values.dex.hubHostName }}/services/oauth2-proxy/oauth2/callback" + oidc_issuer_url = "https://{{ .Values.dex.hubHostName }}/services/dex" + oidc_email_claim = "sub" + # We don't actually use email for anything here, so skip email verification + insecure_oidc_allow_unverified_email = true + email_domains = "*" + # Listen on port 9000 + http_address = "http://0.0.0.0:9000" + # Don't require user interaction to log in - treat this more like SSO + skip_provider_button = true + # This is exposed to the internet as a JupyterHub service, + # so it is only available prefixed with this URL + reverse_proxy = true + proxy_prefix = "/services/oauth2-proxy/oauth2" +{{- end }} diff --git a/helm-charts/basehub/templates/dex/deployment.yaml b/helm-charts/basehub/templates/dex/deployment.yaml new file mode 100644 index 0000000000..cb5b435224 --- /dev/null +++ b/helm-charts/basehub/templates/dex/deployment.yaml @@ -0,0 +1,82 @@ +{{- if .Values.dex.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dex +spec: + replicas: 1 + selector: + matchLabels: + app: dex + template: + metadata: + labels: + app: dex + annotations: + checksum/config: {{ include (print $.Template.BasePath "/dex/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/dex/secret.yaml") . | sha256sum }} + spec: + volumes: + - name: db + persistentVolumeClaim: + claimName: dex + - name: config + configMap: + name: dex + securityContext: + # The upstream repo runs with gid 0, and setting this makes + # sure the db volume we mount can be written to by the dex process + fsGroup: 0 + containers: + - name: dex + image: ghcr.io/dexidp/dex:v2.32.0 + ports: + - name: dex + containerPort: 5556 + env: + # These are expanded by the dex config + - name: HUB_OAUTH2_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: hub + key: hub.services.dex.apiToken + - name: OAUTH2_PROXY_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: dex + key: oauth2Proxy.clientSecret + volumeMounts: + - name: config + mountPath: /srv/config + - name: db + mountPath: /srv/db + # Needs to be args, not cmd - this allows gomplate based + # expansion of config file + args: + - dex + - serve + - /srv/config/dex.yaml + - name: oauth2-proxy + image: quay.io/oauth2-proxy/oauth2-proxy:v7.3.0 + command: + - oauth2-proxy + - --config=/srv/config/oauth2-proxy.cfg + volumeMounts: + - name: config + mountPath: /srv/config + ports: + - name: oauth2-proxy + containerPort: 9000 + env: + # This is read by oauth2-proxy + - name: OAUTH2_PROXY_COOKIE_SECRET + valueFrom: + secretKeyRef: + name: dex + key: oauth2Proxy.cookieSecret + - name: OAUTH2_PROXY_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: dex + key: oauth2Proxy.clientSecret +{{- end }} diff --git a/helm-charts/basehub/templates/dex/pvc.yaml b/helm-charts/basehub/templates/dex/pvc.yaml new file mode 100644 index 0000000000..56e6aafd6d --- /dev/null +++ b/helm-charts/basehub/templates/dex/pvc.yaml @@ -0,0 +1,12 @@ +{{ if .Values.dex.enabled -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: dex +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +{{- end }} diff --git a/helm-charts/basehub/templates/dex/secret.yaml b/helm-charts/basehub/templates/dex/secret.yaml new file mode 100644 index 0000000000..74d0bec2d4 --- /dev/null +++ b/helm-charts/basehub/templates/dex/secret.yaml @@ -0,0 +1,21 @@ +{{- if .Values.dex.enabled -}} +apiVersion: v1 +kind: Secret +metadata: + name: dex +type: Opaque +stringData: + {{- $k8sState := lookup "v1" "Secret" .Release.Namespace "dex" | default (dict "data" (dict)) }} + + {{- if hasKey $k8sState.data "oauth2Proxy.clientSecret" }} + oauth2Proxy.clientSecret: {{ index $k8sState.data "oauth2Proxy.clientSecret" | b64dec }} + {{- else }} + oauth2Proxy.clientSecret: {{ include "randHex" 64 }} + {{- end }} + + {{- if hasKey $k8sState.data "oauth2Proxy.cookieSecret" }} + oauth2Proxy.cookieSecret: {{ index $k8sState.data "oauth2Proxy.cookieSecret" | b64dec }} + {{- else }} + oauth2Proxy.cookieSecret: {{ include "randHex" 16 }} + {{- end }} +{{- end }} diff --git a/helm-charts/basehub/templates/dex/service.yaml b/helm-charts/basehub/templates/dex/service.yaml new file mode 100644 index 0000000000..b9ea04526e --- /dev/null +++ b/helm-charts/basehub/templates/dex/service.yaml @@ -0,0 +1,19 @@ +{{- if .Values.dex.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: dex + labels: + app: dex +spec: + type: ClusterIP + ports: + - name: dex + port: 5556 + targetPort: dex + - name: oauth2-proxy + port: 9000 + targetPort: oauth2-proxy + selector: + app: dex +{{- end }} diff --git a/helm-charts/basehub/templates/docs-service-config.yaml b/helm-charts/basehub/templates/docs-service-config.yaml deleted file mode 100644 index 498a40a1a3..0000000000 --- a/helm-charts/basehub/templates/docs-service-config.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if .Values.jupyterhub.custom.docs_service.enabled -}} -kind: ConfigMap -apiVersion: v1 -metadata: - name: nginx-docs-configmap - labels: - app: docs-service -data: - nginx.conf: | - server { - listen 8080; - location / { - index index.html; - root /etc/nginx/html; - } - } -{{- end }} diff --git a/helm-charts/basehub/templates/docs-service-deployment.yaml b/helm-charts/basehub/templates/docs-service-deployment.yaml deleted file mode 100644 index 02b5a5441e..0000000000 --- a/helm-charts/basehub/templates/docs-service-deployment.yaml +++ /dev/null @@ -1,69 +0,0 @@ -{{- if .Values.jupyterhub.custom.docs_service.enabled -}} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: docs-service -spec: - replicas: 1 - selector: - matchLabels: - app: docs-service - template: - metadata: - labels: - app: docs-service - spec: - volumes: - - name: nginx-config - configMap: - name: nginx-docs-configmap - - name: docs - emptyDir: {} - initContainers: - - name: docs-clone - image: alpine/git - args: - - clone - - --depth=1 - - --branch={{ .Values.jupyterhub.custom.docs_service.branch | required "jupyterhub.custom.docs_service.branch is required with jupyterhub.custom.docs_service.enabled set to true" }} - - --single-branch - - -- - - '{{ .Values.jupyterhub.custom.docs_service.repo | required "jupyterhub.custom.docs_service.repo is required with jupyterhub.custom.docs_service.enabled set to true" }}' - - /srv/docs - securityContext: - runAsUser: 1000 - allowPrivilegeEscalation: False - readOnlyRootFilesystem: True - volumeMounts: - - name: docs - mountPath: /srv/docs - containers: - - name: docs-sync - image: alpine/git - workingDir: /srv/docs - command: - - /bin/sh - args: - - -c - - "while true; do git fetch origin; git reset --hard origin/master; sleep\ - \ 5m; done" - securityContext: - runAsUser: 1000 - allowPrivilegeEscalation: False - readOnlyRootFilesystem: True - volumeMounts: - - name: docs - mountPath: /srv/docs - - name: nginx-docs-service - image: nginx:1.19 - command: ["/usr/sbin/nginx", "-g", "daemon off;"] - ports: - - name: nginx-port - containerPort: 8080 - volumeMounts: - - name: nginx-config - mountPath: /etc/nginx/conf.d/default.conf - subPath: nginx.conf - - name: docs - mountPath: /etc/nginx/html/services/docs -{{- end }} diff --git a/helm-charts/basehub/templates/nginx-docs-service.yaml b/helm-charts/basehub/templates/nginx-docs-service.yaml deleted file mode 100644 index 8f9e622591..0000000000 --- a/helm-charts/basehub/templates/nginx-docs-service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -{{- if .Values.jupyterhub.custom.docs_service.enabled -}} -apiVersion: v1 -kind: Service -metadata: - name: docs-service - labels: - app: docs-service -spec: - type: ClusterIP - ports: - - name: http - port: 80 - targetPort: nginx-port - selector: - app: docs-service -{{- end }} diff --git a/helm-charts/basehub/templates/static/configmap.yaml b/helm-charts/basehub/templates/static/configmap.yaml new file mode 100644 index 0000000000..d8173db54e --- /dev/null +++ b/helm-charts/basehub/templates/static/configmap.yaml @@ -0,0 +1,17 @@ +{{- if .Values.staticWebsite.enabled -}} +kind: ConfigMap +apiVersion: v1 +metadata: + name: static-sites + labels: + app: static-sites +data: + nginx.conf: | + server { + listen 8080; + location {{ .Values.staticWebsite.ingress.path }} { + index index.html; + alias /srv/content/repo; + } + } +{{- end }} diff --git a/helm-charts/basehub/templates/static/deployment.yaml b/helm-charts/basehub/templates/static/deployment.yaml new file mode 100644 index 0000000000..4fe8389cac --- /dev/null +++ b/helm-charts/basehub/templates/static/deployment.yaml @@ -0,0 +1,97 @@ +{{- if .Values.staticWebsite.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: static-sites +spec: + replicas: 1 + selector: + matchLabels: + app: static-sites + template: + metadata: + labels: + app: static-sites + annotations: + checksum/config: {{ include (print $.Template.BasePath "/static/configmap.yaml") . | sha256sum }} + spec: + volumes: + - name: config + configMap: + name: static-sites + - name: content + emptyDir: {} + {{- if .Values.staticWebsite.githubAuth.enabled }} + - name: git-config + secret: + secretName: static-sites + {{- end }} + initContainers: + - name: content-clone + image: quay.io/yuvipanda/git-credential-helpers:0.2 + command: + - git + - clone + - --depth=1 + - --branch={{ .Values.staticWebsite.source.git.branch | required "staticWebsite.source.git.branch is required with staticSite.enabled set to true" }} + - --single-branch + - -- + - '{{ .Values.staticWebsite.source.git.repo | required "staticWebsite.source.git.repo is required with staticWebsite.enabled set to true" }}' + - /srv/content/repo + securityContext: + runAsUser: 1000 + allowPrivilegeEscalation: False + readOnlyRootFilesystem: True + volumeMounts: + - name: content + mountPath: /srv/content + {{- if .Values.staticWebsite.githubAuth.enabled }} + - name: git-config + mountPath: /etc/gitconfig + subPath: gitconfig + readOnly: true + - name: git-config + mountPath: /etc/github/github-app-private-key.pem + subPath: github-app-private-key.pem + readOnly: true + {{- end }} + containers: + - name: content-sync + image: quay.io/yuvipanda/git-credential-helpers:0.2 + workingDir: /srv/content + command: + - /bin/sh + args: + - -c + - "while true; do git fetch origin; git reset --hard origin/{{ .Values.staticWebsite.source.git.branch }}; sleep\ + \ 5m; done" + securityContext: + runAsUser: 1000 + allowPrivilegeEscalation: False + readOnlyRootFilesystem: True + volumeMounts: + - name: content + mountPath: /srv/content + {{- if .Values.staticWebsite.githubAuth.enabled }} + - name: git-config + mountPath: /etc/gitconfig + subPath: gitconfig + readOnly: true + - name: git-config + mountPath: /etc/github/github-app-private-key.pem + subPath: github-app-private-key.pem + readOnly: true + {{- end }} + - name: server + image: nginx:1.19 + command: ["/usr/sbin/nginx", "-g", "daemon off;"] + ports: + - name: nginx + containerPort: 8080 + volumeMounts: + - name: config + mountPath: /etc/nginx/conf.d/default.conf + subPath: nginx.conf + - name: content + mountPath: /srv/content +{{- end }} diff --git a/helm-charts/basehub/templates/static/ingress.yaml b/helm-charts/basehub/templates/static/ingress.yaml new file mode 100644 index 0000000000..8125b9dea3 --- /dev/null +++ b/helm-charts/basehub/templates/static/ingress.yaml @@ -0,0 +1,24 @@ +{{- if .Values.staticWebsite.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + # Authenticate with oauth2-proxy so only hub logged-in users see this + # https://kubernetes.github.io/ingress-nginx/examples/auth/oauth-external-auth/ + nginx.ingress.kubernetes.io/auth-url: "http://dex.{{ .Release.Namespace }}.svc.cluster.local:9000/services/oauth2-proxy/oauth2/auth" + nginx.ingress.kubernetes.io/auth-signin: "https://$host/services/oauth2-proxy/oauth2/start?rd=$escaped_request_uri" + name: static-sites +spec: + ingressClassName: nginx + rules: + - host: {{ .Values.staticWebsite.ingress.host }} + http: + paths: + - path: {{ .Values.staticWebsite.ingress.path }} + pathType: Prefix + backend: + service: + name: static-sites + port: + number: 80 +{{- end }} diff --git a/helm-charts/basehub/templates/static/secret.yaml b/helm-charts/basehub/templates/static/secret.yaml new file mode 100644 index 0000000000..9e64bd8b78 --- /dev/null +++ b/helm-charts/basehub/templates/static/secret.yaml @@ -0,0 +1,16 @@ +{{- if .Values.staticWebsite.enabled -}} +{{- if .Values.staticWebsite.githubAuth.enabled -}} +apiVersion: v1 +kind: Secret +metadata: + name: static-sites +type: Opaque +stringData: + gitconfig: | + [credential "https://github.com"] + helper = !git-credential-github-app --app-key-file /etc/github/github-app-private-key.pem --app-id {{ .Values.staticWebsite.githubAuth.githubApp.id }} + useHttpPath = true + github-app-private-key.pem: | + {{ .Values.staticWebsite.githubAuth.githubApp.privateKey | nindent 4 }} +{{- end }} +{{- end }} diff --git a/helm-charts/basehub/templates/static/service.yaml b/helm-charts/basehub/templates/static/service.yaml new file mode 100644 index 0000000000..a181240475 --- /dev/null +++ b/helm-charts/basehub/templates/static/service.yaml @@ -0,0 +1,16 @@ +{{- if .Values.staticWebsite.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: static-sites + labels: + app: static-sites +spec: + type: ClusterIP + ports: + - name: http + port: 80 + targetPort: nginx + selector: + app: static-sites +{{- end }} diff --git a/helm-charts/basehub/values.schema.yaml b/helm-charts/basehub/values.schema.yaml index 987fb29e18..837422a7f4 100644 --- a/helm-charts/basehub/values.schema.yaml +++ b/helm-charts/basehub/values.schema.yaml @@ -17,7 +17,97 @@ required: - global - jupyterhub - userServiceAccount + - dex + - staticWebsite properties: + staticWebsite: + type: object + additionalProperties: false + required: + - enabled + properties: + enabled: + type: boolean + description: | + Enable hosting static sites associated with this hub. + source: + type: object + additionalProperties: false + description: | + Source of the static files to serve + properties: + git: + type: object + additionalProperties: false + description: | + Config of git repository to pull from + properties: + repo: + type: string + description: | + Git repo to clone and serve statically + branch: + type: string + description: | + Branch in given git repo to check out after cloning the repo + githubAuth: + type: object + additionalProperties: false + description: | + Enable using a GitHub app to authenticate the cloner, + so private repositories can be cloned. Uses + https://github.com/yuvipanda/git-credential-helpers + properties: + enabled: + type: boolean + description: | + Enable the github app integration + githubApp: + type: object + additionalProperties: false + description: | + Configuration of the github app to use for authentication + properties: + id: + type: integer + description: | + Integer id of GitHub app to use when cloning private repos + privateKey: + type: string + description: | + Private RSA key created to authenticate as this GitHuba pp + ingress: + type: object + additionalProperties: false + description: | + Configuration for the ingress that gets traffic into the static site + properties: + host: + type: string + description: | + DNS host name of the JupyterHub. + + Must match what the JupyterHub and dex are set up with. + path: + type: string + description: | + Absolute path under which the static sites should be available + dex: + type: object + additionalProperties: false + required: + - enabled + properties: + enabled: + type: boolean + description: | + Enable dex to provide OIDC + hubHostName: + type: string + description: | + Publicly accessible domain name of the hub. + + Used to construct URLs. userServiceAccount: type: object additionalProperties: false @@ -143,7 +233,6 @@ properties: required: - singleuserAdmin - cloudResources - - docs_service - 2i2c properties: homepage: @@ -251,20 +340,6 @@ properties: properties: enabled: type: boolean - docs_service: - type: object - additionalProperties: false - required: - - enabled - - repo - - branch - properties: - enabled: - type: boolean - repo: - type: string - branch: - type: string 2i2c: type: object additionalProperties: false diff --git a/helm-charts/basehub/values.yaml b/helm-charts/basehub/values.yaml index f7024a14e4..47e3bf1ed0 100644 --- a/helm-charts/basehub/values.yaml +++ b/helm-charts/basehub/values.yaml @@ -5,6 +5,23 @@ userServiceAccount: enabled: true annotations: {} +dex: + enabled: false + +staticWebsite: + enabled: false + source: + git: + branch: main + githubAuth: + enabled: false + githubApp: + # Primarily here for validation to 'work', + # as these are set in secret config otherwise. I don't like this, + # as we won't catch these values missing if they aren't set. + id: 0 + privateKey: "" + azureFile: enabled: false pv: @@ -55,10 +72,6 @@ jupyterhub: projectId: "" scratchBucket: enabled: false - docs_service: - enabled: false - repo: "" - branch: "" 2i2c: # Should 2i2c engineering staff user IDs be injected to the admin_users # configuration of the JupyterHub's authenticator by our custom @@ -452,12 +465,7 @@ jupyterhub: if authenticator_class == "github" and c.Authenticator.allowed_users: print("WARNING: hub.config.JupyterHub.authenticator_class was set to github and c.Authenticator.allowed_users was set, custom 2i2c jupyterhub config is now resetting allowed_users to an empty set.") c.Authenticator.allowed_users = set() - 05-add-docs-service-if-enabled: | - from z2jh import get_config - - if get_config("custom.docs_service.enabled"): - c.JupyterHub.services.append({"name": "docs", "url": "http://docs-service"}) - 06-gh-teams: | + 05-gh-teams: | from textwrap import dedent from tornado import gen, web from oauthenticator.github import GitHubOAuthenticator