diff --git a/jupyterhub/README.md b/jupyterhub/README.md new file mode 100644 index 0000000..b1bc25f --- /dev/null +++ b/jupyterhub/README.md @@ -0,0 +1,17 @@ +# Jupyterhub Stack + +This directory contains a Jupyterhub deployment that's integrated with Keycloak + +## Caveats +1) Reliance on `ref-implementation` for SSO + - This is possible to work around by setting `authenticator_class` in the `jupyterhub.yaml` to `dummy`. + +## Components +- Jupyterhub + +## Installation +Note: The stack is configured to use Keycloak for SSO; therefore, the ref-implementation is required for this to work. + +`idpbuilder create --use-path-routing -p https://github.com/cnoe-io/stacks//ref-implementation -p https://github.com/cnoe-io/stacks//jupyterhub` + +A `jupyterhub-config` job will be deployed into the keycloak namespace to create/patch some of the keycloak components. If deployed at the same time as the `ref-implementation`, this job will fail until the `config` job succeeds. This is normal diff --git a/jupyterhub/jupyterhub.yaml b/jupyterhub/jupyterhub.yaml new file mode 100644 index 0000000..4ae67c3 --- /dev/null +++ b/jupyterhub/jupyterhub.yaml @@ -0,0 +1,54 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: jupyterhub + namespace: argocd + labels: + env: dev + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + sources: + - repoURL: 'https://jupyterhub.github.io/helm-chart/' + targetRevision: 3.3.7 + helm: + releaseName: jupyterhub + values: | + hub: + baseUrl: /jupyterhub + extraEnv: + - name: OAUTH_TLS_VERIFY # for getting around self signed certificate issue + value: "0" + - name: OAUTH_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: jupyterhub-oidc + key: JUPYTERHUB_OAUTH_CLIENT_SECRET + config: + GenericOAuthenticator: + oauth_callback_url: https://cnoe.localtest.me:8443/jupyterhub/hub/oauth_callback + client_id: jupyterhub + authorize_url: https://cnoe.localtest.me:8443/keycloak/realms/cnoe/protocol/openid-connect/auth + token_url: https://cnoe.localtest.me:8443/keycloak/realms/cnoe/protocol/openid-connect/token + userdata_url: https://cnoe.localtest.me:8443/keycloak/realms/cnoe/protocol/openid-connect/userinfo + scope: + - openid + - profile + username_key: "preferred_username" + login_service: "keycloak" + allow_all: true # Allows all oauth authenticated users to use Jupyterhub. For finer grained control, you can use `allowed_users`: https://jupyterhub.readthedocs.io/en/stable/tutorial/getting-started/authenticators-users-basics.html#deciding-who-is-allowed + JupyterHub: + authenticator_class: generic-oauth + chart: jupyterhub + - repoURL: cnoe://jupyterhub + targetRevision: HEAD + path: "manifests" + destination: + server: "https://kubernetes.default.svc" + namespace: jupyterhub + syncPolicy: + syncOptions: + - CreateNamespace=true + automated: + selfHeal: true diff --git a/jupyterhub/jupyterhub/manifests/jupyterhub-config.yaml b/jupyterhub/jupyterhub/manifests/jupyterhub-config.yaml new file mode 100644 index 0000000..1a3b330 --- /dev/null +++ b/jupyterhub/jupyterhub/manifests/jupyterhub-config.yaml @@ -0,0 +1,127 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: jupyterhub-config-job + namespace: keycloak +data: + jupyterhub-client-payload.json: | + { + "protocol": "openid-connect", + "clientId": "jupyterhub", + "name": "Jupyterhub Client", + "description": "Used for Jupyterhub SSO", + "publicClient": false, + "authorizationServicesEnabled": false, + "serviceAccountsEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "standardFlowEnabled": true, + "frontchannelLogout": true, + "attributes": { + "saml_idp_initiated_sso_url_name": "", + "oauth2.device.authorization.grant.enabled": false, + "oidc.ciba.grant.enabled": false + }, + "alwaysDisplayInConsole": false, + "rootUrl": "", + "baseUrl": "", + "redirectUris": [ + "https://cnoe.localtest.me:8443/jupyterhub/hub/oauth_callback" + ], + "webOrigins": [ + "/*" + ] + } +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: jupyterhub-config + namespace: keycloak +spec: + template: + metadata: + generateName: jupyterhub-config + spec: + serviceAccountName: keycloak-config + restartPolicy: Never + volumes: + - name: keycloak-config + secret: + secretName: keycloak-config + - name: config-payloads + configMap: + name: jupyterhub-config-job + containers: + - name: kubectl + image: docker.io/library/ubuntu:22.04 + volumeMounts: + - name: keycloak-config + readOnly: true + mountPath: "/var/secrets/" + - name: config-payloads + readOnly: true + mountPath: "/var/config/" + command: ["/bin/bash", "-c"] + args: + - | + #! /bin/bash + set -ex -o pipefail + apt -qq update && apt -qq install curl jq gettext-base -y + + curl -sS -LO "https://dl.k8s.io/release/v1.28.3//bin/linux/amd64/kubectl" + chmod +x kubectl + + echo "checking if we're ready to start" + set +e + ./kubectl get secret -n keycloak keycloak-clients &> /dev/null + if [ $? -ne 0 ]; then + exit 1 + fi + set -e + + ADMIN_PASSWORD=$(cat /var/secrets/KEYCLOAK_ADMIN_PASSWORD) + KEYCLOAK_URL=http://keycloak.keycloak.svc.cluster.local:8080/keycloak + KEYCLOAK_TOKEN=$(curl -sS --fail-with-body -X POST -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "username=cnoe-admin" \ + --data-urlencode "password=${ADMIN_PASSWORD}" \ + --data-urlencode "grant_type=password" \ + --data-urlencode "client_id=admin-cli" \ + ${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token | jq -e -r '.access_token') + + set +e + + curl --fail-with-body -H "Authorization: bearer ${KEYCLOAK_TOKEN}" "${KEYCLOAK_URL}/admin/realms/cnoe" &> /dev/null + if [ $? -ne 0 ]; then + exit 0 + fi + set -e + + echo "creating Jupyterhub client" + curl -sS -H "Content-Type: application/json" \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X POST --data @/var/config/jupyterhub-client-payload.json \ + ${KEYCLOAK_URL}/admin/realms/cnoe/clients + + CLIENT_ID=$(curl -sS -H "Content-Type: application/json" \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X GET ${KEYCLOAK_URL}/admin/realms/cnoe/clients | jq -e -r '.[] | select(.clientId == "jupyterhub") | .id') + + CLIENT_SCOPE_GROUPS_ID=$(curl -sS -H "Content-Type: application/json" -H "Authorization: bearer ${KEYCLOAK_TOKEN}" -X GET ${KEYCLOAK_URL}/admin/realms/cnoe/client-scopes | jq -e -r '.[] | select(.name == "groups") | .id') + curl -sS -H "Content-Type: application/json" -H "Authorization: bearer ${KEYCLOAK_TOKEN}" -X PUT ${KEYCLOAK_URL}/admin/realms/cnoe/clients/${CLIENT_ID}/default-client-scopes/${CLIENT_SCOPE_GROUPS_ID} + + JUPYTERHUB_CLIENT_SECRET=$(curl -sS -H "Content-Type: application/json" \ + -H "Authorization: bearer ${KEYCLOAK_TOKEN}" \ + -X GET ${KEYCLOAK_URL}/admin/realms/cnoe/clients/${CLIENT_ID} | jq -e -r '.secret') + + ./kubectl patch secret -n keycloak keycloak-clients --type=json \ + -p='[{ + "op" : "add" , + "path" : "/data/JUPYTERHUB_CLIENT_SECRET" , + "value" : "'$(echo -n "$JUPYTERHUB_CLIENT_SECRET" | base64 -w 0)'" + },{ + "op" : "add" , + "path" : "/data/JUPYTERHUB_CLIENT_ID" , + "value" : "'$(echo -n "jupyterhub" | base64 -w 0)'" + }]' diff --git a/jupyterhub/jupyterhub/manifests/jupyterhub-external-secrets.yaml b/jupyterhub/jupyterhub/manifests/jupyterhub-external-secrets.yaml new file mode 100644 index 0000000..a300333 --- /dev/null +++ b/jupyterhub/jupyterhub/manifests/jupyterhub-external-secrets.yaml @@ -0,0 +1,20 @@ +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: keycloak-oidc + namespace: jupyterhub +spec: + secretStoreRef: + name: keycloak + kind: ClusterSecretStore + target: + name: jupyterhub-oidc + data: + - secretKey: JUPYTERHUB_OAUTH_CLIENT_ID + remoteRef: + key: keycloak-clients + property: JUPYTERHUB_CLIENT_ID + - secretKey: JUPYTERHUB_OAUTH_CLIENT_SECRET + remoteRef: + key: keycloak-clients + property: JUPYTERHUB_CLIENT_SECRET diff --git a/jupyterhub/jupyterhub/manifests/jupyterhub-ingress.yaml b/jupyterhub/jupyterhub/manifests/jupyterhub-ingress.yaml new file mode 100644 index 0000000..94f39e3 --- /dev/null +++ b/jupyterhub/jupyterhub/manifests/jupyterhub-ingress.yaml @@ -0,0 +1,32 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: jupyterhub-ingress + namespace: jupyterhub + annotations: + nginx.ingress.kubernetes.io/backend-protocol: HTTP + nginx.ingress.kubernetes.io/rewrite-target: /jupyterhub/$2 + nginx.ingress.kubernetes.io/use-regex: 'true' +spec: + ingressClassName: nginx + rules: + - host: cnoe.localtest.me + http: + paths: + - path: /jupyterhub(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: proxy-public + port: + number: 80 + - host: localhost + http: + paths: + - path: /jupyterhub(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: proxy-public + port: + number: 80