Jupyterhub stack (#21)
Signed-off-by: omrishiv <327609+omrishiv@users.noreply.github.com>
This commit is contained in:
parent
7e0474b3bb
commit
8a38e3c94b
5 changed files with 250 additions and 0 deletions
17
jupyterhub/README.md
Normal file
17
jupyterhub/README.md
Normal file
|
@ -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
|
54
jupyterhub/jupyterhub.yaml
Normal file
54
jupyterhub/jupyterhub.yaml
Normal file
|
@ -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
|
127
jupyterhub/jupyterhub/manifests/jupyterhub-config.yaml
Normal file
127
jupyterhub/jupyterhub/manifests/jupyterhub-config.yaml
Normal file
|
@ -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)'"
|
||||||
|
}]'
|
|
@ -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
|
32
jupyterhub/jupyterhub/manifests/jupyterhub-ingress.yaml
Normal file
32
jupyterhub/jupyterhub/manifests/jupyterhub-ingress.yaml
Normal file
|
@ -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
|
Loading…
Reference in a new issue