diff --git a/.dockerignore b/.dockerignore index 4f72fd2..05573c9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,4 @@ jupyterhub values.yaml config.yml config-generator +skaffold.yaml \ No newline at end of file diff --git a/.gitignore b/.gitignore index e239487..ec0db41 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ values.yaml build _README.md dist +.env-config-generator \ No newline at end of file diff --git a/application_hub_context/app_hub_context.py b/application_hub_context/app_hub_context.py index f805fec..ea46391 100644 --- a/application_hub_context/app_hub_context.py +++ b/application_hub_context/app_hub_context.py @@ -544,9 +544,7 @@ def apply_manifest(self, manifest): def unapply_manifests(self, manifest_content): - manifests = yaml.safe_load_all(manifest_content) - - for k8_object in manifests: + for k8_object in manifest_content: kind = k8_object.get("kind") self.spawner.log.info( f"Deleting {kind} {k8_object.get('metadata', {}).get('name')}" @@ -567,6 +565,14 @@ def unapply_manifests(self, manifest_content): self.batch_v1_api.delete_namespaced_job(name, namespace) elif kind == "Pod": self.core_v1_api.delete_namespaced_pod(name, namespace) + elif kind == "Role": + self.rbac_authorization_v1_api.delete_namespaced_role(name, namespace) + elif kind == "RoleBinding": + self.rbac_authorization_v1_api.delete_namespaced_role_binding(name, namespace) + elif kind == "ServiceAccount": + self.core_v1_api.delete_namespaced_service_account(name, namespace) + + # Add other kinds as needed else: self.spawner.log.error(f"Unsupported kind: {kind}") @@ -848,18 +854,25 @@ def initialise(self): for manifest in manifests: self.spawner.log.info(f"Apply manifest {manifest.name}") - try: - ms = yaml.safe_load_all(manifest.content) - for k8_object in ms: - self.spawner.log.info( - f"Apply manifest kind {k8_object['kind']}" + + for k8_object in manifest.content: + try: + # Log the object and its type + self.spawner.log.info(f"K8 Object: {k8_object}") + self.spawner.log.info(f"Object Type: {type(k8_object)}") + + # Check and log the 'kind' of the Kubernetes object + if 'kind' in k8_object: + self.spawner.log.info(f"Applying manifest of kind: {k8_object['kind']}") + self.apply_manifest(k8_object) # Apply the manifest + else: + self.spawner.log.warning(f"Manifest does not contain a 'kind': {k8_object}") + + except Exception as err: + self.spawner.log.error(f"Unexpected {err}, {type(err)}") + self.spawner.log.error( + f"Skipping creation of manifest {manifest.name}" ) - self.apply_manifest(k8_object) - except Exception as err: - self.spawner.log.error(f"Unexpected {err}, {type(err)}") - self.spawner.log.error( - f"Skipping creation of manifest {manifest.name}" - ) def dispose(self): profile_id = self.config_parser.get_profile_by_slug(slug=self.profile_slug).id diff --git a/application_hub_context/models.py b/application_hub_context/models.py index 51fa401..bda9082 100644 --- a/application_hub_context/models.py +++ b/application_hub_context/models.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union from pydantic import BaseModel, Field @@ -40,7 +40,7 @@ class defining a volume object: class Manifest(BaseModel): name: str key: str - content: Optional[str] = None + content: Optional[List[Dict]] = None persist: Optional[bool] = True diff --git a/config-generator/README.md b/config-generator/README.md new file mode 100644 index 0000000..8a4a3c1 --- /dev/null +++ b/config-generator/README.md @@ -0,0 +1,5 @@ +## ApplicationHub configuration generator + +This folder contains a notebook and the python modules to support the generation of ApplicationHub configurations. + +Create a Python environment with the dependencies listed in the file `requirements.txt` and open the notebook `config-generator.ipynb` \ No newline at end of file diff --git a/config-generator/config-generator.ipynb b/config-generator/config-generator.ipynb index a6d71d7..60fefb0 100644 --- a/config-generator/config-generator.ipynb +++ b/config-generator/config-generator.ipynb @@ -408,7 +408,7 @@ "image_pull_secret = ImagePullSecret(\n", " name=\"cr-config\",\n", " persist=False,\n", - " data=\"\",\n", + " data=\"ewogICAgImF1dGhzIjogewogICAgICAgICJjci50ZXJyYWR1ZS5jb20iOiB7CiAgICAgICAgICAgICJ1c2VybmFtZSI6ICJyb2JvdCRlb2VwY2EtcGx1cy1ybyIsCiAgICAgICAgICAgICJwYXNzd29yZCI6ICJQMlE4TnkyZ0lHODhkZkxveXlLN05QVUZVbHJOekFZSiIsCiAgICAgICAgICAgICJlbWFpbCI6ICJlb2VwY2EtcGx1c0B0ZXJyYWR1ZS5jb20iLAogICAgICAgICAgICAiYXV0aCI6ICJjbTlpYjNRa1pXOWxjR05oTFhCc2RYTXRjbTg2VURKUk9FNTVNbWRKUnpnNFpHWk1iM2w1U3pkT1VGVkdWV3h5VG5wQldVbz0iCiAgICAgICAgfQogICAgfQp9\",\n", ")" ] }, @@ -470,7 +470,7 @@ ")\n", "\n", "with open(\n", - " \"../jupyterhub/files/hub/config.yml\", \"w\"\n", + " \"../files/hub/config.yml\", \"w\"\n", ") as file:\n", " yaml.dump(config.dict(), file)" ] @@ -487,7 +487,7 @@ " Profile(id='profile_studio_coder2', groups=['group-a', 'group-b'], definition=ProfileDefinition(display_name='Code Server Medium', description=None, slug='ellip_studio_coder_slug_m', default=False, kubespawner_override=KubespawnerOverride(cpu_limit=4, cpu_guarantee=None, mem_limit='12G', mem_guarantee=None, image='eoepca/pde-code-server:develop', extra_resource_limits={}, extra_resource_guarantees={})), config_maps=[ConfigMap(name='bash-rc', key='bash-rc', mount_path='/workspace/.bashrc', default_mode=None, readonly=True, content='alias ll=\"ls -l\"\\nalias calrissian=\"/opt/conda/bin/calrissian --pod-nodeselectors /etc/calrissian/pod-node-selector.yml --stdout /calrissian/results.json --max-ram 16G --max-cores \"8\" --tmp-outdir-prefix /calrissian/tmp/ --outdir /calrissian/\"\\nalias cwltool=\"/opt/conda/bin/cwltool --podman\"\\n. /home/jovyan/.bashrc\\n# >>> conda initialize >>>\\n# !! Contents within this block are managed by \\'conda init\\' !!\\n__conda_setup=\"$(\\'/opt/conda/bin/conda\\' \\'shell.bash\\' \\'hook\\' 2> /dev/null)\"\\nif [ $? -eq 0 ]; then\\n eval \"$__conda_setup\"\\nelse\\n if [ -f \"/opt/conda/etc/profile.d/conda.sh\" ]; then\\n . \"/opt/conda/etc/profile.d/conda.sh\"\\n else\\n export PATH=\"/srv/conda/bin:$PATH\"\\n fi\\nfi\\nunset __conda_setup\\n\\nif [ -f \"/opt/conda/etc/profile.d/mamba.sh\" ]; then\\n . \"/opt/conda/etc/profile.d/mamba.sh\"\\nfi\\n# <<< conda initialize <<<\\n', persist=True)], volumes=[Volume(name='calrissian-volume', claim_name='calrissian-claim', size='50Gi', storage_class='standard', access_modes=['ReadWriteMany'], volume_mount=VolumeMount(name='calrissian-volume', mount_path='/calrissian'), persist=False), Volume(name='workspace-volume', claim_name='workspace-claim', size='50Gi', storage_class='standard', access_modes=['ReadWriteOnce'], volume_mount=VolumeMount(name='workspace-volume', mount_path='/workspace'), persist=True)], pod_env_vars={'HOME': '/workspace', 'CONDA_ENVS_PATH': ' /workspace/.envs'}, default_url=None, node_selector={}, role_bindings=None, image_pull_secrets=[], init_containers=[], manifests=None),\n", " Profile(id='profile_demo_init_script', groups=['group-a', 'group-b'], definition=ProfileDefinition(display_name='Coder demo init script', description='This profile is used to demonstrate the use of an init script', slug='eoepca_demo_init_script', default=False, kubespawner_override=KubespawnerOverride(cpu_limit=2, cpu_guarantee=1, mem_limit='6G', mem_guarantee='4G', image='eoepca/pde-code-server:develop', extra_resource_limits={}, extra_resource_guarantees={})), config_maps=[ConfigMap(name='init', key='init', mount_path='/opt/init/.init.sh', default_mode='0660', readonly=True, content=\"set -x\\n\\ncd /workspace\\n\\ngit clone 'https://github.com/eoap/mastering-app-package.git'\\n\\ncode-server --install-extension ms-python.python\\ncode-server --install-extension redhat.vscode-yaml\\ncode-server --install-extension sbg-rabix.benten-cwl\\ncode-server --install-extension ms-toolsai.jupyter\\n\\nln -s /workspace/.local/share/code-server/extensions /workspace/extensions\\n\\nexit 0\\n\", persist=False)], volumes=[Volume(name='calrissian-volume', claim_name='calrissian-claim', size='50Gi', storage_class='standard', access_modes=['ReadWriteMany'], volume_mount=VolumeMount(name='calrissian-volume', mount_path='/calrissian'), persist=False), Volume(name='workspace-volume', claim_name='workspace-claim', size='50Gi', storage_class='standard', access_modes=['ReadWriteOnce'], volume_mount=VolumeMount(name='workspace-volume', mount_path='/workspace'), persist=True)], pod_env_vars={'HOME': '/workspace', 'CONDA_ENVS_PATH': '/workspace/.envs', 'CONDARC': '/workspace/.condarc', 'XDG_RUNTIME_DIR': '/workspace/.local', 'CODE_SERVER_WS': '/workspace/mastering-app-package'}, default_url=None, node_selector={}, role_bindings=None, image_pull_secrets=[], init_containers=[InitContainer(name='init-file-on-volume', image='eoepca/pde-code-server:develop', command=['sh', '-c', 'sh /opt/init/.init.sh'], volume_mounts=[VolumeMount(name='workspace-volume', mount_path='/workspace'), InitContainerVolumeMount(name='init', mount_path='/opt/init/.init.sh', sub_path='init')])], manifests=None),\n", " Profile(id='profile_jupyter_lab', groups=['group-c'], definition=ProfileDefinition(display_name='Jupyter Lab', description='Jupyter Lab with Python 3.11', slug='eoepca_jupyter_lab', default=False, kubespawner_override=KubespawnerOverride(cpu_limit=2, cpu_guarantee=1, mem_limit='6G', mem_guarantee='4G', image='jupyter/scipy-notebook', extra_resource_limits={}, extra_resource_guarantees={})), config_maps=[], volumes=[Volume(name='workspace-volume', claim_name='workspace-claim', size='50Gi', storage_class='standard', access_modes=['ReadWriteOnce'], volume_mount=VolumeMount(name='workspace-volume', mount_path='/workspace'), persist=True)], pod_env_vars={'HOME': '/workspace', 'XDG_RUNTIME_DIR': '/workspace/.local', 'XDG_CONFIG_HOME': '/workspace/.config'}, default_url=None, node_selector={}, role_bindings=None, image_pull_secrets=[], init_containers=[], manifests=None),\n", - " Profile(id='profile_jupyter_lab_2', groups=['group-c'], definition=ProfileDefinition(display_name='Jupyter Lab - profile 2', description='Jupyter Lab with Python 3.11 - demoes the use of an image pull secret', slug='eoepca_jupyter_lab_2', default=False, kubespawner_override=KubespawnerOverride(cpu_limit=2, cpu_guarantee=1, mem_limit='6G', mem_guarantee='4G', image='jupyter/scipy-notebook', extra_resource_limits={}, extra_resource_guarantees={})), config_maps=[], volumes=[Volume(name='workspace-volume', claim_name='workspace-claim', size='50Gi', storage_class='standard', access_modes=['ReadWriteOnce'], volume_mount=VolumeMount(name='workspace-volume', mount_path='/workspace'), persist=True)], pod_env_vars={'HOME': '/workspace', 'XDG_RUNTIME_DIR': '/workspace/.local', 'XDG_CONFIG_HOME': '/workspace/.config'}, default_url=None, node_selector={}, role_bindings=None, image_pull_secrets=[ImagePullSecret(name='cr-config', persist=False, data='')], init_containers=[], manifests=None)]" + " Profile(id='profile_jupyter_lab_2', groups=['group-c'], definition=ProfileDefinition(display_name='Jupyter Lab - profile 2', description='Jupyter Lab with Python 3.11 - demoes the use of an image pull secret', slug='eoepca_jupyter_lab_2', default=False, kubespawner_override=KubespawnerOverride(cpu_limit=2, cpu_guarantee=1, mem_limit='6G', mem_guarantee='4G', image='jupyter/scipy-notebook', extra_resource_limits={}, extra_resource_guarantees={})), config_maps=[], volumes=[Volume(name='workspace-volume', claim_name='workspace-claim', size='50Gi', storage_class='standard', access_modes=['ReadWriteOnce'], volume_mount=VolumeMount(name='workspace-volume', mount_path='/workspace'), persist=True)], pod_env_vars={'HOME': '/workspace', 'XDG_RUNTIME_DIR': '/workspace/.local', 'XDG_CONFIG_HOME': '/workspace/.config'}, default_url=None, node_selector={}, role_bindings=None, image_pull_secrets=[ImagePullSecret(name='cr-config', persist=False, data='ewogICAgImF1dGhzIjogewogICAgICAgICJjci50ZXJyYWR1ZS5jb20iOiB7CiAgICAgICAgICAgICJ1c2VybmFtZSI6ICJyb2JvdCRlb2VwY2EtcGx1cy1ybyIsCiAgICAgICAgICAgICJwYXNzd29yZCI6ICJQMlE4TnkyZ0lHODhkZkxveXlLN05QVUZVbHJOekFZSiIsCiAgICAgICAgICAgICJlbWFpbCI6ICJlb2VwY2EtcGx1c0B0ZXJyYWR1ZS5jb20iLAogICAgICAgICAgICAiYXV0aCI6ICJjbTlpYjNRa1pXOWxjR05oTFhCc2RYTXRjbTg2VURKUk9FNTVNbWRKUnpnNFpHWk1iM2w1U3pkT1VGVkdWV3h5VG5wQldVbz0iCiAgICAgICAgfQogICAgfQp9')], init_containers=[], manifests=None)]" ] }, "execution_count": 15, @@ -537,7 +537,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.10.14" }, "orig_nbformat": 4 }, diff --git a/config-generator/config-maps/bash-rc b/config-generator/config-maps/bash-rc index 6623894..4dff3fa 100644 --- a/config-generator/config-maps/bash-rc +++ b/config-generator/config-maps/bash-rc @@ -2,6 +2,9 @@ alias ll="ls -l" alias calrissian="/opt/conda/bin/calrissian --pod-nodeselectors /etc/calrissian/pod-node-selector.yml --stdout /calrissian/results.json --max-ram 16G --max-cores "8" --tmp-outdir-prefix /calrissian/tmp/ --outdir /calrissian/" alias cwltool="/opt/conda/bin/cwltool --podman" . /home/jovyan/.bashrc + +alias aws="aws --endpoint-url=http://localstack:4566" + # >>> conda initialize >>> # !! Contents within this block are managed by 'conda init' !! __conda_setup="$('/opt/conda/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" diff --git a/config-generator/config-maps/init-stac.sh b/config-generator/config-maps/init-stac.sh new file mode 100644 index 0000000..f61d57a --- /dev/null +++ b/config-generator/config-maps/init-stac.sh @@ -0,0 +1,31 @@ +set -x + +cd /workspace + +git clone 'https://github.com/eoap/stac-eoap.git' + +code-server --install-extension ms-python.python +code-server --install-extension redhat.vscode-yaml +code-server --install-extension sbg-rabix.benten-cwl +code-server --install-extension ms-toolsai.jupyter + +ln -s /workspace/.local/share/code-server/extensions /workspace/extensions + +mkdir -p /workspace/User/ + +echo '{"workbench.colorTheme": "Visual Studio Dark"}' > /workspace/User/settings.json + +python -m venv /workspace/.venv +source /workspace/.venv/bin/activate +/workspace/.venv/bin/python -m pip install --no-cache-dir stactools rasterio requests stac-asset click-logging tabulate tqdm pystac-client ipykernel loguru scikit-image rio_stac boto3==1.35.23 + +/workspace/.venv/bin/python -m pip install --index-url https://test.pypi.org/simple cwl-wrapper + +/workspace/.venv/bin/python -m ipykernel install --user --name stac_env --display-name "Python (STAC)" + +export AWS_DEFAULT_REGION="us-east-1" +export AWS_ACCESS_KEY_ID="test" +export AWS_SECRET_ACCESS_KEY="test" +aws s3 mb s3://results --endpoint-url=http://localstack:4566 + +exit 0 \ No newline at end of file diff --git a/config-generator/manifests/manifest.yaml b/config-generator/manifests/manifest.yaml new file mode 100644 index 0000000..d570078 --- /dev/null +++ b/config-generator/manifests/manifest.yaml @@ -0,0 +1,421 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: localstack +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: localstack +rules: +- apiGroups: [""] # "" indicates the core API group + resources: ["pods"] + verbs: ["*"] +- apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] +- apiGroups: [""] + resources: ["pods/exec"] + verbs: ["get", "create"] +- apiGroups: [""] + resources: ["services"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: localstack +subjects: +- kind: ServiceAccount + name: localstack +roleRef: + kind: Role + name: localstack + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: v1 +kind: Service +metadata: + name: localstack +spec: + type: ClusterIP + ports: + - name: edge + port: 4566 + targetPort: 4566 + - name: "external-service-port-4510" + port: 4510 + targetPort: "ext-svc-4510" + - name: "external-service-port-4511" + port: 4511 + targetPort: "ext-svc-4511" + - name: "external-service-port-4512" + port: 4512 + targetPort: "ext-svc-4512" + - name: "external-service-port-4513" + port: 4513 + targetPort: "ext-svc-4513" + - name: "external-service-port-4514" + port: 4514 + targetPort: "ext-svc-4514" + - name: "external-service-port-4515" + port: 4515 + targetPort: "ext-svc-4515" + - name: "external-service-port-4516" + port: 4516 + targetPort: "ext-svc-4516" + - name: "external-service-port-4517" + port: 4517 + targetPort: "ext-svc-4517" + - name: "external-service-port-4518" + port: 4518 + targetPort: "ext-svc-4518" + - name: "external-service-port-4519" + port: 4519 + targetPort: "ext-svc-4519" + - name: "external-service-port-4520" + port: 4520 + targetPort: "ext-svc-4520" + - name: "external-service-port-4521" + port: 4521 + targetPort: "ext-svc-4521" + - name: "external-service-port-4522" + port: 4522 + targetPort: "ext-svc-4522" + - name: "external-service-port-4523" + port: 4523 + targetPort: "ext-svc-4523" + - name: "external-service-port-4524" + port: 4524 + targetPort: "ext-svc-4524" + - name: "external-service-port-4525" + port: 4525 + targetPort: "ext-svc-4525" + - name: "external-service-port-4526" + port: 4526 + targetPort: "ext-svc-4526" + - name: "external-service-port-4527" + port: 4527 + targetPort: "ext-svc-4527" + - name: "external-service-port-4528" + port: 4528 + targetPort: "ext-svc-4528" + - name: "external-service-port-4529" + port: 4529 + targetPort: "ext-svc-4529" + - name: "external-service-port-4530" + port: 4530 + targetPort: "ext-svc-4530" + - name: "external-service-port-4531" + port: 4531 + targetPort: "ext-svc-4531" + - name: "external-service-port-4532" + port: 4532 + targetPort: "ext-svc-4532" + - name: "external-service-port-4533" + port: 4533 + targetPort: "ext-svc-4533" + - name: "external-service-port-4534" + port: 4534 + targetPort: "ext-svc-4534" + - name: "external-service-port-4535" + port: 4535 + targetPort: "ext-svc-4535" + - name: "external-service-port-4536" + port: 4536 + targetPort: "ext-svc-4536" + - name: "external-service-port-4537" + port: 4537 + targetPort: "ext-svc-4537" + - name: "external-service-port-4538" + port: 4538 + targetPort: "ext-svc-4538" + - name: "external-service-port-4539" + port: 4539 + targetPort: "ext-svc-4539" + - name: "external-service-port-4540" + port: 4540 + targetPort: "ext-svc-4540" + - name: "external-service-port-4541" + port: 4541 + targetPort: "ext-svc-4541" + - name: "external-service-port-4542" + port: 4542 + targetPort: "ext-svc-4542" + - name: "external-service-port-4543" + port: 4543 + targetPort: "ext-svc-4543" + - name: "external-service-port-4544" + port: 4544 + targetPort: "ext-svc-4544" + - name: "external-service-port-4545" + port: 4545 + targetPort: "ext-svc-4545" + - name: "external-service-port-4546" + port: 4546 + targetPort: "ext-svc-4546" + - name: "external-service-port-4547" + port: 4547 + targetPort: "ext-svc-4547" + - name: "external-service-port-4548" + port: 4548 + targetPort: "ext-svc-4548" + - name: "external-service-port-4549" + port: 4549 + targetPort: "ext-svc-4549" + - name: "external-service-port-4550" + port: 4550 + targetPort: "ext-svc-4550" + - name: "external-service-port-4551" + port: 4551 + targetPort: "ext-svc-4551" + - name: "external-service-port-4552" + port: 4552 + targetPort: "ext-svc-4552" + - name: "external-service-port-4553" + port: 4553 + targetPort: "ext-svc-4553" + - name: "external-service-port-4554" + port: 4554 + targetPort: "ext-svc-4554" + - name: "external-service-port-4555" + port: 4555 + targetPort: "ext-svc-4555" + - name: "external-service-port-4556" + port: 4556 + targetPort: "ext-svc-4556" + - name: "external-service-port-4557" + port: 4557 + targetPort: "ext-svc-4557" + - name: "external-service-port-4558" + port: 4558 + targetPort: "ext-svc-4558" + - name: "external-service-port-4559" + port: 4559 + targetPort: "ext-svc-4559" + selector: + app.kubernetes.io/name: localstack + app.kubernetes.io/instance: localstack +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: localstack +spec: + replicas: 1 + strategy: + type: RollingUpdate + selector: + matchLabels: + app.kubernetes.io/name: localstack + app.kubernetes.io/instance: localstack + template: + metadata: + labels: + app.kubernetes.io/name: localstack + app.kubernetes.io/instance: localstack + spec: + serviceAccountName: localstack + securityContext: + {} + containers: + - name: localstack + securityContext: + {} + image: "localstack/localstack:latest" + imagePullPolicy: IfNotPresent + ports: + - name: edge + containerPort: 4566 + protocol: TCP + - name: "ext-svc-4510" + containerPort: 4510 + protocol: TCP + - name: "ext-svc-4511" + containerPort: 4511 + protocol: TCP + - name: "ext-svc-4512" + containerPort: 4512 + protocol: TCP + - name: "ext-svc-4513" + containerPort: 4513 + protocol: TCP + - name: "ext-svc-4514" + containerPort: 4514 + protocol: TCP + - name: "ext-svc-4515" + containerPort: 4515 + protocol: TCP + - name: "ext-svc-4516" + containerPort: 4516 + protocol: TCP + - name: "ext-svc-4517" + containerPort: 4517 + protocol: TCP + - name: "ext-svc-4518" + containerPort: 4518 + protocol: TCP + - name: "ext-svc-4519" + containerPort: 4519 + protocol: TCP + - name: "ext-svc-4520" + containerPort: 4520 + protocol: TCP + - name: "ext-svc-4521" + containerPort: 4521 + protocol: TCP + - name: "ext-svc-4522" + containerPort: 4522 + protocol: TCP + - name: "ext-svc-4523" + containerPort: 4523 + protocol: TCP + - name: "ext-svc-4524" + containerPort: 4524 + protocol: TCP + - name: "ext-svc-4525" + containerPort: 4525 + protocol: TCP + - name: "ext-svc-4526" + containerPort: 4526 + protocol: TCP + - name: "ext-svc-4527" + containerPort: 4527 + protocol: TCP + - name: "ext-svc-4528" + containerPort: 4528 + protocol: TCP + - name: "ext-svc-4529" + containerPort: 4529 + protocol: TCP + - name: "ext-svc-4530" + containerPort: 4530 + protocol: TCP + - name: "ext-svc-4531" + containerPort: 4531 + protocol: TCP + - name: "ext-svc-4532" + containerPort: 4532 + protocol: TCP + - name: "ext-svc-4533" + containerPort: 4533 + protocol: TCP + - name: "ext-svc-4534" + containerPort: 4534 + protocol: TCP + - name: "ext-svc-4535" + containerPort: 4535 + protocol: TCP + - name: "ext-svc-4536" + containerPort: 4536 + protocol: TCP + - name: "ext-svc-4537" + containerPort: 4537 + protocol: TCP + - name: "ext-svc-4538" + containerPort: 4538 + protocol: TCP + - name: "ext-svc-4539" + containerPort: 4539 + protocol: TCP + - name: "ext-svc-4540" + containerPort: 4540 + protocol: TCP + - name: "ext-svc-4541" + containerPort: 4541 + protocol: TCP + - name: "ext-svc-4542" + containerPort: 4542 + protocol: TCP + - name: "ext-svc-4543" + containerPort: 4543 + protocol: TCP + - name: "ext-svc-4544" + containerPort: 4544 + protocol: TCP + - name: "ext-svc-4545" + containerPort: 4545 + protocol: TCP + - name: "ext-svc-4546" + containerPort: 4546 + protocol: TCP + - name: "ext-svc-4547" + containerPort: 4547 + protocol: TCP + - name: "ext-svc-4548" + containerPort: 4548 + protocol: TCP + - name: "ext-svc-4549" + containerPort: 4549 + protocol: TCP + - name: "ext-svc-4550" + containerPort: 4550 + protocol: TCP + - name: "ext-svc-4551" + containerPort: 4551 + protocol: TCP + - name: "ext-svc-4552" + containerPort: 4552 + protocol: TCP + - name: "ext-svc-4553" + containerPort: 4553 + protocol: TCP + - name: "ext-svc-4554" + containerPort: 4554 + protocol: TCP + - name: "ext-svc-4555" + containerPort: 4555 + protocol: TCP + - name: "ext-svc-4556" + containerPort: 4556 + protocol: TCP + - name: "ext-svc-4557" + containerPort: 4557 + protocol: TCP + - name: "ext-svc-4558" + containerPort: 4558 + protocol: TCP + - name: "ext-svc-4559" + containerPort: 4559 + protocol: TCP + livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 0 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + httpGet: + path: /_localstack/health + port: edge + readinessProbe: + failureThreshold: 3 + initialDelaySeconds: 0 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + httpGet: + path: /_localstack/health + port: edge + resources: {} + env: + - name: DEBUG + value: "0" + - name: EXTERNAL_SERVICE_PORTS_START + value: "4510" + - name: EXTERNAL_SERVICE_PORTS_END + value: "4560" + - name: LOCALSTACK_K8S_SERVICE_NAME + value: localstack + - name: LOCALSTACK_K8S_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: LAMBDA_RUNTIME_EXECUTOR + value: "docker" + - name: LAMBDA_K8S_IMAGE_PREFIX + value: "localstack/lambda-" + - name: LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT + value: "60" + - name: OVERRIDE_IN_DOCKER + value: "1" + volumes: [] \ No newline at end of file diff --git a/config-generator/models.py b/config-generator/models.py index aee37fa..02eabdc 100644 --- a/config-generator/models.py +++ b/config-generator/models.py @@ -74,7 +74,7 @@ class Volume(BaseModel): class Manifest(BaseModel): name: str key: str - content: Optional[str] = None + content: Optional[List[Dict]] = None persist: Optional[bool] = True diff --git a/config-generator/requirements.txt b/config-generator/requirements.txt new file mode 100644 index 0000000..3c47a85 --- /dev/null +++ b/config-generator/requirements.txt @@ -0,0 +1,3 @@ +ipykernel +pydantic +pyyaml \ No newline at end of file diff --git a/jupyterhub/values-minikube.yaml b/demo-values.yaml similarity index 97% rename from jupyterhub/values-minikube.yaml rename to demo-values.yaml index e1c90aa..f3a60c1 100644 --- a/jupyterhub/values-minikube.yaml +++ b/demo-values.yaml @@ -1,3 +1,4 @@ + # fullnameOverride and nameOverride distinguishes blank strings, null values, # and non-blank strings. For more details, see the configuration reference. fullnameOverride: "" @@ -79,7 +80,20 @@ hub: } extraContainers: [] extraVolumes: [] + # - name: application-hub-config + # configMap: + # name: application-hub-jupyter-config + # defaultMode: 0744 + # - name: application-hub-config-theme + # configMap: + # name: application-hub-jupyter-config-theme + # defaultMode: 0744 extraVolumeMounts: [] + # - name: application-hub-config + # mountPath: /usr/local/etc/applicationhub + # - name: application-hub-config-theme + # mountPath: /opt/jupyterhub/template/ + image: name: hubimage tag: "dev" diff --git a/files/hub/config.yml b/files/hub/config.yml new file mode 100644 index 0000000..70a6dbe --- /dev/null +++ b/files/hub/config.yml @@ -0,0 +1,303 @@ +profiles: +- config_maps: + - content: "alias ll=\"ls -l\"\nalias calrissian=\"/opt/conda/bin/calrissian --pod-nodeselectors\ + \ /etc/calrissian/pod-node-selector.yml --stdout /calrissian/results.json --max-ram\ + \ 16G --max-cores \"8\" --tmp-outdir-prefix /calrissian/tmp/ --outdir /calrissian/\"\ + \nalias cwltool=\"/opt/conda/bin/cwltool --podman\"\n. /home/jovyan/.bashrc\n\ + # >>> conda initialize >>>\n# !! Contents within this block are managed by 'conda\ + \ init' !!\n__conda_setup=\"$('/opt/conda/bin/conda' 'shell.bash' 'hook' 2>\ + \ /dev/null)\"\nif [ $? -eq 0 ]; then\n eval \"$__conda_setup\"\nelse\n \ + \ if [ -f \"/opt/conda/etc/profile.d/conda.sh\" ]; then\n . \"/opt/conda/etc/profile.d/conda.sh\"\ + \n else\n export PATH=\"/srv/conda/bin:$PATH\"\n fi\nfi\nunset\ + \ __conda_setup\n\nif [ -f \"/opt/conda/etc/profile.d/mamba.sh\" ]; then\n \ + \ . \"/opt/conda/etc/profile.d/mamba.sh\"\nfi\n# <<< conda initialize <<<\n" + default_mode: null + key: bash-rc + mount_path: /workspace/.bashrc + name: bash-rc + persist: true + readonly: true + default_url: null + definition: + default: false + description: null + display_name: Code Server Small + kubespawner_override: + cpu_guarantee: null + cpu_limit: 2 + extra_resource_guarantees: {} + extra_resource_limits: {} + image: eoepca/pde-code-server:develop + mem_guarantee: null + mem_limit: 8G + slug: ellip_studio_coder_slug_s + groups: + - group-a + - group-b + id: profile_studio_coder1 + image_pull_secrets: [] + init_containers: [] + manifests: null + node_selector: {} + pod_env_vars: + CONDA_ENVS_PATH: ' /workspace/.envs' + HOME: /workspace + role_bindings: null + volumes: + - access_modes: + - ReadWriteMany + claim_name: calrissian-claim + name: calrissian-volume + persist: false + size: 50Gi + storage_class: standard + volume_mount: + mount_path: /calrissian + name: calrissian-volume + - access_modes: + - ReadWriteOnce + claim_name: workspace-claim + name: workspace-volume + persist: true + size: 50Gi + storage_class: standard + volume_mount: + mount_path: /workspace + name: workspace-volume +- config_maps: + - content: "alias ll=\"ls -l\"\nalias calrissian=\"/opt/conda/bin/calrissian --pod-nodeselectors\ + \ /etc/calrissian/pod-node-selector.yml --stdout /calrissian/results.json --max-ram\ + \ 16G --max-cores \"8\" --tmp-outdir-prefix /calrissian/tmp/ --outdir /calrissian/\"\ + \nalias cwltool=\"/opt/conda/bin/cwltool --podman\"\n. /home/jovyan/.bashrc\n\ + # >>> conda initialize >>>\n# !! Contents within this block are managed by 'conda\ + \ init' !!\n__conda_setup=\"$('/opt/conda/bin/conda' 'shell.bash' 'hook' 2>\ + \ /dev/null)\"\nif [ $? -eq 0 ]; then\n eval \"$__conda_setup\"\nelse\n \ + \ if [ -f \"/opt/conda/etc/profile.d/conda.sh\" ]; then\n . \"/opt/conda/etc/profile.d/conda.sh\"\ + \n else\n export PATH=\"/srv/conda/bin:$PATH\"\n fi\nfi\nunset\ + \ __conda_setup\n\nif [ -f \"/opt/conda/etc/profile.d/mamba.sh\" ]; then\n \ + \ . \"/opt/conda/etc/profile.d/mamba.sh\"\nfi\n# <<< conda initialize <<<\n" + default_mode: null + key: bash-rc + mount_path: /workspace/.bashrc + name: bash-rc + persist: true + readonly: true + default_url: null + definition: + default: false + description: null + display_name: Code Server Medium + kubespawner_override: + cpu_guarantee: null + cpu_limit: 4 + extra_resource_guarantees: {} + extra_resource_limits: {} + image: eoepca/pde-code-server:develop + mem_guarantee: null + mem_limit: 12G + slug: ellip_studio_coder_slug_m + groups: + - group-a + - group-b + id: profile_studio_coder2 + image_pull_secrets: [] + init_containers: [] + manifests: null + node_selector: {} + pod_env_vars: + CONDA_ENVS_PATH: ' /workspace/.envs' + HOME: /workspace + role_bindings: null + volumes: + - access_modes: + - ReadWriteMany + claim_name: calrissian-claim + name: calrissian-volume + persist: false + size: 50Gi + storage_class: standard + volume_mount: + mount_path: /calrissian + name: calrissian-volume + - access_modes: + - ReadWriteOnce + claim_name: workspace-claim + name: workspace-volume + persist: true + size: 50Gi + storage_class: standard + volume_mount: + mount_path: /workspace + name: workspace-volume +- config_maps: + - content: 'set -x + + + cd /workspace + + + git clone ''https://github.com/eoap/mastering-app-package.git'' + + + code-server --install-extension ms-python.python + + code-server --install-extension redhat.vscode-yaml + + code-server --install-extension sbg-rabix.benten-cwl + + code-server --install-extension ms-toolsai.jupyter + + + ln -s /workspace/.local/share/code-server/extensions /workspace/extensions + + + exit 0 + + ' + default_mode: '0660' + key: init + mount_path: /opt/init/.init.sh + name: init + persist: false + readonly: true + default_url: null + definition: + default: false + description: This profile is used to demonstrate the use of an init script + display_name: Coder demo init script + kubespawner_override: + cpu_guarantee: 1 + cpu_limit: 2 + extra_resource_guarantees: {} + extra_resource_limits: {} + image: eoepca/pde-code-server:develop + mem_guarantee: 4G + mem_limit: 6G + slug: eoepca_demo_init_script + groups: + - group-a + - group-b + id: profile_demo_init_script + image_pull_secrets: [] + init_containers: + - command: + - sh + - -c + - sh /opt/init/.init.sh + image: eoepca/pde-code-server:develop + name: init-file-on-volume + volume_mounts: + - mount_path: /workspace + name: workspace-volume + - mount_path: /opt/init/.init.sh + name: init + sub_path: init + manifests: null + node_selector: {} + pod_env_vars: + CODE_SERVER_WS: /workspace/mastering-app-package + CONDARC: /workspace/.condarc + CONDA_ENVS_PATH: /workspace/.envs + HOME: /workspace + XDG_RUNTIME_DIR: /workspace/.local + role_bindings: null + volumes: + - access_modes: + - ReadWriteMany + claim_name: calrissian-claim + name: calrissian-volume + persist: false + size: 50Gi + storage_class: standard + volume_mount: + mount_path: /calrissian + name: calrissian-volume + - access_modes: + - ReadWriteOnce + claim_name: workspace-claim + name: workspace-volume + persist: true + size: 50Gi + storage_class: standard + volume_mount: + mount_path: /workspace + name: workspace-volume +- config_maps: [] + default_url: null + definition: + default: false + description: Jupyter Lab with Python 3.11 + display_name: Jupyter Lab + kubespawner_override: + cpu_guarantee: 1 + cpu_limit: 2 + extra_resource_guarantees: {} + extra_resource_limits: {} + image: jupyter/scipy-notebook + mem_guarantee: 4G + mem_limit: 6G + slug: eoepca_jupyter_lab + groups: + - group-c + id: profile_jupyter_lab + image_pull_secrets: [] + init_containers: [] + manifests: null + node_selector: {} + pod_env_vars: + HOME: /workspace + XDG_CONFIG_HOME: /workspace/.config + XDG_RUNTIME_DIR: /workspace/.local + role_bindings: null + volumes: + - access_modes: + - ReadWriteOnce + claim_name: workspace-claim + name: workspace-volume + persist: true + size: 50Gi + storage_class: standard + volume_mount: + mount_path: /workspace + name: workspace-volume +- config_maps: [] + default_url: null + definition: + default: false + description: Jupyter Lab with Python 3.11 - demoes the use of an image pull secret + display_name: Jupyter Lab - profile 2 + kubespawner_override: + cpu_guarantee: 1 + cpu_limit: 2 + extra_resource_guarantees: {} + extra_resource_limits: {} + image: jupyter/scipy-notebook + mem_guarantee: 4G + mem_limit: 6G + slug: eoepca_jupyter_lab_2 + groups: + - group-c + id: profile_jupyter_lab_2 + image_pull_secrets: + - data: ewogICAgImF1dGhzIjogewogICAgICAgICJjci50ZXJyYWR1ZS5jb20iOiB7CiAgICAgICAgICAgICJ1c2VybmFtZSI6ICJyb2JvdCRlb2VwY2EtcGx1cy1ybyIsCiAgICAgICAgICAgICJwYXNzd29yZCI6ICJQMlE4TnkyZ0lHODhkZkxveXlLN05QVUZVbHJOekFZSiIsCiAgICAgICAgICAgICJlbWFpbCI6ICJlb2VwY2EtcGx1c0B0ZXJyYWR1ZS5jb20iLAogICAgICAgICAgICAiYXV0aCI6ICJjbTlpYjNRa1pXOWxjR05oTFhCc2RYTXRjbTg2VURKUk9FNTVNbWRKUnpnNFpHWk1iM2w1U3pkT1VGVkdWV3h5VG5wQldVbz0iCiAgICAgICAgfQogICAgfQp9 + name: cr-config + persist: false + init_containers: [] + manifests: null + node_selector: {} + pod_env_vars: + HOME: /workspace + XDG_CONFIG_HOME: /workspace/.config + XDG_RUNTIME_DIR: /workspace/.local + role_bindings: null + volumes: + - access_modes: + - ReadWriteOnce + claim_name: workspace-claim + name: workspace-volume + persist: true + size: 50Gi + storage_class: standard + volume_mount: + mount_path: /workspace + name: workspace-volume diff --git a/files/hub/jupyterhub_config.py b/files/hub/jupyterhub_config.py new file mode 100644 index 0000000..56395ea --- /dev/null +++ b/files/hub/jupyterhub_config.py @@ -0,0 +1,179 @@ +import os +import sys + +from tornado.httpclient import AsyncHTTPClient + + +from application_hub_context.app_hub_context import DefaultApplicationHubContext + + +configuration_directory = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, configuration_directory) + +from z2jh import ( + get_config, + get_name, + get_name_env, +) + +config_path = "/usr/local/etc/applicationhub/config.yml" + +namespace_prefix = "jupyter" + + +def custom_options_form(spawner): + + spawner.log.info("Configure profile list") + + namespace = f"{namespace_prefix}-{spawner.user.name}" + + workspace = DefaultApplicationHubContext( + namespace=namespace, + spawner=spawner, + config_path=config_path, + ) + + spawner.profile_list = workspace.get_profile_list() + + return spawner._options_form_default() + + +def pre_spawn_hook(spawner): + + profile_slug = spawner.user_options.get("profile", None) + + env = os.environ["JUPYTERHUB_ENV"].lower() + + spawner.environment["CALRISSIAN_POD_NAME"] = f"jupyter-{spawner.user.name}-{env}" + + spawner.log.info(f"Using profile slug {profile_slug}") + + namespace = f"{namespace_prefix}-{spawner.user.name}" + + workspace = DefaultApplicationHubContext( + namespace=namespace, spawner=spawner, config_path=config_path + ) + + workspace.initialise() + + profile_id = workspace.config_parser.get_profile_by_slug(slug=profile_slug).id + + default_url = workspace.config_parser.get_profile_default_url(profile_id=profile_id) + + if default_url: + spawner.log.info(f"Setting default url to {default_url}") + spawner.default_url = default_url + + +def post_stop_hook(spawner): + + namespace = f"jupyter-{spawner.user.name}" + + workspace = DefaultApplicationHubContext( + namespace=namespace, spawner=spawner, config_path=config_path + ) + spawner.log.info("Dispose in post stop hook") + workspace.dispose() + + +c.JupyterHub.default_url = "spawn" + + +# Configure JupyterHub to use the curl backend for making HTTP requests, +# rather than the pure-python implementations. The default one starts +# being too slow to make a large number of requests to the proxy API +# at the rate required. +AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient") + +c.ConfigurableHTTPProxy.api_url = ( + f'http://{get_name("proxy-api")}:{get_name_env("proxy-api", "_SERVICE_PORT")}' +) +# c.ConfigurableHTTPProxy.should_start = False + +# Don't wait at all before redirecting a spawning user to the progress page +c.JupyterHub.tornado_settings = { + "slow_spawn_timeout": 0, +} + +jupyterhub_env = os.environ["JUPYTERHUB_ENV"].upper() +jupyterhub_hub_host = "hub.jupyter" +jupyterhub_single_user_image = os.environ["JUPYTERHUB_SINGLE_USER_IMAGE_NOTEBOOKS"] + +# Authentication +c.LocalAuthenticator.create_system_users = True +c.Authenticator.admin_users = {"jovyan"} +# Deprecated +c.Authenticator.allowed_users = {"jovyan"} +c.JupyterHub.authenticator_class = "dummy" + +# HTTP Proxy auth token +c.ConfigurableHTTPProxy.auth_token = get_config("proxy.secretToken") +c.JupyterHub.cookie_secret_file = "/srv/jupyterhub/cookie_secret" +# Proxy config +c.JupyterHub.cleanup_servers = False +# Network +c.JupyterHub.allow_named_servers = True +c.JupyterHub.ip = "0.0.0.0" +c.JupyterHub.hub_ip = "0.0.0.0" +c.JupyterHub.hub_connect_ip = jupyterhub_hub_host +# Misc +c.JupyterHub.cleanup_servers = False + +# Culling +c.JupyterHub.services = [ + { + "name": "idle-culler", + "admin": True, + "command": [sys.executable, "-m", "jupyterhub_idle_culler", "--timeout=3600"], + } +] + +# Logs +c.JupyterHub.log_level = "DEBUG" + +# Spawner +c.JupyterHub.spawner_class = "kubespawner.KubeSpawner" +c.KubeSpawner.environment = { + "JUPYTER_ENABLE_LAB": "true", +} + +c.KubeSpawner.uid = 1001 +c.KubeSpawner.fs_gid = 100 +c.KubeSpawner.hub_connect_ip = jupyterhub_hub_host + +# SecurityContext +c.KubeSpawner.privileged = True +c.KubeSpawner.allow_privilege_escalation = True + +# ServiceAccount +c.KubeSpawner.service_account = "default" +c.KubeSpawner.start_timeout = 60 * 15 +c.KubeSpawner.image = jupyterhub_single_user_image +c.KubernetesSpawner.verify_ssl = True +c.KubeSpawner.pod_name_template = ( + "jupyter-{username}-{servername}-" + os.environ["JUPYTERHUB_ENV"].lower() +) + +# Namespace +c.KubeSpawner.namespace = "jupyter" + +# User namespace +c.KubeSpawner.enable_user_namespaces = True + +# Volumes +# volumes are managed by the pre_spawn_hook/post_stop_hook + +# TODO - move this value to the values.yaml file +c.KubeSpawner.image_pull_secrets = ["cr-config"] + +# custom options form +c.KubeSpawner.options_form = custom_options_form + +# hooks +c.KubeSpawner.pre_spawn_hook = pre_spawn_hook +c.KubeSpawner.post_stop_hook = post_stop_hook + +c.JupyterHub.template_paths = [ + "/opt/jupyterhub/template", + "/usr/local/share/jupyterhub/templates", +] diff --git a/files/hub/z2jh.py b/files/hub/z2jh.py new file mode 100644 index 0000000..57e463f --- /dev/null +++ b/files/hub/z2jh.py @@ -0,0 +1,121 @@ +""" +Utility methods for use in jupyterhub_config.py and dynamic subconfigs. + +Methods here can be imported by extraConfig in values.yaml +""" +# from collections import Mapping +from collections.abc import Mapping +from functools import lru_cache +import os + +import yaml + +# memoize so we only load config once +@lru_cache() +def _load_config(): + """Load the Helm chart configuration used to render the Helm templates of + the chart from a mounted k8s Secret, and merge in values from an optionally + mounted secret (hub.existingSecret).""" + + cfg = {} + for source in ("secret/values.yaml", "existing-secret/values.yaml"): + path = f"/usr/local/etc/jupyterhub/{source}" + if os.path.exists(path): + print(f"Loading {path}") + with open(path) as f: + values = yaml.safe_load(f) + cfg = _merge_dictionaries(cfg, values) + else: + print(f"No config at {path}") + return cfg + + +@lru_cache() +def _get_config_value(key): + """Load value from the k8s ConfigMap given a key.""" + + path = f"/usr/local/etc/jupyterhub/config/{key}" + if os.path.exists(path): + with open(path) as f: + return f.read() + else: + raise Exception(f"{path} not found!") + + +@lru_cache() +def get_secret_value(key, default="never-explicitly-set"): + """Load value from the user managed k8s Secret or the default k8s Secret + given a key.""" + + for source in ("existing-secret", "secret"): + path = f"/usr/local/etc/jupyterhub/{source}/{key}" + if os.path.exists(path): + with open(path) as f: + return f.read() + if default != "never-explicitly-set": + return default + raise Exception(f"{key} not found in either k8s Secret!") + + +def get_name(name): + """Returns the fullname of a resource given its short name""" + return _get_config_value(name) + + +def get_name_env(name, suffix=""): + """Returns the fullname of a resource given its short name along with a + suffix, converted to uppercase with dashes replaced with underscores. This + is useful to reference named services associated environment variables, such + as PROXY_PUBLIC_SERVICE_PORT.""" + env_key = _get_config_value(name) + suffix + env_key = env_key.upper().replace("-", "_") + return os.environ[env_key] + + +def _merge_dictionaries(a, b): + """Merge two dictionaries recursively. + + Simplified From https://stackoverflow.com/a/7205107 + """ + merged = a.copy() + for key in b: + if key in a: + if isinstance(a[key], Mapping) and isinstance(b[key], Mapping): + merged[key] = _merge_dictionaries(a[key], b[key]) + else: + merged[key] = b[key] + else: + merged[key] = b[key] + return merged + + +def get_config(key, default=None): + """ + Find a config item of a given name & return it + + Parses everything as YAML, so lists and dicts are available too + + get_config("a.b.c") returns config['a']['b']['c'] + """ + value = _load_config() + # resolve path in yaml + for level in key.split("."): + if not isinstance(value, dict): + # a parent is a scalar or null, + # can't resolve full path + return default + if level not in value: + return default + else: + value = value[level] + return value + + +def set_config_if_not_none(cparent, name, key): + """ + Find a config item of a given name, set the corresponding Jupyter + configuration item if not None + """ + data = get_config(key) + if data is not None: + setattr(cparent, name, data) diff --git a/files/theme/page.html b/files/theme/page.html new file mode 100644 index 0000000..5bec635 --- /dev/null +++ b/files/theme/page.html @@ -0,0 +1,5 @@ +{% extends "templates/page.html" %} + +{% set announcement = 'EOEPCA+ ApplicationHub demonstration instance (super cool)' %} + +{% block title %}ApplicationHub{% endblock %} diff --git a/files/theme/spawn.html b/files/theme/spawn.html new file mode 100644 index 0000000..8bb7896 --- /dev/null +++ b/files/theme/spawn.html @@ -0,0 +1,7 @@ +{% extends "templates/spawn.html" %} + +{% block heading %} +
+

Available EOEPCA+ demonstration applications

+
+{% endblock %} diff --git a/files/theme/spawn_pending.html b/files/theme/spawn_pending.html new file mode 100644 index 0000000..3cc1dfb --- /dev/null +++ b/files/theme/spawn_pending.html @@ -0,0 +1,94 @@ +{% extends "page.html" %} + +{% block main %} + +
+
+
+ {% block message %} +

Your EOEPCA application is starting up.

+

You will be redirected automatically when it's ready for you.

+ {% endblock %} +
+
+ 0% Complete +
+
+

+
+
+
+
+
+ Event log +
+
+
+
+
+ +{% endblock %} + +{% block script %} +{{ `{{ super() }} `}} + +{% endblock %} diff --git a/jupyterhub/files/hub/jupyterhub_config.py b/jupyterhub/files/hub/jupyterhub_config.py index 5e6fb9e..b97ec51 100644 --- a/jupyterhub/files/hub/jupyterhub_config.py +++ b/jupyterhub/files/hub/jupyterhub_config.py @@ -18,8 +18,15 @@ config_path = "/usr/local/etc/jupyterhub/config.yml" -namespace_prefix = "jupyter" +def get_namespace_prefix(): + env = os.environ["JUPYTERHUB_ENV"].lower() # Retrieve the JUPYTERHUB_ENV environment variable + return f"jupyter-{env}" # Dynamically generate the namespace prefix +def get_jupyterhub_hub_host(): + env = os.environ["JUPYTERHUB_HUB_HOST"].lower() + return f"hub.{env}" + +namespace_prefix = get_namespace_prefix() # Create dynamic namespace prefix def custom_options_form(spawner): @@ -44,7 +51,7 @@ def pre_spawn_hook(spawner): env = os.environ["JUPYTERHUB_ENV"].lower() - spawner.environment["CALRISSIAN_POD_NAME"] = f"jupyter-{spawner.user.name}-{env}" + spawner.environment["CALRISSIAN_POD_NAME"] = f"jupyter-{env}-{spawner.user.name}" spawner.log.info(f"Using profile slug {profile_slug}") @@ -67,7 +74,7 @@ def pre_spawn_hook(spawner): def post_stop_hook(spawner): - namespace = f"jupyter-{spawner.user.name}" + namespace = f"{namespace_prefix}-{spawner.user.name}" workspace = DefaultApplicationHubContext( namespace=namespace, spawner=spawner, config_path=config_path @@ -96,7 +103,8 @@ def post_stop_hook(spawner): } jupyterhub_env = os.environ["JUPYTERHUB_ENV"].upper() -jupyterhub_hub_host = "hub.jupyter" +jupyterhub_hub_host = get_jupyterhub_hub_host() + jupyterhub_single_user_image = os.environ["JUPYTERHUB_SINGLE_USER_IMAGE_NOTEBOOKS"] # Authentication @@ -151,11 +159,12 @@ def post_stop_hook(spawner): c.KubeSpawner.image = jupyterhub_single_user_image c.KubernetesSpawner.verify_ssl = True c.KubeSpawner.pod_name_template = ( - "jupyter-{username}-{servername}-" + os.environ["JUPYTERHUB_ENV"].lower() + "jupyter-" + os.environ["JUPYTERHUB_ENV"].lower() + "-{username}" ) # Namespace -c.KubeSpawner.namespace = "jupyter" +# Kubernetes namespace to spawn user pods in. +c.KubeSpawner.user_namespace_template = get_namespace_prefix() + "-{username}" # User namespace c.KubeSpawner.enable_user_namespaces = True diff --git a/jupyterhub/templates/hub/configmap-theme.yaml b/jupyterhub/templates/hub/configmap-theme.yaml index 8d744b7..cfbae26 100644 --- a/jupyterhub/templates/hub/configmap-theme.yaml +++ b/jupyterhub/templates/hub/configmap-theme.yaml @@ -13,7 +13,10 @@ data: z2jh.py: | multi line string content... */}} - {{- (.Files.Glob "files/theme/*").AsConfig | nindent 2 }} + # templates for the hub + page.html: {{ required "A valid .Values.pageTheme entry required!" (tpl (.Values.pageTheme | default (.Files.Get "files/theme/page.html")) . | quote ) }} + spawn_pending.html: {{ required "A valid .Values.spawnPendingTheme entry required!" (tpl (.Values.spawnPendingTheme | default (.Files.Get "files/theme/spawn_pending.html")) . | quote ) }} + spawn.html: {{ required "A valid .Values.spawnTheme entry required!" (tpl (.Values.spawnTheme | default (.Files.Get "files/theme/spawn.html")) . | quote) }} {{- /* Store away a checksum of the hook-image-puller daemonset so future upgrades diff --git a/jupyterhub/templates/hub/configmap.yaml b/jupyterhub/templates/hub/configmap.yaml index 128a6a0..08debe6 100644 --- a/jupyterhub/templates/hub/configmap.yaml +++ b/jupyterhub/templates/hub/configmap.yaml @@ -21,7 +21,11 @@ data: z2jh.py: | multi line string content... */}} - {{- (.Files.Glob "files/hub/*").AsConfig | nindent 2 }} + config.yml: {{ required "A valid .Values.appHubConfig entry required!" (tpl (.Values.appHubConfig | default (.Files.Get "files/hub/config.yml")) . | quote) }} + jupyterhub_config.py: {{ required "A valid .Values.jupHubConfig entry required!" (tpl (.Values.jupHubConfig | default (.Files.Get "files/hub/jupyterhub_config.py")) . | quote) }} + z2jh.py: {{ required "A valid .Values.z2jhConfig entry required!" (tpl (.Values.z2jhConfig | default (.Files.Get "files/hub/z2jh.py")) . | quote) }} + + {{- /* Store away a checksum of the hook-image-puller daemonset so future upgrades diff --git a/jupyterhub/values.schema.json b/jupyterhub/values.schema.json deleted file mode 100644 index cc589df..0000000 --- a/jupyterhub/values.schema.json +++ /dev/null @@ -1 +0,0 @@ -{"$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "additionalProperties": false, "required": ["imagePullSecrets", "hub", "proxy", "singleuser", "ingress", "prePuller", "custom", "cull", "debug", "rbac", "global"], "properties": {"fullnameOverride": {"type": ["string", "null"]}, "nameOverride": {"type": ["string", "null"]}, "imagePullSecret": {"type": "object", "required": ["create"], "if": {"properties": {"create": {"const": true}}}, "then": {"additionalProperties": false, "required": ["registry", "username", "password"], "description": "This is configuration to create a k8s Secret resource of `type:\nkubernetes.io/dockerconfigjson`, with credentials to pull images from a\nprivate image registry. If you opt to do so, it will be available for use\nby all pods in their respective `spec.imagePullSecrets` alongside other\nk8s Secrets defined in `imagePullSecrets` or the pod respective\n`...image.pullSecrets` configuration.\n\nIn other words, using this configuration option can automate both the\notherwise manual creation of a k8s Secret and the otherwise manual\nconfiguration to reference this k8s Secret in all the pods of the Helm\nchart.\n\n```sh\n# you won't need to create a k8s Secret manually...\nkubectl create secret docker-registry image-pull-secret \\\n --docker-server= \\\n --docker-username= \\\n --docker-email= \\\n --docker-password=\n```\n\nIf you just want to let all Pods reference an existing secret, use the\n[`imagePullSecrets`](schema_imagePullSecrets) configuration instead.\n", "properties": {"create": {"type": "boolean", "description": "Toggle the creation of the k8s Secret with provided credentials to\naccess a private image registry.\n"}, "automaticReferenceInjection": {"type": "boolean", "description": "Toggle the automatic reference injection of the created Secret to all\npods' `spec.imagePullSecrets` configuration.\n"}, "registry": {"type": "string", "description": "Name of the private registry you want to create a credential set for.\nIt will default to Docker Hub's image registry.\n\nExamples:\n - https://index.docker.io/v1/\n - quay.io\n - eu.gcr.io\n - alexmorreale.privatereg.net\n"}, "username": {"type": "string", "description": "Name of the user you want to use to connect to your private registry.\n\nFor external gcr.io, you will use the `_json_key`.\n\nExamples:\n - alexmorreale\n - alex@pfc.com\n - _json_key\n"}, "password": {"type": "string", "description": "Password for the private image registry's user.\n\nExamples:\n - plaintextpassword\n - abc123SECRETzyx098\n\nFor gcr.io registries the password will be a big JSON blob for a\nGoogle cloud service account, it should look something like below.\n\n```yaml\npassword: |-\n {\n \"type\": \"service_account\",\n \"project_id\": \"jupyter-se\",\n \"private_key_id\": \"f2ba09118a8d3123b3321bd9a7d6d0d9dc6fdb85\",\n ...\n }\n```\n"}, "email": {"type": ["string", "null"], "description": "Specification of an email is most often not required, but it is\nsupported.\n"}}}}, "imagePullSecrets": {"type": "array"}, "hub": {"type": "object", "additionalProperties": false, "required": ["baseUrl"], "properties": {"revisionHistoryLimit": {"type": ["integer", "null"], "minimum": 0}, "config": {"type": "object", "additionalProperties": true}, "extraFiles": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "object", "additionalProperties": false, "required": ["mountPath"], "oneOf": [{"required": ["data"]}, {"required": ["stringData"]}, {"required": ["binaryData"]}], "properties": {"mountPath": {"type": "string"}, "data": {"type": "object", "additionalProperties": true}, "stringData": {"type": "string"}, "binaryData": {"type": "string"}, "mode": {"type": "number"}}}}}, "baseUrl": {"type": "string"}, "command": {"type": "array"}, "args": {"type": "array"}, "cookieSecret": {"type": ["string", "null"]}, "image": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}, "pullPolicy": {"enum": [null, "", "IfNotPresent", "Always", "Never"]}, "pullSecrets": {"type": "array"}}}, "networkPolicy": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}, "ingress": {"type": "array"}, "egress": {"type": "array"}, "egressAllowRules": {"type": "object", "additionalProperties": false, "properties": {"cloudMetadataServer": {"type": "boolean"}, "dnsPortsPrivateIPs": {"type": "boolean"}, "nonPrivateIPs": {"type": "boolean"}, "privateIPs": {"type": "boolean"}}}, "interNamespaceAccessLabels": {"enum": ["accept", "ignore"]}, "allowedIngressPorts": {"type": "array"}}}, "db": {"type": "object", "additionalProperties": false, "properties": {"type": {"enum": ["sqlite-pvc", "sqlite-memory", "mysql", "postgres", "other"]}, "pvc": {"type": "object", "additionalProperties": false, "required": ["storage"], "properties": {"annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "selector": {"type": "object", "additionalProperties": true}, "storage": {"type": "string"}, "accessModes": {"type": "array", "items": {"type": ["string", "null"]}}, "storageClassName": {"type": ["string", "null"]}, "subPath": {"type": ["string", "null"]}}}, "upgrade": {"type": ["boolean", "null"]}, "url": {"type": ["string", "null"]}, "password": {"type": ["string", "null"]}}}, "labels": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "initContainers": {"type": "array"}, "extraEnv": {"type": ["object", "array"], "additionalProperties": true}, "extraConfig": {"type": "object", "additionalProperties": true}, "fsGid": {"type": ["integer", "null"], "minimum": 0}, "service": {"type": "object", "additionalProperties": false, "properties": {"type": {"enum": ["ClusterIP", "NodePort", "LoadBalancer", "ExternalName"]}, "ports": {"type": "object", "additionalProperties": false, "properties": {"nodePort": {"type": ["integer", "null"], "minimum": 0}}}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "extraPorts": {"type": "array"}, "loadBalancerIP": {"type": ["string", "null"]}}}, "pdb": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}, "maxUnavailable": {"type": ["integer", "null"]}, "minAvailable": {"type": ["integer", "null"]}}}, "existingSecret": {"type": ["string", "null"]}, "nodeSelector": {"type": "object", "additionalProperties": true}, "tolerations": {"type": "array"}, "activeServerLimit": {"type": ["integer", "null"]}, "allowNamedServers": {"type": ["boolean", "null"]}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "authenticatePrometheus": {"type": ["boolean", "null"]}, "concurrentSpawnLimit": {"type": ["integer", "null"]}, "consecutiveFailureLimit": {"type": ["integer", "null"]}, "podSecurityContext": {"additionalProperties": true}, "containerSecurityContext": {"type": "object", "additionalProperties": true}, "deploymentStrategy": {"type": "object", "additionalProperties": false, "properties": {"rollingUpdate": {"type": ["string", "null"]}, "type": {"type": ["string", "null"]}}}, "extraContainers": {"type": "array"}, "extraVolumeMounts": {"type": "array"}, "extraVolumes": {"type": "array"}, "livenessProbe": {"type": "object", "additionalProperties": true, "required": ["enabled"], "if": {"properties": {"enabled": {"const": true}}}, "then": {"description": "This config option is like the k8s native specification of a\ncontainer probe, except that it also supports an `enabled` boolean\nflag.\n\nSee [the k8s\ndocumentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#probe-v1-core)\nfor more details.\n"}}, "readinessProbe": {"type": "object", "additionalProperties": true, "required": ["enabled"], "if": {"properties": {"enabled": {"const": true}}}, "then": {"description": "This config option is like the k8s native specification of a\ncontainer probe, except that it also supports an `enabled` boolean\nflag.\n\nSee [the k8s\ndocumentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#probe-v1-core)\nfor more details.\n"}}, "namedServerLimitPerUser": {"type": ["integer", "null"]}, "redirectToServer": {"type": ["boolean", "null"]}, "resources": {"type": "object", "additionalProperties": true}, "lifecycle": {"type": "object", "additionalProperties": false, "properties": {"postStart": {"type": "object", "additionalProperties": true}, "preStop": {"type": "object", "additionalProperties": true}}}, "services": {"type": "object", "additionalProperties": true, "properties": {"name": {"type": "string"}, "admin": {"type": "boolean"}, "command": {"type": ["string", "array"]}, "url": {"type": "string"}, "api_token": {"type": ["string", "null"]}, "apiToken": {"type": ["string", "null"]}}}, "loadRoles": {"type": "object", "additionalProperties": true}, "shutdownOnLogout": {"type": ["boolean", "null"]}, "templatePaths": {"type": "array"}, "templateVars": {"type": "object", "additionalProperties": true}, "serviceAccount": {"type": "object", "required": ["create"], "additionalProperties": false, "properties": {"create": {"type": "boolean"}, "name": {"type": ["string", "null"]}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}}}, "extraPodSpec": {"type": "object", "additionalProperties": true}}}, "proxy": {"type": "object", "additionalProperties": false, "properties": {"chp": {"type": "object", "additionalProperties": false, "properties": {"revisionHistoryLimit": {"type": ["integer", "null"], "minimum": 0}, "networkPolicy": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}, "ingress": {"type": "array"}, "egress": {"type": "array"}, "egressAllowRules": {"type": "object", "additionalProperties": false, "properties": {"cloudMetadataServer": {"type": "boolean"}, "dnsPortsPrivateIPs": {"type": "boolean"}, "nonPrivateIPs": {"type": "boolean"}, "privateIPs": {"type": "boolean"}}}, "interNamespaceAccessLabels": {"enum": ["accept", "ignore"]}, "allowedIngressPorts": {"type": "array"}}}, "extraCommandLineFlags": {"type": "array"}, "extraEnv": {"type": ["object", "array"], "additionalProperties": true}, "pdb": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}, "maxUnavailable": {"type": ["integer", "null"]}, "minAvailable": {"type": ["integer", "null"]}}}, "nodeSelector": {"type": "object", "additionalProperties": true}, "tolerations": {"type": "array"}, "containerSecurityContext": {"type": "object", "additionalProperties": true}, "image": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}, "pullPolicy": {"enum": [null, "", "IfNotPresent", "Always", "Never"]}, "pullSecrets": {"type": "array"}}}, "livenessProbe": {"type": "object", "additionalProperties": true, "required": ["enabled"], "if": {"properties": {"enabled": {"const": true}}}, "then": {"description": "This config option is like the k8s native specification of a\ncontainer probe, except that it also supports an `enabled` boolean\nflag.\n\nSee [the k8s\ndocumentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#probe-v1-core)\nfor more details.\n"}}, "readinessProbe": {"type": "object", "additionalProperties": true, "required": ["enabled"], "if": {"properties": {"enabled": {"const": true}}}, "then": {"description": "This config option is like the k8s native specification of a\ncontainer probe, except that it also supports an `enabled` boolean\nflag.\n\nSee [the k8s\ndocumentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#probe-v1-core)\nfor more details.\n"}}, "resources": {"type": "object", "additionalProperties": true}, "defaultTarget": {"type": ["string", "null"]}, "errorTarget": {"type": ["string", "null"]}, "extraPodSpec": {"type": "object", "additionalProperties": true}}}, "secretToken": {"type": ["string", "null"]}, "service": {"type": "object", "additionalProperties": false, "properties": {"type": {"enum": ["ClusterIP", "NodePort", "LoadBalancer", "ExternalName"]}, "labels": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "nodePorts": {"type": "object", "additionalProperties": false, "properties": {"http": {"type": ["integer", "null"]}, "https": {"type": ["integer", "null"]}}}, "disableHttpPort": {"type": "boolean"}, "extraPorts": {"type": "array"}, "loadBalancerIP": {"type": ["string", "null"]}, "loadBalancerSourceRanges": {"type": "array"}}}, "https": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": ["boolean", "null"]}, "type": {"enum": [null, "", "letsencrypt", "manual", "offload", "secret"]}, "letsencrypt": {"type": "object", "additionalProperties": false, "properties": {"contactEmail": {"type": ["string", "null"]}, "acmeServer": {"type": ["string", "null"]}}}, "manual": {"type": "object", "additionalProperties": false, "properties": {"key": {"type": ["string", "null"]}, "cert": {"type": ["string", "null"]}}}, "secret": {"type": "object", "additionalProperties": false, "properties": {"name": {"type": ["string", "null"]}, "key": {"type": ["string", "null"]}, "crt": {"type": ["string", "null"]}}}, "hosts": {"type": "array"}}}, "traefik": {"type": "object", "additionalProperties": false, "properties": {"revisionHistoryLimit": {"type": ["integer", "null"], "minimum": 0}, "labels": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "networkPolicy": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}, "ingress": {"type": "array"}, "egress": {"type": "array"}, "egressAllowRules": {"type": "object", "additionalProperties": false, "properties": {"cloudMetadataServer": {"type": "boolean"}, "dnsPortsPrivateIPs": {"type": "boolean"}, "nonPrivateIPs": {"type": "boolean"}, "privateIPs": {"type": "boolean"}}}, "interNamespaceAccessLabels": {"enum": ["accept", "ignore"]}, "allowedIngressPorts": {"type": "array"}}}, "extraInitContainers": {"type": "array"}, "extraEnv": {"type": ["object", "array"], "additionalProperties": true}, "pdb": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}, "maxUnavailable": {"type": ["integer", "null"]}, "minAvailable": {"type": ["integer", "null"]}}}, "nodeSelector": {"type": "object", "additionalProperties": true}, "tolerations": {"type": "array"}, "containerSecurityContext": {"type": "object", "additionalProperties": true}, "extraDynamicConfig": {"type": "object", "additionalProperties": true}, "extraPorts": {"type": "array"}, "extraStaticConfig": {"type": "object", "additionalProperties": true}, "extraVolumes": {"type": "array"}, "extraVolumeMounts": {"type": "array"}, "hsts": {"type": "object", "additionalProperties": false, "required": ["includeSubdomains", "maxAge", "preload"], "properties": {"includeSubdomains": {"type": "boolean"}, "maxAge": {"type": "integer"}, "preload": {"type": "boolean"}}}, "image": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}, "pullPolicy": {"enum": [null, "", "IfNotPresent", "Always", "Never"]}, "pullSecrets": {"type": "array"}}}, "resources": {"type": "object", "additionalProperties": true}, "serviceAccount": {"type": "object", "required": ["create"], "additionalProperties": false, "properties": {"create": {"type": "boolean"}, "name": {"type": ["string", "null"]}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}}}, "extraPodSpec": {"type": "object", "additionalProperties": true}}}, "labels": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "deploymentStrategy": {"type": "object", "additionalProperties": false, "properties": {"rollingUpdate": {"type": ["string", "null"]}, "type": {"type": ["string", "null"]}}}, "secretSync": {"type": "object", "additionalProperties": false, "properties": {"containerSecurityContext": {"type": "object", "additionalProperties": true}, "image": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}, "pullPolicy": {"enum": [null, "", "IfNotPresent", "Always", "Never"]}, "pullSecrets": {"type": "array"}}}, "resources": {"type": "object", "additionalProperties": true}}}}}, "singleuser": {"type": "object", "additionalProperties": false, "properties": {"networkPolicy": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}, "ingress": {"type": "array"}, "egress": {"type": "array"}, "egressAllowRules": {"type": "object", "additionalProperties": false, "properties": {"cloudMetadataServer": {"type": "boolean"}, "dnsPortsPrivateIPs": {"type": "boolean"}, "nonPrivateIPs": {"type": "boolean"}, "privateIPs": {"type": "boolean"}}}, "interNamespaceAccessLabels": {"enum": ["accept", "ignore"]}, "allowedIngressPorts": {"type": "array"}}}, "podNameTemplate": {"type": ["string", "null"]}, "cpu": {"type": "object", "additionalProperties": false, "properties": {"limit": {"type": ["number", "null"]}, "guarantee": {"type": ["number", "null"]}}}, "memory": {"type": "object", "additionalProperties": false, "properties": {"limit": {"type": ["number", "string", "null"]}, "guarantee": {"type": ["number", "string", "null"]}}}, "image": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}, "pullPolicy": {"enum": [null, "", "IfNotPresent", "Always", "Never"]}, "pullSecrets": {"type": "array"}}}, "initContainers": {"type": "array"}, "profileList": {"type": "array"}, "extraFiles": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "object", "additionalProperties": false, "required": ["mountPath"], "oneOf": [{"required": ["data"]}, {"required": ["stringData"]}, {"required": ["binaryData"]}], "properties": {"mountPath": {"type": "string"}, "data": {"type": "object", "additionalProperties": true}, "stringData": {"type": "string"}, "binaryData": {"type": "string"}, "mode": {"type": "number"}}}}}, "extraEnv": {"type": ["object", "array"], "additionalProperties": true}, "nodeSelector": {"type": "object", "additionalProperties": true}, "extraTolerations": {"type": "array"}, "extraNodeAffinity": {"type": "object", "additionalProperties": false, "properties": {"required": {"type": "array"}, "preferred": {"type": "array"}}}, "extraPodAffinity": {"type": "object", "additionalProperties": false, "properties": {"required": {"type": "array"}, "preferred": {"type": "array"}}}, "extraPodAntiAffinity": {"type": "object", "additionalProperties": false, "properties": {"required": {"type": "array"}, "preferred": {"type": "array"}}}, "cloudMetadata": {"type": "object", "additionalProperties": false, "properties": {"blockWithIptables": {"type": "boolean"}, "ip": {"type": "string"}}}, "cmd": {"type": ["array", "string", "null"]}, "defaultUrl": {"type": ["string", "null"]}, "events": {"type": ["boolean", "null"]}, "extraAnnotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "extraContainers": {"type": "array"}, "extraLabels": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "extraPodConfig": {"type": "object", "additionalProperties": true}, "extraResource": {"type": "object", "additionalProperties": false, "properties": {"guarantees": {"type": "object", "additionalProperties": true}, "limits": {"type": "object", "additionalProperties": true}}}, "fsGid": {"type": ["integer", "null"]}, "lifecycleHooks": {"type": "object", "additionalProperties": false, "properties": {"postStart": {"type": "object", "additionalProperties": true}, "preStop": {"type": "object", "additionalProperties": true}}}, "networkTools": {"type": "object", "additionalProperties": false, "properties": {"image": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}, "pullPolicy": {"enum": [null, "", "IfNotPresent", "Always", "Never"]}, "pullSecrets": {"type": "array"}}}, "resources": {"type": "object", "additionalProperties": true}}}, "serviceAccountName": {"type": ["string", "null"]}, "startTimeout": {"type": ["integer", "null"]}, "storage": {"type": "object", "additionalProperties": false, "required": ["type", "homeMountPath"], "properties": {"capacity": {"type": ["string", "null"]}, "dynamic": {"type": "object", "additionalProperties": false, "properties": {"pvcNameTemplate": {"type": ["string", "null"]}, "storageAccessModes": {"type": "array", "items": {"type": ["string", "null"]}}, "storageClass": {"type": ["string", "null"]}, "volumeNameTemplate": {"type": ["string", "null"]}}}, "extraLabels": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "extraVolumeMounts": {"type": "array"}, "extraVolumes": {"type": "array"}, "homeMountPath": {"type": "string"}, "static": {"type": "object", "additionalProperties": false, "properties": {"pvcName": {"type": ["string", "null"]}, "subPath": {"type": ["string", "null"]}}}, "type": {"enum": ["dynamic", "static", "none"]}}}, "allowPrivilegeEscalation": {"type": ["boolean", "null"]}, "uid": {"type": ["integer", "null"]}}}, "scheduling": {"type": "object", "additionalProperties": false, "properties": {"userScheduler": {"type": "object", "additionalProperties": false, "required": ["enabled", "plugins", "pluginConfig", "logLevel"], "properties": {"enabled": {"type": "boolean"}, "revisionHistoryLimit": {"type": ["integer", "null"], "minimum": 0}, "replicas": {"type": "integer"}, "image": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}, "pullPolicy": {"enum": [null, "", "IfNotPresent", "Always", "Never"]}, "pullSecrets": {"type": "array"}}}, "pdb": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}, "maxUnavailable": {"type": ["integer", "null"]}, "minAvailable": {"type": ["integer", "null"]}}}, "nodeSelector": {"type": "object", "additionalProperties": true}, "tolerations": {"type": "array"}, "labels": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "containerSecurityContext": {"type": "object", "additionalProperties": true}, "logLevel": {"type": "integer"}, "plugins": {"type": "object", "additionalProperties": true}, "pluginConfig": {"type": "array"}, "resources": {"type": "object", "additionalProperties": true}, "serviceAccount": {"type": "object", "required": ["create"], "additionalProperties": false, "properties": {"create": {"type": "boolean"}, "name": {"type": ["string", "null"]}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}}}, "extraPodSpec": {"type": "object", "additionalProperties": true}}}, "podPriority": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}, "globalDefault": {"type": "boolean"}, "defaultPriority": {"type": "integer"}, "imagePullerPriority": {"type": "integer"}, "userPlaceholderPriority": {"type": "integer"}}}, "userPlaceholder": {"type": "object", "additionalProperties": false, "properties": {"enabled": {"type": "boolean"}, "image": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}, "pullPolicy": {"enum": [null, "", "IfNotPresent", "Always", "Never"]}, "pullSecrets": {"type": "array"}}}, "revisionHistoryLimit": {"type": ["integer", "null"], "minimum": 0}, "replicas": {"type": "integer"}, "labels": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "resources": {"type": "object", "additionalProperties": true}, "containerSecurityContext": {"type": "object", "additionalProperties": true}}}, "corePods": {"type": "object", "additionalProperties": false, "properties": {"tolerations": {"type": "array"}, "nodeAffinity": {"type": "object", "additionalProperties": false, "properties": {"matchNodePurpose": {"enum": ["ignore", "prefer", "require"]}}}}}, "userPods": {"type": "object", "additionalProperties": false, "properties": {"tolerations": {"type": "array"}, "nodeAffinity": {"type": "object", "additionalProperties": false, "properties": {"matchNodePurpose": {"enum": ["ignore", "prefer", "require"]}}}}}}}, "ingress": {"type": "object", "additionalProperties": false, "required": ["enabled"], "properties": {"enabled": {"type": "boolean"}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "ingressClassName": {"type": ["string", "null"]}, "hosts": {"type": "array"}, "pathSuffix": {"type": ["string", "null"]}, "pathType": {"enum": ["Prefix", "Exact", "ImplementationSpecific"]}, "tls": {"type": "array"}}}, "prePuller": {"type": "object", "additionalProperties": false, "required": ["hook", "continuous"], "properties": {"revisionHistoryLimit": {"type": ["integer", "null"], "minimum": 0}, "labels": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}, "resources": {"type": "object", "additionalProperties": true}, "extraTolerations": {"type": "array"}, "hook": {"type": "object", "additionalProperties": false, "required": ["enabled"], "properties": {"enabled": {"type": "boolean"}, "pullOnlyOnChanges": {"type": "boolean"}, "podSchedulingWaitDuration": {"type": "integer"}, "nodeSelector": {"type": "object", "additionalProperties": true}, "tolerations": {"type": "array"}, "containerSecurityContext": {"type": "object", "additionalProperties": true}, "image": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}, "pullPolicy": {"enum": [null, "", "IfNotPresent", "Always", "Never"]}, "pullSecrets": {"type": "array"}}}, "resources": {"type": "object", "additionalProperties": true}, "serviceAccount": {"type": "object", "required": ["create"], "additionalProperties": false, "properties": {"create": {"type": "boolean"}, "name": {"type": ["string", "null"]}, "annotations": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "string"}}}}}}}, "continuous": {"type": "object", "additionalProperties": false, "required": ["enabled"], "properties": {"enabled": {"type": "boolean"}}}, "pullProfileListImages": {"type": "boolean"}, "extraImages": {"type": "object", "additionalProperties": false, "patternProperties": {".*": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}}}}}, "containerSecurityContext": {"type": "object", "additionalProperties": true}, "pause": {"type": "object", "additionalProperties": false, "properties": {"containerSecurityContext": {"type": "object", "additionalProperties": true}, "image": {"type": "object", "additionalProperties": false, "required": ["name", "tag"], "properties": {"name": {"type": "string"}, "tag": {"type": "string"}, "pullPolicy": {"enum": [null, "", "IfNotPresent", "Always", "Never"]}, "pullSecrets": {"type": "array"}}}}}}}, "custom": {"type": "object", "additionalProperties": true}, "cull": {"type": "object", "additionalProperties": false, "required": ["enabled"], "properties": {"enabled": {"type": "boolean"}, "users": {"type": ["boolean", "null"]}, "adminUsers": {"type": ["boolean", "null"]}, "removeNamedServers": {"type": ["boolean", "null"]}, "timeout": {"type": ["integer", "null"]}, "every": {"type": ["integer", "null"]}, "concurrency": {"type": ["integer", "null"]}, "maxAge": {"type": ["integer", "null"]}}}, "debug": {"type": "object", "additionalProperties": false, "required": ["enabled"], "properties": {"enabled": {"type": "boolean"}}}, "rbac": {"type": "object", "additionalProperties": false, "required": ["create"], "properties": {"enabled": {"type": "boolean"}, "create": {"type": "boolean"}}}, "global": {"type": "object", "additionalProperties": true, "properties": {"safeToShowValues": {"type": "boolean"}}}}} diff --git a/skaffold.yaml b/skaffold.yaml index 7cca29e..d4ddbee 100644 --- a/skaffold.yaml +++ b/skaffold.yaml @@ -14,16 +14,22 @@ profiles: namespace: jupyter createNamespace: true valuesFiles: - - jupyterhub/values-minikube.yaml + - demo-values.yaml setValueTemplates: hub.image.name: "{{.IMAGE_REPO_hubimage}}/{{.IMAGE_TAG_hubimage}}@{{.IMAGE_DIGEST_hubimage}}" + setFiles: { + appHubConfig: ./files/hub/config.yml, + pageTheme: ./files/theme/page.html, + spawnPendingTheme: ./files/theme/spawn_pending.html, + spawnTheme: ./files/theme/spawn.html + } + manifests: rawYaml: - sk-k8s/cluster-role-binding.yaml - sk-k8s/script.yaml - sk-k8s/job.yaml - portForward: - resourceType: service resourceName: proxy-public