commit 7c1c161cedd3b2569f4d30041e6bb7ac41c6245e
Author: DerGrumpf
Date: Tue Mar 24 10:59:50 2026 +0100
Init
diff --git a/.kubeconfig b/.kubeconfig
new file mode 100644
index 0000000..d57bb6f
--- /dev/null
+++ b/.kubeconfig
@@ -0,0 +1,18 @@
+apiVersion: v1
+clusters:
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJY1R6R0hSU3FrblF3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBek1qUXdNREExTXpOYUZ3MHpOakF6TWpFd01ERXdNek5hTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUN2aVFqN0Z0dVhpZlZRYURtNUNNYmxNYXFaMXJXVGZDMk5SSEJWcVN4M0YxS1IzWlpRZ1J3YjZ5eSsKbU5FLzRZV2ZOK1NJUVEvbG93Y2llMXZxLzl6QTFNaXY4enM0YlRQaU5sY0hRTm9iYkRiUHBUNkNVdXpvdjdqRgpqbXhiQ3BlNlhpSmlTVHQvT3h6Wlg3NzZiUVAvNkxUTEdtdm1xaWxTa0I4Rjl6eVlFRmJrbkVYQkp1UHBXY0pUCkdCUmpyMzJzS05RbE13c2thSFhnNmVsRzhVa1FFMDNQeUpITi8yazlNZWpVWmVwQll2VE1mKzZCMW56ZDdVWWoKKzA2Z3EzWUhlUDE3R0kxVjRzR0UwMDJ3a1RsS213UUNiV1p2NkZMRHl3M01hL0RWdGFjQ0pQMklVeFhHeCs3agowRE0wZXZMN2ZMUnNGbGtSNGl2U09VcmJIamlQQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJSNjE3TXZ0L01uQXR0aHZXZHZoTWxMcEs3MDZ6QVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQWczVmNNUVFYcQpxSXNhd1RxOXByM05oY282Zk83U3g0MkNncXZXZzFIMXYxR1hiNVMzaDlaaUN1cVF4MkUvT0lNSmdUdytNblY5ClNZMVJKZ1Qvc0RGQk5jQ3ZLM1ZPVmpCdmFmRUh3b3V4dTlGSzdTaityaWJ4STNod3dsNFVrSit6Ly93ZWl0L0IKMXRWSXE5Y1lLblhFck9sQmJ1KzZYR3phTmRzOVBPSGZwU2g3ZWlhTFVZemNDaXN3b2NQN0NVdE53bUViekVZNwpLRFZKclJSWEtTNTdtYnNXSlpnWVFEZGlDVzMvNkI4TUg1YTVoakdjempadjBNc2hsVmtuWWlNL3RrbEExdWxCCi9TM2ZXdnhuallicWlWcXJDbzRMSFhwV0tHbFdyY1ZtSFJVcnZ1ZjlYQ09ncEhyUnRVVHFOOFVUQy95MXN0eDAKZjAzcHd0aGZXSU8rCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
+ server: https://127.0.0.1:39209
+ name: kind-kupyter
+contexts:
+- context:
+ cluster: kind-kupyter
+ user: kind-kupyter
+ name: kind-kupyter
+current-context: kind-kupyter
+kind: Config
+users:
+- name: kind-kupyter
+ user:
+ client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJT0xIdlBuY3E1VWN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBek1qUXdNREExTXpOYUZ3MHlOekF6TWpRd01ERXdNek5hTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDdFRVMzgKYXFqTzVlbG5hclhRUWxDcEZiTDNHSnpVY0lhY2ROeHV5NzFZUitEZmc2bEdJK3puQlFVTlpYOXhpak5TbHlZNgpiQ00rQk9IR09vVGxsdUxhL1RXbWNaUnhYYVBCeVpHY3ZkWVJCNFNVT2JlZ1BwT0xjajFBaXdvbU9IQUVOYXNzClZUeFlEcXlOUnd5M1cwNGFVcWNMREMyVUtDU0hEUVk1dlphOWo4MXpkdjl0K0g5L2RZb1diM3RHOTVySlRndE0KUGhhcDZkNlpoVUVXSDl1aE5Ha2YreE01Zi9KaUVRNnovWmc1alBsdHVHcGhaSUR5QmhUcUJaZnNJTjN3UllreQpiRnp6SndMZTZVVmhRQlE2YjVjZjlHYXVndWVocmFLb0wxUThJVFJuR2JvR0ZEb3dUbkQwaGN1V0RqZi9QbnV2CnZIUWk4L0pqK3pKcDBWek5BZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkhyWHN5KzM4eWNDMjJHOQpaMitFeVV1a3J2VHJNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNXUHhIdDJpM0ZERHkyd3JOeDBJZkFLYUF3CldWMzVCNTcxblVNTWltNVFjalVMUEU1c0praEpra3VGNGRZTlU5amM4K0c1WHF6QnpyQlpBajg5dWI3UmM2MXAKbVpQekEvWHlUbjZBK1BBRFBBRlkxSE9FMmdtSmd4eFZjV08rRmtyRzBZN1pjemRQdzdJRjQySzFDYzVSMDU1TApENjNGMkhaWEQ5d3NYWC8zYS9ReWpuMlFFN0dXOWc4MTUya1lMeVYycGN3cVo5aTFRNlgyUjBzbXRiOEt4UFZDCkdhS2ZwKy9NVXdZOE40WHdOakIraVo5anVvWU1mR095TFBieGIzTThMblR3bkRxNHdRRjV5c24vVFJQWCtFNHAKbnFCblNXbDVSVzdERi9VVi9na0pIbDlxWTJydFlEZkkyWHJ5YzJKdVhUSTh3N1BjRi80QVlFQnQvQkZXCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
+ client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBclUxTi9HcW96dVhwWjJxMTBFSlFxUld5OXhpYzFIQ0duSFRjYnN1OVdFZmczNE9wClJpUHM1d1VGRFdWL2NZb3pVcGNtT213alBnVGh4anFFNVpiaTJ2MDFwbkdVY1YyandjbVJuTDNXRVFlRWxEbTMKb0Q2VGkzSTlRSXNLSmpod0JEV3JMRlU4V0E2c2pVY010MXRPR2xLbkN3d3RsQ2draHcwR09iMld2WS9OYzNiLwpiZmgvZjNXS0ZtOTdSdmVheVU0TFRENFdxZW5lbVlWQkZoL2JvVFJwSC9zVE9YL3lZaEVPcy8yWU9ZejViYmhxCllXU0E4Z1lVNmdXWDdDRGQ4RVdKTW14Yzh5Y0MzdWxGWVVBVU9tK1hIL1Jtcm9Mbm9hMmlxQzlVUENFMFp4bTYKQmhRNk1FNXc5SVhMbGc0My96NTdyN3gwSXZQeVkvc3lhZEZjelFJREFRQUJBb0lCQUFLSjMxcVROV1hTZUZqTApkMTVWbWxqZnVIOW1IT1gvdi9rS3ZTL2lUQ08rNmN4Y1lWNWxxRks2QUJqeUk2dkdHbnBiUEhRZW9XV0hMTWQ2CmsvZkkvZ20zSzlJRVYraFJOdFRmM3dJc3hiWDZKamNGb1dyM2Y3SExPcHAzYnU2Z1pRT3F4WmNncUlHaHRXVmMKWlJOS2d4cGZtNUxOMnQwUXVYaEErSlpmOEpWV1A3OHFsaUNFQ2ZEd3pCRVpRVHVoYmhwRGhvNjBxL0UzU3RQVQprTGMzVitONnFEK0szeTZpM0hWU3lldkpYWWg0RXJCNlY1RTVweU5wZU90d1BhbThVRG91ZWZpcFZJQkVWVHhvCm00NVNCanZSQnRRYWRxMGN5M0p0L3N3aEtJQUVpT0c2WE1icmc4bzFWSDRNNnJiWmlQdDBiODVOUVJNd0FuR2kKMUsrTU9RRUNnWUVBeWtWMFZlSmdObS8rSVVsblRscGgzOENKd3kydU5QVXBRSHBoQ3pjVUxkQXV0Ri8yNTgvTwpJeVBoMVB5bzNCOXBLVVdmYnRpVjJoZjhXUjV3UjdJakpLWDhzaHRqcEs3TkJvTDhrdnBRUVl0QS9CVS9KR2E0ClRKaWpsbGJNOWtXYzF2dEs5YStQV255U3hCb1psb0hYaE1lRzFMK2JpRE5FcFk4c2hTbzVkTU1DZ1lFQTIxWHIKNnRuelhSRGVtMzdRRmttOXpUL01tSmROQ3MxOVovU2kvNGVRTHpKNkM2RlJUMGNFTHpHVjA3SXdIYjhMZ1JPTwpUY2g3V3RXYngwa2o4L2RVcXNJWXpLYzRhT2hsVVg2aUNpZVZBakdETzg2aFVWT3FkTmVXaFZudDdyZE95cTgxCmtNdkdTTmV6ZlFTMnE0cU1GQVUwcGpVdDQwaiszWFdJdVdvRWp5OENnWUVBbmtzc2QrbnBFYkVqV0RseHQwZlUKUUo4Vk1NR1hDNnF3MWR6d0JTN2RnOXpnTUJqSnlUQS9TaERTc3pQbmtoeWkxOEc4dTZxVDIxSGFFb1JYcWtRbQpiSS9aNmlpMUdqUVNEMzZDMnlNNW01RzNFWkF2RWZXeFZZQSt4WEM0aGlLRVUxbmxsOUFFaC9QbGg4SkZOQnY0CjVkaWdFKzYvY1I5dUlZS2lmTFJHc3JFQ2dZQmZ3cXFtdFpPSURXWnpVekY4bWFOeGFpcGtjS0psVmdRcmorWmUKVkF5Q1hySmtRNEVoY0tzR0E4c2JTdyt3M1FranlLcjNrTkV5ZmxKdDlxUG96eEk3SDFUK2ZQK201ZGZlZGNBLwpXTHE0NDI4ZGZJQjM1bVJrY1ArNXB1SzN0M2FDRFc4QWtjYzNaRjFyOXRQZUh6WTdRMjZTSm1PcmVPSTFSQ3gyCmJ6QWdad0tCZ1FDNUlhVTdzWmlrVVlwbmZJYUZKK1NGTTN4b1lIWVpzNHYvN0t5OEhCM2VqVHlZbXl1QjBZNFYKUDhqZlg4SUxsN0o3a3dKNGpnd2kyWGZxckorOW9SRzZ0N0I5R3EyM3pUR29rSnJXSFRKeXJOcnpUalRRT1A2LwpTNERObWgrM1Mxd1MxTC9IY25VZk1uUmFnMEtraTFIQ0ExdFQzK3Q1a254dWxxYldOUlUyVUE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
diff --git a/Dockerfile.hub b/Dockerfile.hub
new file mode 100644
index 0000000..87f0210
--- /dev/null
+++ b/Dockerfile.hub
@@ -0,0 +1,10 @@
+FROM quay.io/jupyterhub/k8s-hub:4.3.2
+
+USER root
+RUN apt-get update && apt-get install -y nodejs npm && apt-get clean
+
+USER jovyan
+RUN pip install --upgrade pip
+RUN pip install git+https://github.com/jupyter/nbgrader.git@main ngshare_exchange
+
+COPY ./kupyter-notebook/nbgrader_config.py /etc/jupyter/nbgrader_config.py
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..172d7d3
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,89 @@
+hub:
+ image:
+ name: localhost/kupyter-hub
+ tag: latest
+ pullPolicy: Never
+
+ services:
+ ngshare:
+ url: http://ngshare.jupyterhub.svc.cluster.local:8080
+ apiToken: 822235aaaf83f0232c799fe948cbb1594b01d7d7b11af051871cc2d3fcb08fe8
+ ngshare-admin:
+ apiToken: setuptoken123456789012345678901234567890123456789012345678901234
+
+
+ db:
+ type: postgres
+ url: postgresql+psycopg2://jupyterhub:jupyterhub@jupyterhub-db-postgresql.jupyterhub.svc:5432/jupyterhub
+
+ config:
+ JupyterHub:
+ authenticator_class: nativeauthenticator.NativeAuthenticator
+ admin_users:
+ - admin
+ - instructor-Prog
+ - instructor-Sig
+
+ NativeAuthenticator:
+ open_signup: false
+ allowed_users:
+ - admin
+ - instructor-Prog
+ - instructor-Sig
+
+ extraVolumes:
+ - name: hub-logos
+ configMap:
+ name: hub-logos
+
+ extraVolumeMounts:
+ - name: hub-logos
+ mountPath: /usr/local/share/jupyterhub/static/images/jupyterhub-80.png
+ subPath: jupyterhub-80.png
+ - name: hub-logos
+ mountPath: /usr/local/share/jupyterhub/static/images/jupyterhub.svg
+ subPath: jupyterhub.svg
+
+ extraConfig:
+ ngshare.py: |
+ c.JupyterHub.services.append({
+ 'name': 'ngshare',
+ 'url': 'http://ngshare.jupyterhub.svc.cluster.local:8080',
+ 'api_token': '822235aaaf83f0232c799fe948cbb1594b01d7d7b11af051871cc2d3fcb08fe8',
+ 'oauth_no_confirm': True})
+ nbgrader: |
+ c.JupyterHub.load_groups = {
+ "formgrader-users": ["admin"]
+ }
+
+proxy:
+ secretToken: "8031b6dca309ff7259a10d1d38c70c4852d17d5bab02c31cbe7d76a3fb60cb66"
+
+prePuller:
+ hook:
+ enabled: false
+ continuous:
+ enabled: false
+
+cull:
+ enabled: true
+ timeout: 1800
+ every: 300
+
+singleuser:
+ defaultUrl: "/tree"
+ image:
+ name: localhost/kupyter-notebook
+ tag: latest
+ pullPolicy: Never
+ memory:
+ limit: 1G
+ guarantee: 256M
+ cpu:
+ limit: 1
+ guarantee: 0.1
+ cloudMetadata:
+ blockWithIptables: false
+ networkPolicy:
+ enabled: false
+
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..8b5984e
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1773734432,
+ "narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "cda48547b432e8d3b18b4180ba07473762ec8558",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..700bf1b
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,318 @@
+{
+ description = "kupyter – rootless Kubernetes dev shell";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+
+ outputs =
+ {
+ self,
+ nixpkgs,
+ flake-utils,
+ }:
+ flake-utils.lib.eachDefaultSystem (
+ system:
+ let
+ pkgs = import nixpkgs { inherit system; };
+
+ get-all = pkgs.writeShellApplication {
+ name = "get-all";
+ runtimeInputs = [ pkgs.kubectl ];
+ text = ''kubectl get pods -A'';
+ };
+
+ cluster-start = pkgs.writeShellApplication {
+ name = "cluster-start";
+ runtimeInputs = [
+ pkgs.kind
+ pkgs.kubectl
+ ];
+ text = ''
+ kind create cluster --name kupyter --wait 60s
+ kubectl config use-context kind-kupyter
+ echo "✓ Cluster ready"
+ kubectl cluster-info
+ '';
+ };
+
+ cluster-stop = pkgs.writeShellApplication {
+ name = "cluster-stop";
+ runtimeInputs = [ pkgs.kind ];
+ text = ''
+ kind delete cluster --name kupyter
+ rm -f .kubeconfig
+ echo "✓ Cluster deleted, kubeconfig cleared"
+ '';
+ };
+
+ tls-init = pkgs.writeShellApplication {
+ name = "tls-init";
+ runtimeInputs = [
+ pkgs.openssl
+ pkgs.kubectl
+ ];
+ text = ''
+ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
+ -keyout tls.key \
+ -out tls.crt \
+ -subj "/CN=jupyterhub.local/O=kupyter"
+
+ kubectl create namespace jupyterhub --dry-run=client -o yaml | kubectl apply -f -
+
+ kubectl create secret tls jupyterhub-tls \
+ --cert=tls.crt \
+ --key=tls.key \
+ --namespace jupyterhub
+ echo "✓ TLS secret stored in jupyterhub namespace"
+ '';
+ };
+
+ helm-repos = pkgs.writeShellApplication {
+ name = "helm-repos";
+ runtimeInputs = [ pkgs.kubernetes-helm ];
+ text = ''
+ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
+ helm repo add jupyterhub https://hub.jupyter.org/helm-chart/
+ helm repo add bitnami https://charts.bitnami.com/bitnami
+ helm repo add ngshare https://libretexts.github.io/ngshare-helm-repo/
+ helm repo add prometheus https://prometheus-community.github.io/helm-charts
+ helm repo update
+ echo "✓ all helm repos added and updated"
+ '';
+ };
+
+ ingress-install = pkgs.writeShellApplication {
+ name = "ingress-install";
+ runtimeInputs = [
+ pkgs.kubernetes-helm
+ pkgs.kubectl
+ ];
+ text = ''
+ helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
+ --namespace ingress-nginx \
+ --create-namespace \
+ --set controller.service.type=NodePort \
+ --set controller.service.nodePorts.http=30080 \
+ --set controller.service.nodePorts.https=30443
+ echo "✓ ingress-nginx installed"
+ kubectl get pods -n ingress-nginx
+ '';
+ };
+
+ jupyterhub-install = pkgs.writeShellApplication {
+ name = "jupyterhub-install";
+ runtimeInputs = [
+ pkgs.kubernetes-helm
+ pkgs.kubectl
+ ];
+ text = ''
+ kubectl create namespace jupyterhub --dry-run=client -o yaml | kubectl apply -f -
+
+ kubectl create configmap hub-logos \
+ --from-file=jupyterhub-80.png=./kupyter-notebook/logo.png \
+ --from-file=jupyterhub.svg=./kupyter-notebook/logo.svg \
+ --namespace jupyterhub \
+ --dry-run=client -o yaml | kubectl apply -f -
+
+ helm upgrade --install jupyterhub jupyterhub/jupyterhub \
+ --namespace jupyterhub \
+ --values config.yaml
+
+ kubectl apply -f jupyterhub-ingress.yaml
+ echo "✓ jupyterhub installing"
+ kubectl get pods -n jupyterhub
+ '';
+ };
+
+ db-install = pkgs.writeShellApplication {
+ name = "db-install";
+ runtimeInputs = [
+ pkgs.kubernetes-helm
+ pkgs.kubectl
+ ];
+ text = ''
+ helm upgrade --install jupyterhub-db bitnami/postgresql \
+ --namespace jupyterhub \
+ --set auth.username=jupyterhub \
+ --set auth.password=jupyterhub \
+ --set auth.database=jupyterhub
+ echo "✓ postgres installing"
+ kubectl get pods -n jupyterhub
+ '';
+ };
+
+ notebook-build = pkgs.writeShellApplication {
+ name = "notebook-build";
+ runtimeInputs = [
+ pkgs.podman
+ pkgs.kind
+ ];
+ text = ''
+ podman build -t kupyter-notebook:latest ./kupyter-notebook
+ podman save kupyter-notebook:latest -o /tmp/kupyter-notebook.tar
+ kind load image-archive /tmp/kupyter-notebook.tar --name kupyter
+ rm /tmp/kupyter-notebook.tar
+ echo "✓ image built and loaded into cluster"
+ '';
+ };
+
+ hub-build = pkgs.writeShellApplication {
+ name = "hub-build";
+ runtimeInputs = [
+ pkgs.podman
+ pkgs.kind
+ ];
+ text = ''
+ podman build -t kupyter-hub:latest -f Dockerfile.hub .
+ podman save kupyter-hub:latest -o /tmp/kupyter-hub.tar
+ kind load image-archive /tmp/kupyter-hub.tar --name kupyter
+ rm /tmp/kupyter-hub.tar
+ echo "✓ hub image built and loaded"
+ '';
+ };
+
+ ngshare-install = pkgs.writeShellApplication {
+ name = "ngshare-install";
+ runtimeInputs = [
+ pkgs.kubernetes-helm
+ pkgs.kubectl
+ ];
+ text = ''
+ helm upgrade --install ngshare ngshare/ngshare \
+ --namespace jupyterhub \
+ --set ngshare.token=822235aaaf83f0232c799fe948cbb1594b01d7d7b11af051871cc2d3fcb08fe8 \
+ --set ngshare.hub_api_token=822235aaaf83f0232c799fe948cbb1594b01d7d7b11af051871cc2d3fcb08fe8 \
+ --set ngshare.admins="admin,ngshare-setup" \
+ --values ngshare-values.yaml
+
+ echo "Waiting for ngshare..."
+ kubectl wait pod \
+ --for=condition=ready \
+ --selector=app.kubernetes.io/name=ngshare \
+ --namespace jupyterhub \
+ --timeout=120s
+
+ echo "✓ ngshare installed"
+ kubectl get pods -n jupyterhub
+ '';
+ };
+
+ ngshare-courses = pkgs.writeShellApplication {
+ name = "ngshare-courses";
+ runtimeInputs = [ pkgs.kubectl ];
+ text = ''
+ HUB_POD=$(kubectl get pod -n jupyterhub -l component=hub -o name)
+
+ kubectl exec -n jupyterhub "$HUB_POD" -- curl -s -X POST \
+ "http://ngshare.jupyterhub.svc.cluster.local:8080/services/ngshare/course/Programmieren" \
+ -d "instructors=[\"instructor-Prog\"]" \
+ -H "Authorization: token setuptoken123456789012345678901234567890123456789012345678901234"
+
+ kubectl exec -n jupyterhub "$HUB_POD" -- curl -s -X POST \
+ "http://ngshare.jupyterhub.svc.cluster.local:8080/services/ngshare/course/Signale" \
+ -d "instructors=[\"instructor-Sig\"]" \
+ -H "Authorization: token setuptoken123456789012345678901234567890123456789012345678901234"
+
+ echo "✓ courses created"
+ '';
+ };
+
+ monitoring-install = pkgs.writeShellApplication {
+ name = "monitoring-install";
+ runtimeInputs = [
+ pkgs.kubernetes-helm
+ pkgs.kubectl
+ ];
+ text = ''
+ helm upgrade --install kube-prometheus-stack prometheus/kube-prometheus-stack \
+ --namespace monitoring \
+ --create-namespace \
+ --wait
+ kubectl apply -f ./jupyterhub-monitor.yaml
+ echo "✓ monitoring installed"
+ kubectl get pods -n monitoring
+ '';
+ };
+
+ rebuild-all = pkgs.writeShellApplication {
+ name = "rebuild-all";
+ runtimeInputs = [
+ pkgs.kind
+ pkgs.kubectl
+ pkgs.kubernetes-helm
+ pkgs.podman
+ ];
+ text = ''
+ cluster-stop
+ cluster-start
+ tls-init
+ helm-repos
+ ingress-install
+ hub-build
+ notebook-build
+ db-install
+ ngshare-install
+ jupyterhub-install
+ monitoring-install
+ echo "Waiting for hub to be ready..."
+ kubectl wait pod \
+ --for=condition=ready \
+ --selector=component=hub \
+ --namespace jupyterhub \
+ --timeout=300s
+ ngshare-courses
+ '';
+ };
+
+ in
+ {
+ devShells.default = pkgs.mkShell {
+ name = "kupyter";
+
+ packages = [
+ get-all
+ cluster-start
+ cluster-stop
+ tls-init
+ helm-repos
+ ingress-install
+ jupyterhub-install
+ notebook-build
+ db-install
+ ngshare-install
+ ngshare-courses
+ hub-build
+ monitoring-install
+ rebuild-all
+ pkgs.openssl
+ pkgs.kubectl
+ pkgs.kubernetes-helm
+ pkgs.podman
+ pkgs.kind
+ ];
+
+ shellHook = ''
+ export KUBECONFIG="$(pwd)/.kubeconfig"
+ export KIND_EXPERIMENTAL_PROVIDER=podman
+
+ echo ""
+ echo " kupyter dev shell"
+ echo " KUBECONFIG → $(pwd)/.kubeconfig (session-local)"
+ echo " runtime → podman (rootless)"
+ echo ""
+ echo " cluster-start # create cluster"
+ echo " cluster-stop # delete cluster + wipe kubeconfig"
+ echo " tls-init # generate self-signed cert"
+ echo " helm-repos # add and update helm repos"
+ echo " ingress-install # install nginx ingress"
+ echo " notebook-build # build and load notebook image"
+ echo " jupyterhub-install # install jupyterhub"
+ echo " get-all # show all pods"
+ echo ""
+ '';
+ };
+ }
+ );
+}
diff --git a/hub-logo-configmap.yaml b/hub-logo-configmap.yaml
new file mode 100644
index 0000000..a6c2d69
--- /dev/null
+++ b/hub-logo-configmap.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: hub-logos
+ namespace: jupyterhub
+binaryData:
+ jupyterhub-80.png: ""
+ jupyterhub.svg: ""
diff --git a/jupyterhub-ingress.yaml b/jupyterhub-ingress.yaml
new file mode 100644
index 0000000..d182855
--- /dev/null
+++ b/jupyterhub-ingress.yaml
@@ -0,0 +1,21 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: jupyterhub
+ namespace: jupyterhub
+ annotations:
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
+ nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: jupyterhub.local
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: proxy-public
+ port:
+ number: 80
diff --git a/jupyterhub-monitor.yaml b/jupyterhub-monitor.yaml
new file mode 100644
index 0000000..f1a203b
--- /dev/null
+++ b/jupyterhub-monitor.yaml
@@ -0,0 +1,20 @@
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+ name: jupyterhub
+ namespace: monitoring
+ labels:
+ release: kube-prometheus-stack
+spec:
+ namespaceSelector:
+ matchNames:
+ - jupyterhub
+ selector:
+ matchLabels:
+ component: hub
+ app: jupyterhub
+ release: jupyterhub
+ endpoints:
+ - port: hub
+ path: /hub/metrics
+ interval: 30s
diff --git a/kind-config-resolved.yaml b/kind-config-resolved.yaml
new file mode 100644
index 0000000..3ce5dc5
--- /dev/null
+++ b/kind-config-resolved.yaml
@@ -0,0 +1,7 @@
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+name: kupyter
+containerdConfigPatches:
+ - |-
+ [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"]
+ endpoint = ["http://10.89.2.2:5000"]
diff --git a/kind-config.yaml b/kind-config.yaml
new file mode 100644
index 0000000..6abb430
--- /dev/null
+++ b/kind-config.yaml
@@ -0,0 +1,16 @@
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+name: kupyter
+nodes:
+ - role: control-plane
+ extraPortMappings:
+ - containerPort: 80
+ hostPort: 80
+ protocol: TCP
+ - containerPort: 443
+ hostPort: 443
+ protocol: TCP
+containerdConfigPatches:
+ - |-
+ [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"]
+ endpoint = ["http://REGISTRY_IP:5000"]
diff --git a/kupyter-notebook/Dockerfile b/kupyter-notebook/Dockerfile
new file mode 100644
index 0000000..ef055ab
--- /dev/null
+++ b/kupyter-notebook/Dockerfile
@@ -0,0 +1,9 @@
+FROM jupyter/datascience-notebook:latest
+
+COPY requirements.txt .
+RUN pip install -r requirements.txt
+
+COPY nbgrader_config.py /etc/jupyter/nbgrader_config.py
+
+COPY logo.png /opt/conda/lib/python3.13/site-packages/nbclassic/static/base/images/logo.png
+COPY logo.png /opt/conda/lib/python3.13/site-packages/jupyter_server/static/logo/logo.png
diff --git a/kupyter-notebook/logo.png b/kupyter-notebook/logo.png
new file mode 100644
index 0000000..857ca0d
Binary files /dev/null and b/kupyter-notebook/logo.png differ
diff --git a/kupyter-notebook/logo.svg b/kupyter-notebook/logo.svg
new file mode 100644
index 0000000..d491f98
--- /dev/null
+++ b/kupyter-notebook/logo.svg
@@ -0,0 +1,198 @@
+
+
+
+
\ No newline at end of file
diff --git a/kupyter-notebook/nbgrader_config.py b/kupyter-notebook/nbgrader_config.py
new file mode 100644
index 0000000..23a9b3d
--- /dev/null
+++ b/kupyter-notebook/nbgrader_config.py
@@ -0,0 +1,11 @@
+from ngshare_exchange import configureExchange
+
+c = get_config()
+
+configureExchange(
+ c, "http://ngshare.jupyterhub.svc.cluster.local:8080/services/ngshare"
+)
+
+c.CourseDirectory.course_id = "*"
+
+c.FormgradeApp.base_url = "/user/{username}/"
diff --git a/kupyter-notebook/requirements.txt b/kupyter-notebook/requirements.txt
new file mode 100644
index 0000000..2ef65d8
--- /dev/null
+++ b/kupyter-notebook/requirements.txt
@@ -0,0 +1,7 @@
+folium
+seaborn
+#tensorflow
+#torch
+nbgrader
+ngshare_exchange
+rise
diff --git a/nbgrader_config.hub.py b/nbgrader_config.hub.py
new file mode 100644
index 0000000..de1369b
--- /dev/null
+++ b/nbgrader_config.hub.py
@@ -0,0 +1,9 @@
+from ngshare_exchange import configureExchange
+
+c = get_config()
+
+configureExchange(
+ c, "http://ngshare.jupyterhub.svc.cluster.local:8080/services/ngshare"
+)
+
+c.CourseDirectory.course_id = "*"
diff --git a/ngshare-values.yaml b/ngshare-values.yaml
new file mode 100644
index 0000000..a49eeea
--- /dev/null
+++ b/ngshare-values.yaml
@@ -0,0 +1,6 @@
+pvc:
+ accessModes:
+ - ReadWriteOnce
+
+ngshare:
+ debug: true
diff --git a/tls.crt b/tls.crt
new file mode 100644
index 0000000..527e358
--- /dev/null
+++ b/tls.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDOzCCAiOgAwIBAgIUdweHytW9S6NYnsg8qQC7qkXrSAkwDQYJKoZIhvcNAQEL
+BQAwLTEZMBcGA1UEAwwQanVweXRlcmh1Yi5sb2NhbDEQMA4GA1UECgwHa3VweXRl
+cjAeFw0yNjAzMjQwMDExMjJaFw0yNzAzMjQwMDExMjJaMC0xGTAXBgNVBAMMEGp1
+cHl0ZXJodWIubG9jYWwxEDAOBgNVBAoMB2t1cHl0ZXIwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQCsSo1M0tgSqAwEFQ2XY8qJIdhFvJojTj2mK18gizfS
+Egn9whTynbKX6+AxRRlH+TQ8TQ91IYXGpZJaAXnxpwwC1X9qjeqrKFoI9927Bgp2
+1CzK8dI2Q7DZPTeSgAAW79dSjApwxqiw+Dy+PP1REe94UvKg/PQezN8Zy3V4+c9F
+7noVgUp/xEWPsVwsH84uX2XI7HyHv4t3sMAzqD4d/41GoJX8FZwwK5IKuIyiNVJo
+y2UUjcG7qXUUBUXuOcpyW0l9zwRsDtqp6mqXj4+VCdLHKYP/X/mftLK+cwhzNvN5
+uWooZf78igN7rFp1n6cvmIUuQEUnnPGkwBfbZSFK0l7NAgMBAAGjUzBRMB0GA1Ud
+DgQWBBRA0biw83Xcib+sQcIypsreXfv9oDAfBgNVHSMEGDAWgBRA0biw83Xcib+s
+QcIypsreXfv9oDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB5
+ufcXwO0iKoXkPkAoVXEz4xs0uGQ6Q4nq23VYoNGqKtqlRm7p9FnWv/OFC6p4QXT8
+J1Ju2oa5iQ8iUVvznPEUz78995xMtrnpQbJCHOBvCMpYojNiOmq1d+rxZvzJrVmS
+PxMtljbS1Xk60WTmGoWIFjEl8j0TARVzW3tnafEv83Ss/E4oZjo7RVVsyyAa+VQ/
+6kB3HsJRdie+9FpOFU0YzyFuQb+e8mVVUSMwI42WAflDK+PD8ZjyRWVrj+WNWM0b
+1Ic1Fe8BzGuDigPc6Md8i09WePyLWw+Q11x2ZCMJzKk4Nh0EPmgemusRUPXBsk0Z
+ozVE/zLvqtJg+94F8q9l
+-----END CERTIFICATE-----
diff --git a/tls.key b/tls.key
new file mode 100644
index 0000000..d34a8a3
--- /dev/null
+++ b/tls.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsSo1M0tgSqAwE
+FQ2XY8qJIdhFvJojTj2mK18gizfSEgn9whTynbKX6+AxRRlH+TQ8TQ91IYXGpZJa
+AXnxpwwC1X9qjeqrKFoI9927Bgp21CzK8dI2Q7DZPTeSgAAW79dSjApwxqiw+Dy+
+PP1REe94UvKg/PQezN8Zy3V4+c9F7noVgUp/xEWPsVwsH84uX2XI7HyHv4t3sMAz
+qD4d/41GoJX8FZwwK5IKuIyiNVJoy2UUjcG7qXUUBUXuOcpyW0l9zwRsDtqp6mqX
+j4+VCdLHKYP/X/mftLK+cwhzNvN5uWooZf78igN7rFp1n6cvmIUuQEUnnPGkwBfb
+ZSFK0l7NAgMBAAECggEAAKUpL6g7X4nIvQHm/iwWoHoyGonWwtTPrDJN1deW45qJ
+PAq1Dl+0WF0zQNqGEXv5F4K26D1sZVFxIYlsg8sYTBnfvvPmdNUqcMfiSRUjHhNu
+gMfdIIPkd1JUxZ+2cdPoLo2KgzHe4cA4lI34LzsfXx9K8HrvGXNV8f1fjUjrR+pX
+n2aCTCr+yqvF57ja9JugwOVdpWxfdJK7Km19cEDh78BTwJhmw0dUujh8eFcecxN0
+m9s+HKuOs+TDhl5G2FgfmGxJgz/g5qUDPy+MulPISpQnkFNQ8lYV1FYpCxqsbm+D
+L4uGNqvaUbgsl/TDuujcSnFzPNih01D6ZiHESwE/pwKBgQDSGp6RqfA7e9HTIRmW
+HORupf1eE/qdIwVWVhBcmHOB6teAY5gtltEnFojOi6+nwm3yuU72pD59S+Ev08g+
+fxdDAdaGR16+9Ih6KvPQNiI+2w9p0huoXIz+kZPGOYrL2d5hsKQrOnNHUEVwHQ4J
+BhMrr5Sf4Xjxpv8ATgKyb39CowKBgQDR7WCAFUwvyisVrz1GDAkGzmimbLtWUsSg
+dzOX0e54zDf8sC4QJdgowSIsFbfQqfkQQkUJrVA3Lk92XNfuyBo4RS9rnNv0F4eC
+4CMFcBsjQ5gIgu5OHbXf1RFoc4fwabetIbb7z3xbmixQfJxgbyzySO8GZ7RwtLN+
+/pVvthtfzwKBgQDHkCM3hmu5hFV7rb/o1o6fDqkHOADeSopiRCMMYH2uVArXV0IP
+Y2ZMM1pEnWd99+6JEzyOhtkYF//PduCHhB3rNo62Qooa5JfROoUVKqYCf/427Cv7
+EdWWY14ydSuBjvJsZeS5bq5aeUNLRz2ykoOZBhAsgHRpS86AUpi7Na5x8wKBgGi8
+Dq44cfdR3ScHdAGTlZlQt8N4cgrCZplMf3Aaa+jWsoQefgzOZMcIfH0UJM41Ty6+
+cWU/k8rEDx8VeSIHsZUrZ1pAOzjP2GsCWlanNNLmMV7lu/E7P3c5/WJoaYUXqWz2
+ai29ueSVydAqK3atYPZMTvyaFts4PGl6qKHAcG3fAoGBALYJ7OhtEddzo3PKPZOh
+uhH2vyRsLuTZCZYDGPIdxHSS0Kk307oghIZhYwKf+fx5H1Y6xtmZ59QUAg4V2Ol2
+p8uoJW50skVHx/OwZkm+wQkBF+INj4akG/MaFpJ8XcwWtn8FiEMN+ljBJx7nu6dW
+GxA2b5RVTQ8SBnF2TEFwjw5o
+-----END PRIVATE KEY-----