From bae31c95ba3c67f4647e12f9f45390f3e24cdd96 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Thu, 1 Aug 2024 21:40:29 -0700 Subject: [PATCH 01/26] Initial setup for AWS Thicket Tutorial --- aws/cluster-autoscaler-autodiscover.yaml | 180 +++++++++++ aws/config-aws.yaml | 62 ++++ aws/eksctl-config.yaml | 110 +++++++ aws/eksctl-radiuss-2024.yaml | 79 +++++ aws/storageclass.yaml | 7 + docker/Dockerfile.hub | 9 + docker/Dockerfile.init | 13 + Dockerfile.local => docker/Dockerfile.spawn | 42 ++- {docker_scripts => docker}/entrypoint.sh | 0 docker/init-entrypoint.sh | 11 + docker/jupyter-launcher.yaml | 131 ++++++++ {docker_scripts => docker}/run_all.sh | 0 {docker_scripts => docker}/start.sh | 0 environment.yml | 113 +++++++ gcp/config.yaml | 62 ++++ thicket-logo.png | Bin 0 -> 122222 bytes tmp/requirements-cloud.txt | 339 ++++++++++++++++++++ requirements.txt => tmp/requirements.txt | 0 18 files changed, 1149 insertions(+), 9 deletions(-) create mode 100644 aws/cluster-autoscaler-autodiscover.yaml create mode 100644 aws/config-aws.yaml create mode 100644 aws/eksctl-config.yaml create mode 100644 aws/eksctl-radiuss-2024.yaml create mode 100644 aws/storageclass.yaml create mode 100644 docker/Dockerfile.hub create mode 100644 docker/Dockerfile.init rename Dockerfile.local => docker/Dockerfile.spawn (51%) rename {docker_scripts => docker}/entrypoint.sh (100%) create mode 100644 docker/init-entrypoint.sh create mode 100644 docker/jupyter-launcher.yaml rename {docker_scripts => docker}/run_all.sh (100%) rename {docker_scripts => docker}/start.sh (100%) create mode 100644 environment.yml create mode 100644 gcp/config.yaml create mode 100644 thicket-logo.png create mode 100644 tmp/requirements-cloud.txt rename requirements.txt => tmp/requirements.txt (100%) diff --git a/aws/cluster-autoscaler-autodiscover.yaml b/aws/cluster-autoscaler-autodiscover.yaml new file mode 100644 index 00000000..27576f65 --- /dev/null +++ b/aws/cluster-autoscaler-autodiscover.yaml @@ -0,0 +1,180 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler + name: cluster-autoscaler + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cluster-autoscaler + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +rules: + - apiGroups: [""] + resources: ["events", "endpoints"] + verbs: ["create", "patch"] + - apiGroups: [""] + resources: ["pods/eviction"] + verbs: ["create"] + - apiGroups: [""] + resources: ["pods/status"] + verbs: ["update"] + - apiGroups: [""] + resources: ["endpoints"] + resourceNames: ["cluster-autoscaler"] + verbs: ["get", "update"] + - apiGroups: [""] + resources: ["nodes"] + verbs: ["watch", "list", "get", "update"] + - apiGroups: [""] + resources: + - "namespaces" + - "pods" + - "services" + - "replicationcontrollers" + - "persistentvolumeclaims" + - "persistentvolumes" + verbs: ["watch", "list", "get"] + - apiGroups: ["extensions"] + resources: ["replicasets", "daemonsets"] + verbs: ["watch", "list", "get"] + - apiGroups: ["policy"] + resources: ["poddisruptionbudgets"] + verbs: ["watch", "list"] + - apiGroups: ["apps"] + resources: ["statefulsets", "replicasets", "daemonsets"] + verbs: ["watch", "list", "get"] + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses", "csinodes", "csidrivers", "csistoragecapacities"] + verbs: ["watch", "list", "get"] + - apiGroups: ["batch", "extensions"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "patch"] + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["create"] + - apiGroups: ["coordination.k8s.io"] + resourceNames: ["cluster-autoscaler"] + resources: ["leases"] + verbs: ["get", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["create", "list", "watch"] + - apiGroups: [""] + resources: ["configmaps"] + resourceNames: ["cluster-autoscaler-status", "cluster-autoscaler-priority-expander"] + verbs: ["delete", "get", "update", "watch"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cluster-autoscaler + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-autoscaler +subjects: + - kind: ServiceAccount + name: cluster-autoscaler + namespace: kube-system + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: cluster-autoscaler +subjects: + - kind: ServiceAccount + name: cluster-autoscaler + namespace: kube-system + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + app: cluster-autoscaler +spec: + replicas: 1 + selector: + matchLabels: + app: cluster-autoscaler + template: + metadata: + labels: + app: cluster-autoscaler + annotations: + prometheus.io/scrape: 'true' + prometheus.io/port: '8085' + spec: + priorityClassName: system-cluster-critical + securityContext: + runAsNonRoot: true + runAsUser: 65534 + fsGroup: 65534 + seccompProfile: + type: RuntimeDefault + serviceAccountName: cluster-autoscaler + containers: + - image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.26.2 + name: cluster-autoscaler + resources: + limits: + cpu: 100m + memory: 600Mi + requests: + cpu: 100m + memory: 600Mi + command: + - ./cluster-autoscaler + - --v=4 + - --stderrthreshold=info + - --cloud-provider=aws + - --skip-nodes-with-local-storage=false + - --expander=least-waste + - --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/jupyterhub + volumeMounts: + - name: ssl-certs + mountPath: /etc/ssl/certs/ca-certificates.crt # /etc/ssl/certs/ca-bundle.crt for Amazon Linux Worker Nodes + readOnly: true + imagePullPolicy: "Always" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumes: + - name: ssl-certs + hostPath: + path: "/etc/ssl/certs/ca-bundle.crt" \ No newline at end of file diff --git a/aws/config-aws.yaml b/aws/config-aws.yaml new file mode 100644 index 00000000..c5cb9594 --- /dev/null +++ b/aws/config-aws.yaml @@ -0,0 +1,62 @@ +# A few notes! +# The hub -> authentic class defaults to "dummy" +# We shouldn't need any image pull secrets assuming public +# There is a note about the database being a sqlite pvc +# (and a TODO for better solution for Kubernetes) + +# This is the concurrent spawn limit, likely should be increased (deafults to 64) +hub: + concurrentSpawnLimit: 10 + config: + DummyAuthenticator: + password: butter + JupyterHub: + admin_access: true + authenticator_class: dummy + + # This is the image I built based off of jupyterhub/k8s-hub, 3.0.2 at time of writing this + image: + name: ghcr.io/flux-framework/flux-jupyter-hub + tag: "riken-2024" + pullPolicy: Always + +# https://z2jh.jupyter.org/en/latest/administrator/optimization.html#scaling-up-in-time-user-placeholders +scheduling: + podPriority: + enabled: true + userPlaceholder: + # Specify 3 dummy user pods will be used as placeholders + replicas: 3 + +# This is the "spawn" image +singleuser: + image: + name: ghcr.io/flux-framework/flux-jupyter-spawn + tag: "riken-2024" + pullPolicy: Always + cpu: + limit: 1 + memory: + limit: '4G' + cmd: /entrypoint.sh + + # This runs as the root user, who clones and changes ownership to uid 1000 + initContainers: + - name: init-myservice + image: ghcr.io/flux-framework/flux-jupyter-init:riken-2024 + command: ["/entrypoint.sh"] + volumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan + + # This is how we get the tutorial files added + storage: + type: none + # gitRepo volume is deprecated so we need another way + # https://kubernetes.io/docs/concepts/storage/volumes/#gitrepo + extraVolumes: + - name: flux-tutorial + emptyDir: {} + extraVolumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan \ No newline at end of file diff --git a/aws/eksctl-config.yaml b/aws/eksctl-config.yaml new file mode 100644 index 00000000..c5f0523e --- /dev/null +++ b/aws/eksctl-config.yaml @@ -0,0 +1,110 @@ +# https://www.arhea.net/posts/2020-06-18-jupyterhub-amazon-eks +apiVersion: eksctl.io/v1alpha5 +kind: ClusterConfig +metadata: + name: jupyterhub + region: us-east-2 + +iam: + withOIDC: true + serviceAccounts: + - metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + aws-usage: "cluster-ops" + app.kubernetes.io/name: cluster-autoscaler + + # https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/README.md + attachPolicy: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "autoscaling:DescribeAutoScalingGroups" + - "autoscaling:DescribeAutoScalingInstances" + - "autoscaling:DescribeLaunchConfigurations" + - "autoscaling:DescribeTags" + - "autoscaling:SetDesiredCapacity" + - "autoscaling:TerminateInstanceInAutoScalingGroup" + - "ec2:DescribeLaunchTemplateVersions" + Resource: '*' + + - metadata: + name: ebs-csi-controller-sa + namespace: kube-system + labels: + aws-usage: "cluster-ops" + app.kubernetes.io/name: aws-ebs-csi-driver + attachPolicy: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "ec2:AttachVolume" + - "ec2:CreateSnapshot" + - "ec2:CreateTags" + - "ec2:CreateVolume" + - "ec2:DeleteSnapshot" + - "ec2:DeleteTags" + - "ec2:DeleteVolume" + - "ec2:DescribeInstances" + - "ec2:DescribeSnapshots" + - "ec2:DescribeTags" + - "ec2:DescribeVolumes" + - "ec2:DetachVolume" + Resource: '*' + +availabilityZones: ["us-east-2a", "us-east-2b", "us-east-2c"] +managedNodeGroups: + - name: ng-us-east-2a + iam: + withAddonPolicies: + autoScaler: true + instanceType: m5.large + volumeSize: 30 + desiredCapacity: 1 + minSize: 1 + maxSize: 3 + privateNetworking: true + availabilityZones: + - us-east-2a + # I didn't set this, but I know it's been an issue + # propagateASGTags: true + tags: + k8s.io/cluster-autoscaler/enabled: "true" + k8s.io/cluster-autoscaler/jupyterhub: "owned" + + - name: ng-us-east-2b + iam: + withAddonPolicies: + autoScaler: true + instanceType: m5.large + volumeSize: 30 + desiredCapacity: 1 + minSize: 1 + maxSize: 3 + privateNetworking: true + availabilityZones: + - us-east-2b + # propagateASGTags: true + tags: + k8s.io/cluster-autoscaler/enabled: "true" + k8s.io/cluster-autoscaler/jupyterhub: "owned" + + - name: ng-us-east-2c + iam: + withAddonPolicies: + autoScaler: true + instanceType: m5.large + volumeSize: 30 + desiredCapacity: 1 + minSize: 1 + maxSize: 3 + privateNetworking: true + availabilityZones: + - us-east-2c + # propagateASGTags: true + tags: + k8s.io/cluster-autoscaler/enabled: "true" + k8s.io/cluster-autoscaler/jupyterhub: "owned" \ No newline at end of file diff --git a/aws/eksctl-radiuss-2024.yaml b/aws/eksctl-radiuss-2024.yaml new file mode 100644 index 00000000..7f424951 --- /dev/null +++ b/aws/eksctl-radiuss-2024.yaml @@ -0,0 +1,79 @@ +# https://www.arhea.net/posts/2020-06-18-jupyterhub-amazon-eks +apiVersion: eksctl.io/v1alpha5 +kind: ClusterConfig +metadata: + name: jupyterhub + region: us-east-1 + +iam: + withOIDC: true + serviceAccounts: + - metadata: + name: ebs-csi-controller-sa + namespace: kube-system + labels: + aws-usage: "cluster-ops" + app.kubernetes.io/name: aws-ebs-csi-driver + attachPolicy: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "ec2:AttachVolume" + - "ec2:CreateSnapshot" + - "ec2:CreateTags" + - "ec2:CreateVolume" + - "ec2:DeleteSnapshot" + - "ec2:DeleteTags" + - "ec2:DeleteVolume" + - "ec2:DescribeInstances" + - "ec2:DescribeSnapshots" + - "ec2:DescribeTags" + - "ec2:DescribeVolumes" + - "ec2:DetachVolume" + Resource: '*' + +availabilityZones: + - us-east-1a + - us-east-1b + - us-east-1c + +managedNodeGroups: + - name: ng-us-east-1a + instanceType: m6a.8xlarge + volumeSize: 256 + volumeType: gp3 + volumeIOPS: 16000 + volumeThroughput: 512 + desiredCapacity: 1 + minSize: 1 + maxSize: 6 + privateNetworking: true + availabilityZones: + - us-east-1a + + - name: ng-us-east-1b + instanceType: m6a.8xlarge + volumeSize: 256 + volumeType: gp3 + volumeIOPS: 16000 + volumeThroughput: 512 + desiredCapacity: 1 + minSize: 1 + maxSize: 6 + privateNetworking: true + availabilityZones: + - us-east-1b + + - name: ng-us-east-1c + instanceType: m6a.8xlarge + volumeSize: 256 + volumeType: gp3 + volumeIOPS: 16000 + volumeThroughput: 512 + desiredCapacity: 1 + minSize: 1 + maxSize: 6 + privateNetworking: true + availabilityZones: + - us-east-1c diff --git a/aws/storageclass.yaml b/aws/storageclass.yaml new file mode 100644 index 00000000..e03c1c8a --- /dev/null +++ b/aws/storageclass.yaml @@ -0,0 +1,7 @@ +kind: StorageClass +apiVersion: storage.k8s.io/v1 +metadata: + name: gp3 +provisioner: kubernetes.io/aws-ebs +volumeBindingMode: WaitForFirstConsumer +reclaimPolicy: Delete \ No newline at end of file diff --git a/docker/Dockerfile.hub b/docker/Dockerfile.hub new file mode 100644 index 00000000..4bf8192e --- /dev/null +++ b/docker/Dockerfile.hub @@ -0,0 +1,9 @@ +ARG JUPYTERHUB_VERSION=3.0.2 +FROM jupyterhub/k8s-hub:$JUPYTERHUB_VERSION + +# Add template override directory and copy our example +# Replace the default +USER root +# RUN mv /usr/local/share/jupyterhub/templates/login.html /usr/local/share/jupyterhub/templates/_login.html +# COPY ./docker/login.html /usr/local/share/jupyterhub/templates/login.html +USER jovyan \ No newline at end of file diff --git a/docker/Dockerfile.init b/docker/Dockerfile.init new file mode 100644 index 00000000..06c1f211 --- /dev/null +++ b/docker/Dockerfile.init @@ -0,0 +1,13 @@ +FROM alpine/git + +ENV NB_USER=jovyan \ + NB_UID=1000 \ + HOME=/home/jovyan + +RUN adduser \ + -D \ + -g "Default user" \ + -u ${NB_UID} \ + -h ${HOME} \ + ${NB_USER} +COPY ./docker/init-entrypoint.sh /entrypoint.sh \ No newline at end of file diff --git a/Dockerfile.local b/docker/Dockerfile.spawn similarity index 51% rename from Dockerfile.local rename to docker/Dockerfile.spawn index 08858e7f..4e192511 100644 --- a/Dockerfile.local +++ b/docker/Dockerfile.spawn @@ -26,16 +26,33 @@ RUN conda install -y -c conda-forge \ jupyterlab \ ipython -COPY ./requirements.txt ./requirements.txt +# COPY ./requirements.txt ./requirements.txt +# COPY ./docker/requirements-cloud.txt ./requirements-cloud.txt +COPY ./environment.yml ./environment.yml -RUN python3 -m pip install -r requirements.txt \ - && python3 -m pip install papermill +# RUN python3 -m pip install -r requirements.txt \ +# && python3 -m pip install papermill + +# RUN conda install -y -c conda-forge --file requirements.txt +# RUN conda install -y -c conda-forge --file requirements-cloud.txt +# RUN python3 -m pip install papermill && \ +# python3 -m pip install ipython==7.34.0 && \ +RUN conda env update -n base --file environment.yml +RUN python3 -m IPython kernel install + +# RUN python3 -m pip install -r requirements-cloud.txt && \ +# python3 -m pip install ipython==7.34.0 && \ +# python3 -m IPython kernel install + +RUN python3 -m pip install jupyter_app_launcher && \ + python3 -m pip install --upgrade jupyter-server && \ + mkdir -p /usr/local/share/jupyter/lab/jupyter_app_launcher + +COPY ./docker/jupyter-launcher.yaml /usr/local/share/jupyter/lab/jupyter_app_launcher RUN chown -R ${NB_USER} "${HOME}" -USER ${NB_USER} WORKDIR ${HOME} -ENV PATH="${HOME}/.local/bin:${PATH}" COPY --chown=${NB_USER} ./notebooks/01_thicket_tutorial.ipynb ./notebooks/01_thicket_tutorial.ipynb COPY --chown=${NB_USER} ./notebooks/02_thicket_rajaperf_clustering.ipynb ./notebooks/02_thicket_rajaperf_clustering.ipynb @@ -43,13 +60,20 @@ COPY --chown=${NB_USER} ./notebooks/03_extrap-with-metadata-aggregated.ipynb ./n COPY --chown=${NB_USER} ./notebooks/04_stats-functions.ipynb ./notebooks/04_stats-functions.ipynb COPY --chown=${NB_USER} ./notebooks/05_thicket_query_language.ipynb ./notebooks/05_thicket_query_language.ipynb COPY --chown=${NB_USER} ./data/ ./data/ - -COPY ./docker_scripts/entrypoint.sh /entrypoint.sh -COPY ./docker_scripts/start.sh /start.sh -COPY ./docker_scripts/run_all.sh /run_all.sh +COPY --chown=${NB_USER} ./thicket-logo.png ./thicket-logo.png ENV SHELL=/usr/bin/bash EXPOSE 8888 ENTRYPOINT [ "tini", "--" ] +COPY ./docker/entrypoint.sh /entrypoint.sh +COPY ./docker/start.sh /start.sh +COPY ./docker/run_all.sh /run_all.sh + +RUN mkdir -p $HOME/.local/share && \ + chmod 777 $HOME/.local/share + +USER ${NB_USER} +ENV PATH="${HOME}/.local/bin:${PATH}" + CMD [ "jupyter", "notebook" ] diff --git a/docker_scripts/entrypoint.sh b/docker/entrypoint.sh similarity index 100% rename from docker_scripts/entrypoint.sh rename to docker/entrypoint.sh diff --git a/docker/init-entrypoint.sh b/docker/init-entrypoint.sh new file mode 100644 index 00000000..0c0f7847 --- /dev/null +++ b/docker/init-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Copy the notebook icon +# This would be for the customized launcher, not working yet +# wget https://flux-framework.org/assets/images/Flux-logo-mark-only-full-color.png +# mv Flux-logo-mark-only-full-color.png /home/jovyan/flux-icon.png + +# We need to clone to the user home, and then change permissions to uid 1000 +# That uid is shared by jovyan here and the spawn container +# git clone https://github.com/rse-ops/flux-radiuss-tutorial-2023 /home/jovyan/flux-tutorial +chown -R 1000 /home/jovyan \ No newline at end of file diff --git a/docker/jupyter-launcher.yaml b/docker/jupyter-launcher.yaml new file mode 100644 index 00000000..d2d7bdd0 --- /dev/null +++ b/docker/jupyter-launcher.yaml @@ -0,0 +1,131 @@ +- title: "Chapter 1: Thicket 101" + description: Intro to Thicket and basics + type: jupyterlab-commands + icon: ./thicket-logo.png + source: + - label: Thicket Tutorial + id: 'filebrowser:open-path' + args: + path: 01_thicket_tutorial.ipynb + icon: ./thicket-logo.png + catalog: Notebook +- title: "Chapter 2: Clustering RAJAPerf" + description: Using Thicket to cluster data from the RAJA Performance Suite + type: jupyterlab-commands + icon: ./thicket-logo.png + source: + - label: Thicket Tutorial + id: 'filebrowser:open-path' + args: + path: 02_thicket_rajaperf_clustering.ipynb + icon: ./thicket-logo.png + catalog: Notebook +- title: "Chapter 3: Modeling with Extra-P" + description: Modeling applications using Thicket and Extra-P + type: jupyterlab-commands + icon: ./thicket-logo.png + source: + - label: Thicket Tutorial + id: 'filebrowser:open-path' + args: + path: 03_extrap-with-metadata-aggregated.ipynb + icon: ./thicket-logo.png + catalog: Notebook +- title: "Chapter 4: Stats and Visualization" + description: Using Thicket to calculate statistics and visualize performance across runs + type: jupyterlab-commands + icon: ./thicket-logo.png + source: + - label: Thicket Tutorial + id: 'filebrowser:open-path' + args: + path: 04_stats-functions.ipynb + icon: ./thicket-logo.png + catalog: Notebook +- title: "Chapter 5: Call Graph Query Language" + description: Using Thicket's query language for advanced filtering + type: jupyterlab-commands + icon: ./thicket-logo.png + source: + - label: Thicket Tutorial + id: 'filebrowser:open-path' + args: + path: 05_thicket_query_language.ipynb + icon: ./thicket-logo.png + catalog: Notebook +- title: "Chapter 6: Composing Datasets with Groupby-Aggregate" + description: Using Thicket's groupby-aggregate functionality to compose datasets + type: jupyterlab-commands + icon: ./thicket-logo.png + source: + - label: Thicket Tutorial + id: 'filebrowser:open-path' + args: + path: 06_groupby_aggregate_of_multirun_data.ipynb + icon: ./thicket-logo.png + catalog: Notebook + +- title: Thicket ReadTheDocs + description: Documentation for Thicket + source: https://thicket.readthedocs.io/en/latest/ + type: url + catalog: Thicket Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Thicket Repository + description: Repository for Thicket + source: https://github.com/llnl/thicket + type: url + catalog: Thicket Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Hatchet Documentation + description: Documentation for Hatchet + source: https://llnl-hatchet.readthedocs.io/en/latest/ + type: url + catalog: Thicket Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Hatchet Repository + description: Repository for Hatchet + source: https://github.com/llnl/hatchet + type: url + catalog: Thicket Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] + +- title: Thicket Paper + description: HPDC'23 Paper on Thicket + source: https://doi.org/10.1145/3588195.3592989 + type: url + catalog: Thicket Publications + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Hatchet Paper + description: SC'19 Paper on Thicket + source: https://doi.org/10.1145/3295500.3356219 + type: url + catalog: Thicket Publications + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Hatchet/Thicket Query Language Paper + description: eScience'22 Paper on the Hatchet/Thicket Query Language + source: https://doi.org/10.1109/eScience55777.2022.00039 + type: url + catalog: Thicket Publications + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Hatchet Interactive Visualization Paper + description: 2024 TVCG Paper on Hatchet's Interactive Visualizations + source: https://doi.org/10.1109/TVCG.2024.3354561 + type: url + catalog: Thicket Publications + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Hatchet Improvements Paper + description: 2020 ProTools Workshop Paper at SC + source: https://doi.org/10.1109/HUSTProtools51951.2020.00013 + type: url + catalog: Thicket Publications + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] \ No newline at end of file diff --git a/docker_scripts/run_all.sh b/docker/run_all.sh similarity index 100% rename from docker_scripts/run_all.sh rename to docker/run_all.sh diff --git a/docker_scripts/start.sh b/docker/start.sh similarity index 100% rename from docker_scripts/start.sh rename to docker/start.sh diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..d856517c --- /dev/null +++ b/environment.yml @@ -0,0 +1,113 @@ +channels: + - conda-forge +dependencies: + - pip + - psutil==5.9.5 + - pip: + - llnl-thicket[extrap,plotting]==2024.1.0 + - ipython==8.13.0 + - scikit-learn + - alembic==1.11.3 + - anyio==3.7.1 + - argon2-cffi==23.1.0 + - argon2-cffi-bindings==21.2.0 + - arrow==1.2.3 + - asttokens==2.2.1 + - async-generator==1.10 + - async-lru==2.0.4 + - attrs==23.1.0 + - babel==2.12.1 + - backcall==0.2.0 + - beautifulsoup4==4.12.2 + - bleach==6.0.0 + - certifi==2023.7.22 + - certipy==0.1.3 + - cffi==1.15.1 + - charset-normalizer==3.2.0 + - comm==0.1.4 + - cryptography==41.0.3 + - debugpy==1.6.7.post1 + - decorator==5.1.1 + - defusedxml==0.7.1 + - executing==1.2.0 + - fastjsonschema==2.18.0 + - fqdn==1.5.1 + - greenlet==2.0.2 + - idna==3.4 + - ipykernel==6.25.1 + - isoduration==20.11.0 + - jedi==0.19.0 + - jinja2==3.1.2 + - json5==0.9.14 + - jsonpointer==2.4 + - jsonschema[format-nongpl]==4.19.0 + - jsonschema-specifications==2023.7.1 + - jupyter-client==8.3.0 + - jupyter-core==5.3.1 + - jupyter-events==0.7.0 + - jupyter-lsp==2.2.0 + - jupyter-server==2.7.2 + - jupyter-server-terminals==0.4.4 + - jupyter-telemetry==0.1.0 + - jupyterhub==4.0.2 + - jupyterlab==4.0.5 + - jupyterlab-pygments==0.2.2 + - jupyterlab-server==2.24.0 + - mako==1.2.4 + - markupsafe==2.1.3 + - matplotlib-inline==0.1.6 + - mistune==3.0.1 + - nbclassic==1.0.0 + - nbclient==0.8.0 + - nbconvert==7.7.4 + - nbformat==5.9.2 + - nbgitpuller==1.2.0 + - nest-asyncio==1.5.7 + - notebook-shim==0.2.3 + - oauthlib==3.2.2 + - overrides==7.4.0 + - packaging==23.1 + - pamela==1.1.0 + - pandocfilters==1.5.0 + - parso==0.8.3 + - pexpect==4.8.0 + - pickleshare==0.7.5 + - platformdirs==3.10.0 + - prometheus-client==0.17.1 + - prompt-toolkit==3.0.39 + - ptyprocess==0.7.0 + - pure-eval==0.2.2 + - pycparser==2.21 + - pygments==2.16.1 + - pyopenssl==23.2.0 + - python-dateutil==2.8.2 + - python-json-logger==2.0.7 + - pyyaml==6.0.1 + - pyzmq==25.1.1 + - referencing==0.30.2 + - requests==2.31.0 + - rfc3339-validator==0.1.4 + - rfc3986-validator==0.1.1 + - rpds-py==0.9.2 + - ruamel-yaml==0.17.32 + - ruamel-yaml-clib==0.2.7 + - send2trash==1.8.2 + - six==1.16.0 + - sniffio==1.3.0 + - soupsieve==2.4.1 + - sqlalchemy==2.0.20 + - stack-data==0.6.2 + - terminado==0.17.1 + - tinycss2==1.2.1 + - tornado==6.3.3 + - traitlets==5.9.0 + - typing-extensions==4.7.1 + - uri-template==1.3.0 + - urllib3==2.0.4 + - wcwidth==0.2.6 + - webcolors==1.13 + - webencodings==0.5.1 + - websocket-client==1.6.1 + # - ipython==7.15.0 + # - ipython-genutils==0.2.0 + # - psutil==5.9.5 diff --git a/gcp/config.yaml b/gcp/config.yaml new file mode 100644 index 00000000..edfbae4f --- /dev/null +++ b/gcp/config.yaml @@ -0,0 +1,62 @@ +# A few notes! +# The hub -> authentic class defaults to "dummy" +# We shouldn't need any image pull secrets assuming public +# There is a note about the database being a sqlite pvc +# (and a TODO for better solution for Kubernetes) + +# This is the concurrent spawn limit, likely should be increased (deafults to 64) +hub: + concurrentSpawnLimit: 10 + config: + DummyAuthenticator: + password: butter + JupyterHub: + admin_access: true + authenticator_class: dummy + + # This is the image I built based off of jupyterhub/k8s-hub, 3.0.2 at time of writing this + image: + name: ghcr.io/flux-framework/flux-jupyter-hub + tag: "riken-2024" + pullPolicy: Always + +# https://z2jh.jupyter.org/en/latest/administrator/optimization.html#scaling-up-in-time-user-placeholders +scheduling: + podPriority: + enabled: true + userPlaceholder: + # Specify 3 dummy user pods will be used as placeholders + replicas: 3 + +# This is the "spawn" image +singleuser: + image: + name: ghcr.io/flux-framework/flux-jupyter-spawn + tag: "2023" + pullPolicy: Always + cpu: + limit: 1 + memory: + limit: '4G' + cmd: /entrypoint.sh + +# initContainers: +# - name: init-myservice +# image: alpine/git +# command: ["git", "clone", "https://github.com/rse-ops/flux-radiuss-tutorial-2023", "/home/jovyan/flux-tutorial"] +# volumeMounts: +# - name: flux-tutorial +# mountPath: /home/jovyan + + # This is how we get the tutorial files added + storage: + type: none + + # gitRepo volume is deprecated so we need another way + # https://kubernetes.io/docs/concepts/storage/volumes/#gitrepo + extraVolumes: + - name: flux-tutorial + emptyDir: {} + extraVolumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan/ \ No newline at end of file diff --git a/thicket-logo.png b/thicket-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..35e5c7b2dd0cce7918c94e548b25467abf6c1148 GIT binary patch literal 122222 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

gSO<)!rB(fmtXY=a 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 V$*xx5&0Ylm?KCtsNYl4F_dj?+H?05w literal 0 HcmV?d00001 diff --git a/tmp/requirements-cloud.txt b/tmp/requirements-cloud.txt new file mode 100644 index 00000000..8122f2d8 --- /dev/null +++ b/tmp/requirements-cloud.txt @@ -0,0 +1,339 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# Use the "Run workflow" button at https://github.com/jupyterhub/zero-to-jupyterhub-k8s/actions/workflows/watch-dependencies.yaml +# +alembic==1.11.3 + # via jupyterhub +anyio==3.7.1 + # via jupyter-server +argon2-cffi==23.1.0 + # via + # jupyter-server + # nbclassic +argon2-cffi-bindings==21.2.0 + # via argon2-cffi +arrow==1.2.3 + # via isoduration +asttokens==2.2.1 + # via stack-data +async-generator==1.10 + # via jupyterhub +async-lru==2.0.4 + # via jupyterlab +attrs==23.1.0 + # via + # jsonschema + # referencing +babel==2.12.1 + # via jupyterlab-server +backcall==0.2.0 + # via ipython +beautifulsoup4==4.12.2 + # via nbconvert +bleach==6.0.0 + # via nbconvert +certifi==2023.7.22 + # via requests +certipy==0.1.3 + # via jupyterhub +cffi==1.15.1 + # via + # argon2-cffi-bindings + # cryptography +charset-normalizer==3.2.0 + # via requests +comm==0.1.4 + # via ipykernel +cryptography==41.0.3 + # via pyopenssl +debugpy==1.6.7.post1 + # via ipykernel +decorator==5.1.1 + # via ipython +defusedxml==0.7.1 + # via nbconvert +executing==1.2.0 + # via stack-data +fastjsonschema==2.18.0 + # via nbformat +fqdn==1.5.1 + # via jsonschema +greenlet==2.0.2 + # via sqlalchemy +idna==3.4 + # via + # anyio + # jsonschema + # requests +ipykernel==6.25.1 + # via + # jupyterlab + # nbclassic +ipython==8.13.0 + # via ipykernel +ipython-genutils==0.2.0 + # via nbclassic +isoduration==20.11.0 + # via jsonschema +jedi==0.19.0 + # via ipython +jinja2==3.1.2 + # via + # jupyter-server + # jupyterhub + # jupyterlab + # jupyterlab-server + # nbclassic + # nbconvert +json5==0.9.14 + # via jupyterlab-server +jsonpointer==2.4 + # via jsonschema +jsonschema[format-nongpl]==4.19.0 + # via + # jupyter-events + # jupyter-telemetry + # jupyterlab-server + # nbformat +jsonschema-specifications==2023.7.1 + # via jsonschema +jupyter-client==8.3.0 + # via + # ipykernel + # jupyter-server + # nbclassic + # nbclient +jupyter-core==5.3.1 + # via + # ipykernel + # jupyter-client + # jupyter-server + # jupyterlab + # nbclassic + # nbclient + # nbconvert + # nbformat +jupyter-events==0.7.0 + # via jupyter-server +jupyter-lsp==2.2.0 + # via jupyterlab +jupyter-server==2.7.2 + # via + # jupyter-lsp + # jupyterlab + # jupyterlab-server + # nbclassic + # nbgitpuller + # notebook-shim +jupyter-server-terminals==0.4.4 + # via jupyter-server +jupyter-telemetry==0.1.0 + # via jupyterhub +jupyterhub==4.0.2 + # via -r requirements.in +jupyterlab==4.0.5 + # via -r requirements.in +jupyterlab-pygments==0.2.2 + # via nbconvert +jupyterlab-server==2.24.0 + # via jupyterlab +mako==1.2.4 + # via alembic +markupsafe==2.1.3 + # via + # jinja2 + # mako + # nbconvert +matplotlib-inline==0.1.6 + # via + # ipykernel + # ipython +mistune==3.0.1 + # via nbconvert +nbclassic==1.0.0 + # via -r requirements.in +nbclient==0.8.0 + # via nbconvert +nbconvert==7.7.4 + # via + # jupyter-server + # nbclassic +nbformat==5.9.2 + # via + # jupyter-server + # nbclassic + # nbclient + # nbconvert +nbgitpuller==1.2.0 + # via -r requirements.in +nest-asyncio==1.5.7 + # via + # ipykernel + # nbclassic +notebook-shim==0.2.3 + # via + # jupyterlab + # nbclassic +oauthlib==3.2.2 + # via jupyterhub +overrides==7.4.0 + # via jupyter-server +packaging==23.1 + # via + # ipykernel + # jupyter-server + # jupyterhub + # jupyterlab + # jupyterlab-server + # nbconvert +pamela==1.1.0 + # via jupyterhub +pandocfilters==1.5.0 + # via nbconvert +parso==0.8.3 + # via jedi +pexpect==4.8.0 + # via ipython +pickleshare==0.7.5 + # via ipython +platformdirs==3.10.0 + # via jupyter-core +prometheus-client==0.17.1 + # via + # jupyter-server + # jupyterhub + # nbclassic +prompt-toolkit==3.0.39 + # via ipython +psutil==5.9.5 + # via ipykernel +ptyprocess==0.7.0 + # via + # pexpect + # terminado +pure-eval==0.2.2 + # via stack-data +pycparser==2.21 + # via cffi +pygments==2.16.1 + # via + # ipython + # nbconvert +pyopenssl==23.2.0 + # via certipy +python-dateutil==2.8.2 + # via + # arrow + # jupyter-client + # jupyterhub +python-json-logger==2.0.7 + # via + # jupyter-events + # jupyter-telemetry +pyyaml==6.0.1 + # via jupyter-events +pyzmq==25.1.1 + # via + # ipykernel + # jupyter-client + # jupyter-server + # nbclassic +referencing==0.30.2 + # via + # jsonschema + # jsonschema-specifications + # jupyter-events +requests==2.31.0 + # via + # jupyterhub + # jupyterlab-server +rfc3339-validator==0.1.4 + # via + # jsonschema + # jupyter-events +rfc3986-validator==0.1.1 + # via + # jsonschema + # jupyter-events +rpds-py==0.9.2 + # via + # jsonschema + # referencing +ruamel-yaml==0.17.32 + # via jupyter-telemetry +ruamel-yaml-clib==0.2.7 + # via ruamel-yaml +send2trash==1.8.2 + # via + # jupyter-server + # nbclassic +six==1.16.0 + # via + # asttokens + # bleach + # python-dateutil + # rfc3339-validator +sniffio==1.3.0 + # via anyio +soupsieve==2.4.1 + # via beautifulsoup4 +sqlalchemy==2.0.20 + # via + # alembic + # jupyterhub +stack-data==0.6.2 + # via ipython +terminado==0.17.1 + # via + # jupyter-server + # jupyter-server-terminals + # nbclassic +tinycss2==1.2.1 + # via nbconvert +tornado==6.3.3 + # via + # ipykernel + # jupyter-client + # jupyter-server + # jupyterhub + # jupyterlab + # nbclassic + # nbgitpuller + # terminado +traitlets==5.9.0 + # via + # comm + # ipykernel + # ipython + # jupyter-client + # jupyter-core + # jupyter-events + # jupyter-server + # jupyter-telemetry + # jupyterhub + # jupyterlab + # matplotlib-inline + # nbclassic + # nbclient + # nbconvert + # nbformat +typing-extensions==4.7.1 + # via + # alembic + # sqlalchemy +uri-template==1.3.0 + # via jsonschema +urllib3==2.0.4 + # via requests +wcwidth==0.2.6 + # via prompt-toolkit +webcolors==1.13 + # via jsonschema +webencodings==0.5.1 + # via + # bleach + # tinycss2 +websocket-client==1.6.1 + # via jupyter-server \ No newline at end of file diff --git a/requirements.txt b/tmp/requirements.txt similarity index 100% rename from requirements.txt rename to tmp/requirements.txt From fcc6ca2543a73a609e164af5b8c1fe66d4f51901 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Thu, 1 Aug 2024 21:57:39 -0700 Subject: [PATCH 02/26] Gets local runs working again --- docker/Dockerfile.spawn | 7 ++++--- docker/start.sh | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile.spawn b/docker/Dockerfile.spawn index 4e192511..2e3d6260 100644 --- a/docker/Dockerfile.spawn +++ b/docker/Dockerfile.spawn @@ -50,8 +50,6 @@ RUN python3 -m pip install jupyter_app_launcher && \ COPY ./docker/jupyter-launcher.yaml /usr/local/share/jupyter/lab/jupyter_app_launcher -RUN chown -R ${NB_USER} "${HOME}" - WORKDIR ${HOME} COPY --chown=${NB_USER} ./notebooks/01_thicket_tutorial.ipynb ./notebooks/01_thicket_tutorial.ipynb @@ -59,9 +57,12 @@ COPY --chown=${NB_USER} ./notebooks/02_thicket_rajaperf_clustering.ipynb ./noteb COPY --chown=${NB_USER} ./notebooks/03_extrap-with-metadata-aggregated.ipynb ./notebooks/03_extrap-with-metadata-aggregated.ipynb COPY --chown=${NB_USER} ./notebooks/04_stats-functions.ipynb ./notebooks/04_stats-functions.ipynb COPY --chown=${NB_USER} ./notebooks/05_thicket_query_language.ipynb ./notebooks/05_thicket_query_language.ipynb +COPY --chown=${NB_USER} ./notebooks/06_groupby_aggregate_of_multirun_data.ipynb ./notebooks/06_groupby_aggregate_of_multirun_data.ipynb COPY --chown=${NB_USER} ./data/ ./data/ COPY --chown=${NB_USER} ./thicket-logo.png ./thicket-logo.png +RUN chown -R ${NB_USER} "${HOME}" + ENV SHELL=/usr/bin/bash EXPOSE 8888 ENTRYPOINT [ "tini", "--" ] @@ -76,4 +77,4 @@ RUN mkdir -p $HOME/.local/share && \ USER ${NB_USER} ENV PATH="${HOME}/.local/bin:${PATH}" -CMD [ "jupyter", "notebook" ] +CMD [ "jupyter", "lab" ] diff --git a/docker/start.sh b/docker/start.sh index 877f861d..79f0a5db 100755 --- a/docker/start.sh +++ b/docker/start.sh @@ -1,3 +1,3 @@ #!/bin/bash -/opt/conda/bin/jupyter-notebook --ip=0.0.0.0 \ No newline at end of file +/opt/conda/bin/jupyter-lab --ip=0.0.0.0 \ No newline at end of file From 879a4f455b0d771f27664fa66d310e903fa95f3f Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Thu, 1 Aug 2024 22:57:59 -0700 Subject: [PATCH 03/26] Gets local runs fully working --- docker/Dockerfile.spawn | 39 +++++----- docker/jupyter-launcher.yaml | 134 +++++++++++++++++------------------ 2 files changed, 90 insertions(+), 83 deletions(-) diff --git a/docker/Dockerfile.spawn b/docker/Dockerfile.spawn index 2e3d6260..a163150f 100644 --- a/docker/Dockerfile.spawn +++ b/docker/Dockerfile.spawn @@ -17,7 +17,11 @@ RUN adduser \ RUN apt-get update \ && apt-get upgrade -y \ && apt-get install -y --no-install-recommends \ + ca-certificates \ + dnsutils \ + iputils-ping \ build-essential \ + vim \ git \ && rm -rf /var/lib/apt/lists/* @@ -38,7 +42,7 @@ COPY ./environment.yml ./environment.yml # RUN python3 -m pip install papermill && \ # python3 -m pip install ipython==7.34.0 && \ RUN conda env update -n base --file environment.yml -RUN python3 -m IPython kernel install +# RUN python3 -m IPython kernel install # RUN python3 -m pip install -r requirements-cloud.txt && \ # python3 -m pip install ipython==7.34.0 && \ @@ -46,24 +50,21 @@ RUN python3 -m IPython kernel install RUN python3 -m pip install jupyter_app_launcher && \ python3 -m pip install --upgrade jupyter-server && \ + python3 -m pip install jupyter-launcher-shortcuts && \ mkdir -p /usr/local/share/jupyter/lab/jupyter_app_launcher -COPY ./docker/jupyter-launcher.yaml /usr/local/share/jupyter/lab/jupyter_app_launcher +COPY ./docker/jupyter-launcher.yaml /usr/local/share/jupyter/lab/jupyter_app_launcher/jp_app_launcher.yaml -WORKDIR ${HOME} +ENV JUPYTER_APP_LAUNCHER_PATH /usr/local/share/jupyter/lab/jupyter_app_launcher + +RUN chmod -R 777 ~/ /home/jovyan -COPY --chown=${NB_USER} ./notebooks/01_thicket_tutorial.ipynb ./notebooks/01_thicket_tutorial.ipynb -COPY --chown=${NB_USER} ./notebooks/02_thicket_rajaperf_clustering.ipynb ./notebooks/02_thicket_rajaperf_clustering.ipynb -COPY --chown=${NB_USER} ./notebooks/03_extrap-with-metadata-aggregated.ipynb ./notebooks/03_extrap-with-metadata-aggregated.ipynb -COPY --chown=${NB_USER} ./notebooks/04_stats-functions.ipynb ./notebooks/04_stats-functions.ipynb -COPY --chown=${NB_USER} ./notebooks/05_thicket_query_language.ipynb ./notebooks/05_thicket_query_language.ipynb -COPY --chown=${NB_USER} ./notebooks/06_groupby_aggregate_of_multirun_data.ipynb ./notebooks/06_groupby_aggregate_of_multirun_data.ipynb -COPY --chown=${NB_USER} ./data/ ./data/ -COPY --chown=${NB_USER} ./thicket-logo.png ./thicket-logo.png +USER ${NB_USER} +WORKDIR ${HOME} -RUN chown -R ${NB_USER} "${HOME}" +COPY ./thicket-logo.png ${HOME}/thicket-logo.png -ENV SHELL=/usr/bin/bash +ENV SHELL=/bin/bash EXPOSE 8888 ENTRYPOINT [ "tini", "--" ] @@ -71,10 +72,16 @@ COPY ./docker/entrypoint.sh /entrypoint.sh COPY ./docker/start.sh /start.sh COPY ./docker/run_all.sh /run_all.sh +ENV PATH "${HOME}/.local/bin:${PATH}" RUN mkdir -p $HOME/.local/share && \ chmod 777 $HOME/.local/share -USER ${NB_USER} -ENV PATH="${HOME}/.local/bin:${PATH}" - CMD [ "jupyter", "lab" ] + +COPY ./notebooks/01_thicket_tutorial.ipynb ./notebooks/01_thicket_tutorial.ipynb +COPY ./notebooks/02_thicket_rajaperf_clustering.ipynb ./notebooks/02_thicket_rajaperf_clustering.ipynb +COPY ./notebooks/03_extrap-with-metadata-aggregated.ipynb ./notebooks/03_extrap-with-metadata-aggregated.ipynb +COPY ./notebooks/04_stats-functions.ipynb ./notebooks/04_stats-functions.ipynb +COPY ./notebooks/05_thicket_query_language.ipynb ./notebooks/05_thicket_query_language.ipynb +COPY ./notebooks/06_groupby_aggregate_of_multirun_data.ipynb ./notebooks/06_groupby_aggregate_of_multirun_data.ipynb +COPY ./data/ ./data/ diff --git a/docker/jupyter-launcher.yaml b/docker/jupyter-launcher.yaml index d2d7bdd0..2f0877ea 100644 --- a/docker/jupyter-launcher.yaml +++ b/docker/jupyter-launcher.yaml @@ -1,68 +1,68 @@ - title: "Chapter 1: Thicket 101" description: Intro to Thicket and basics type: jupyterlab-commands - icon: ./thicket-logo.png + icon: /home/jovyan/thicket-logo.png source: - label: Thicket Tutorial id: 'filebrowser:open-path' args: - path: 01_thicket_tutorial.ipynb - icon: ./thicket-logo.png + path: ./notebooks/01_thicket_tutorial.ipynb + icon: /home/jovyan/thicket-logo.png catalog: Notebook - title: "Chapter 2: Clustering RAJAPerf" description: Using Thicket to cluster data from the RAJA Performance Suite type: jupyterlab-commands - icon: ./thicket-logo.png + icon: /home/jovyan/thicket-logo.png source: - label: Thicket Tutorial id: 'filebrowser:open-path' args: - path: 02_thicket_rajaperf_clustering.ipynb - icon: ./thicket-logo.png + path: ./notebooks/02_thicket_rajaperf_clustering.ipynb + icon: /home/jovyan/thicket-logo.png catalog: Notebook - title: "Chapter 3: Modeling with Extra-P" description: Modeling applications using Thicket and Extra-P type: jupyterlab-commands - icon: ./thicket-logo.png + icon: /home/jovyan/thicket-logo.png source: - label: Thicket Tutorial id: 'filebrowser:open-path' args: - path: 03_extrap-with-metadata-aggregated.ipynb - icon: ./thicket-logo.png + path: ./notebooks/03_extrap-with-metadata-aggregated.ipynb + icon: /home/jovyan/thicket-logo.png catalog: Notebook - title: "Chapter 4: Stats and Visualization" description: Using Thicket to calculate statistics and visualize performance across runs type: jupyterlab-commands - icon: ./thicket-logo.png + icon: /home/jovyan/thicket-logo.png source: - label: Thicket Tutorial id: 'filebrowser:open-path' args: - path: 04_stats-functions.ipynb - icon: ./thicket-logo.png + path: ./notebooks/04_stats-functions.ipynb + icon: /home/jovyan/thicket-logo.png catalog: Notebook - title: "Chapter 5: Call Graph Query Language" description: Using Thicket's query language for advanced filtering type: jupyterlab-commands - icon: ./thicket-logo.png + icon: /home/jovyan/thicket-logo.png source: - label: Thicket Tutorial id: 'filebrowser:open-path' args: - path: 05_thicket_query_language.ipynb - icon: ./thicket-logo.png + path: ./notebooks/05_thicket_query_language.ipynb + icon: /home/jovyan/thicket-logo.png catalog: Notebook - title: "Chapter 6: Composing Datasets with Groupby-Aggregate" description: Using Thicket's groupby-aggregate functionality to compose datasets type: jupyterlab-commands - icon: ./thicket-logo.png + icon: /home/jovyan/thicket-logo.png source: - label: Thicket Tutorial id: 'filebrowser:open-path' args: - path: 06_groupby_aggregate_of_multirun_data.ipynb - icon: ./thicket-logo.png + path: ./notebooks/06_groupby_aggregate_of_multirun_data.ipynb + icon: /home/jovyan/thicket-logo.png catalog: Notebook - title: Thicket ReadTheDocs @@ -72,13 +72,13 @@ catalog: Thicket Resources args: sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] -- title: Thicket Repository - description: Repository for Thicket - source: https://github.com/llnl/thicket - type: url - catalog: Thicket Resources - args: - sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +# - title: Thicket Repository +# description: Repository for Thicket +# source: https://github.com/llnl/thicket +# type: url +# catalog: Thicket Resources +# args: +# sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] - title: Hatchet Documentation description: Documentation for Hatchet source: https://llnl-hatchet.readthedocs.io/en/latest/ @@ -86,46 +86,46 @@ catalog: Thicket Resources args: sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] -- title: Hatchet Repository - description: Repository for Hatchet - source: https://github.com/llnl/hatchet - type: url - catalog: Thicket Resources - args: - sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +# - title: Hatchet Repository +# description: Repository for Hatchet +# source: https://github.com/llnl/hatchet +# type: url +# catalog: Thicket Resources +# args: +# sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] -- title: Thicket Paper - description: HPDC'23 Paper on Thicket - source: https://doi.org/10.1145/3588195.3592989 - type: url - catalog: Thicket Publications - args: - sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] -- title: Hatchet Paper - description: SC'19 Paper on Thicket - source: https://doi.org/10.1145/3295500.3356219 - type: url - catalog: Thicket Publications - args: - sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] -- title: Hatchet/Thicket Query Language Paper - description: eScience'22 Paper on the Hatchet/Thicket Query Language - source: https://doi.org/10.1109/eScience55777.2022.00039 - type: url - catalog: Thicket Publications - args: - sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] -- title: Hatchet Interactive Visualization Paper - description: 2024 TVCG Paper on Hatchet's Interactive Visualizations - source: https://doi.org/10.1109/TVCG.2024.3354561 - type: url - catalog: Thicket Publications - args: - sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] -- title: Hatchet Improvements Paper - description: 2020 ProTools Workshop Paper at SC - source: https://doi.org/10.1109/HUSTProtools51951.2020.00013 - type: url - catalog: Thicket Publications - args: - sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] \ No newline at end of file +# - title: Thicket Paper +# description: HPDC'23 Paper on Thicket +# source: https://doi.org/10.1145/3588195.3592989 +# type: url +# catalog: Thicket Publications +# args: +# sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +# - title: Hatchet Paper +# description: SC'19 Paper on Thicket +# source: https://doi.org/10.1145/3295500.3356219 +# type: url +# catalog: Thicket Publications +# args: +# sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +# - title: Hatchet/Thicket Query Language Paper +# description: eScience'22 Paper on the Hatchet/Thicket Query Language +# source: https://doi.org/10.1109/eScience55777.2022.00039 +# type: url +# catalog: Thicket Publications +# args: +# sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +# - title: Hatchet Interactive Visualization Paper +# description: 2024 TVCG Paper on Hatchet's Interactive Visualizations +# source: https://doi.org/10.1109/TVCG.2024.3354561 +# type: url +# catalog: Thicket Publications +# args: +# sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +# - title: Hatchet Improvements Paper +# description: 2020 ProTools Workshop Paper at SC +# source: https://doi.org/10.1109/HUSTProtools51951.2020.00013 +# type: url +# catalog: Thicket Publications +# args: +# sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] From b0c2c786fb71035bff24cb9a29dbf8ba53a6185f Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Thu, 1 Aug 2024 23:02:22 -0700 Subject: [PATCH 04/26] Modifies config-aws.yaml for AWS deployment in K8s --- aws/config-aws.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/aws/config-aws.yaml b/aws/config-aws.yaml index c5cb9594..7ef33f58 100644 --- a/aws/config-aws.yaml +++ b/aws/config-aws.yaml @@ -16,8 +16,8 @@ hub: # This is the image I built based off of jupyterhub/k8s-hub, 3.0.2 at time of writing this image: - name: ghcr.io/flux-framework/flux-jupyter-hub - tag: "riken-2024" + name: ghcr.io/LLNL/thicket-jupyter-hub + tag: "radiuss-2024" pullPolicy: Always # https://z2jh.jupyter.org/en/latest/administrator/optimization.html#scaling-up-in-time-user-placeholders @@ -31,8 +31,8 @@ scheduling: # This is the "spawn" image singleuser: image: - name: ghcr.io/flux-framework/flux-jupyter-spawn - tag: "riken-2024" + name: ghcr.io/LLNL/thicket-jupyter-spawn + tag: "radiuss-2024" pullPolicy: Always cpu: limit: 1 @@ -43,10 +43,10 @@ singleuser: # This runs as the root user, who clones and changes ownership to uid 1000 initContainers: - name: init-myservice - image: ghcr.io/flux-framework/flux-jupyter-init:riken-2024 + image: ghcr.io/LLNL/thicket-jupyter-init:radiuss-2024 command: ["/entrypoint.sh"] volumeMounts: - - name: flux-tutorial + - name: thicket-tutorial mountPath: /home/jovyan # This is how we get the tutorial files added @@ -55,8 +55,8 @@ singleuser: # gitRepo volume is deprecated so we need another way # https://kubernetes.io/docs/concepts/storage/volumes/#gitrepo extraVolumes: - - name: flux-tutorial + - name: thicket-tutorial emptyDir: {} extraVolumeMounts: - - name: flux-tutorial + - name: thicket-tutorial mountPath: /home/jovyan \ No newline at end of file From 908f9e8060d27ae70a088ace33bf650209b1519d Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Thu, 1 Aug 2024 23:03:25 -0700 Subject: [PATCH 05/26] Updates GCP config --- gcp/config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gcp/config.yaml b/gcp/config.yaml index edfbae4f..51e67f9a 100644 --- a/gcp/config.yaml +++ b/gcp/config.yaml @@ -16,8 +16,8 @@ hub: # This is the image I built based off of jupyterhub/k8s-hub, 3.0.2 at time of writing this image: - name: ghcr.io/flux-framework/flux-jupyter-hub - tag: "riken-2024" + name: ghcr.io/LLNL/thicket-jupyter-hub + tag: "radiuss-2024" pullPolicy: Always # https://z2jh.jupyter.org/en/latest/administrator/optimization.html#scaling-up-in-time-user-placeholders @@ -31,8 +31,8 @@ scheduling: # This is the "spawn" image singleuser: image: - name: ghcr.io/flux-framework/flux-jupyter-spawn - tag: "2023" + name: ghcr.io/LLNL/thicket-jupyter-spawn + tag: "radiuss-2024" pullPolicy: Always cpu: limit: 1 @@ -55,8 +55,8 @@ singleuser: # gitRepo volume is deprecated so we need another way # https://kubernetes.io/docs/concepts/storage/volumes/#gitrepo extraVolumes: - - name: flux-tutorial + - name: thicket-tutorial emptyDir: {} extraVolumeMounts: - - name: flux-tutorial + - name: thicket-tutorial mountPath: /home/jovyan/ \ No newline at end of file From b951737aa27d6e01bbc55c2e85678e81606e9fd9 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Mon, 5 Aug 2024 16:00:21 -0700 Subject: [PATCH 06/26] Adds GitHub Action for building and uploading images --- .github/workflows/build_docker_images.yaml | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/build_docker_images.yaml diff --git a/.github/workflows/build_docker_images.yaml b/.github/workflows/build_docker_images.yaml new file mode 100644 index 00000000..dda9f5a0 --- /dev/null +++ b/.github/workflows/build_docker_images.yaml @@ -0,0 +1,60 @@ +name: Build containers for the Thicket Tutorial + +on: + workflow_dispatch: + inputs: + tutorial_name: + description: 'Name of the tutorial. Will be used as the container tag.' + required: true + type: string + +jobs: + build-containers: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + registry_url_base: ["ghcr.io/LLNL"] + container_info: [["thicket-tutorial-hub", "docker/Dockerfile.hub"], + ["thicket-tutorial-init", "docker/Dockerfile.init"], + ["thicket-tutorial-spawn", "docker/Dockerfile.spawn"]] + + steps: + - name: Clone the thicket-tutorial repo + uses: actions/checkout@v4 + + - name: Clean unneeded stuff in runner to make space for the Docker image + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: true + + - name: GHCR Login + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pull existing layers + env: + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + run: | + docker pull ${container} || echo "${container} has not been pushed yet" + + - name: Build Container + env: + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + dockerfile: ${{ matrix.container_info[1] }} + run: | + docker build -f ${dockerfile} -t ${container} . + + - name: Deploy Container + env: + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + run: docker push ${container} From f1f6c033aba983be5ab7a805729aa37c4a83f11c Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Mon, 5 Aug 2024 16:42:51 -0700 Subject: [PATCH 07/26] Tries fixing the action yaml --- .github/workflows/build_docker_images.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_docker_images.yaml b/.github/workflows/build_docker_images.yaml index dda9f5a0..e8cbe6c7 100644 --- a/.github/workflows/build_docker_images.yaml +++ b/.github/workflows/build_docker_images.yaml @@ -20,7 +20,7 @@ jobs: ["thicket-tutorial-spawn", "docker/Dockerfile.spawn"]] steps: - - name: Clone the thicket-tutorial repo + - name: Clone the thicket-tutorial repo uses: actions/checkout@v4 - name: Clean unneeded stuff in runner to make space for the Docker image From ed1d099269c3989d7d8a2543bc76374a7ac8d14a Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Mon, 5 Aug 2024 16:44:05 -0700 Subject: [PATCH 08/26] Fixes yaml indentation --- .github/workflows/build_docker_images.yaml | 70 +++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build_docker_images.yaml b/.github/workflows/build_docker_images.yaml index e8cbe6c7..f1785d76 100644 --- a/.github/workflows/build_docker_images.yaml +++ b/.github/workflows/build_docker_images.yaml @@ -19,42 +19,42 @@ jobs: ["thicket-tutorial-init", "docker/Dockerfile.init"], ["thicket-tutorial-spawn", "docker/Dockerfile.spawn"]] - steps: - - name: Clone the thicket-tutorial repo - uses: actions/checkout@v4 + steps: + - name: Clone the thicket-tutorial repo + uses: actions/checkout@v4 - - name: Clean unneeded stuff in runner to make space for the Docker image - uses: jlumbroso/free-disk-space@v1.3.1 - with: - tool-cache: false - android: true - dotnet: true - haskell: true - large-packages: true - docker-images: false - swap-storage: true + - name: Clean unneeded stuff in runner to make space for the Docker image + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: true - - name: GHCR Login - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Pull existing layers - env: - container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} - run: | - docker pull ${container} || echo "${container} has not been pushed yet" + - name: GHCR Login + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pull existing layers + env: + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + run: | + docker pull ${container} || echo "${container} has not been pushed yet" - - name: Build Container - env: - container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} - dockerfile: ${{ matrix.container_info[1] }} - run: | - docker build -f ${dockerfile} -t ${container} . + - name: Build Container + env: + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + dockerfile: ${{ matrix.container_info[1] }} + run: | + docker build -f ${dockerfile} -t ${container} . - - name: Deploy Container - env: - container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} - run: docker push ${container} + - name: Deploy Container + env: + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + run: docker push ${container} From c8bf13d9424f281d9fb52b1dfb3d324b103c23d1 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Mon, 5 Aug 2024 16:46:25 -0700 Subject: [PATCH 09/26] More YAML formatting --- .github/workflows/build_docker_images.yaml | 66 +++++++++++----------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build_docker_images.yaml b/.github/workflows/build_docker_images.yaml index f1785d76..f9855c64 100644 --- a/.github/workflows/build_docker_images.yaml +++ b/.github/workflows/build_docker_images.yaml @@ -21,40 +21,40 @@ jobs: steps: - name: Clone the thicket-tutorial repo - uses: actions/checkout@v4 + uses: actions/checkout@v4 - - name: Clean unneeded stuff in runner to make space for the Docker image - uses: jlumbroso/free-disk-space@v1.3.1 - with: - tool-cache: false - android: true - dotnet: true - haskell: true - large-packages: true - docker-images: false - swap-storage: true + - name: Clean unneeded stuff in runner to make space for the Docker image + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: true - - name: GHCR Login - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Pull existing layers - env: - container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} - run: | - docker pull ${container} || echo "${container} has not been pushed yet" + - name: GHCR Login + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pull existing layers + env: + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + run: | + docker pull ${container} || echo "${container} has not been pushed yet" - - name: Build Container - env: - container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} - dockerfile: ${{ matrix.container_info[1] }} - run: | - docker build -f ${dockerfile} -t ${container} . + - name: Build Container + env: + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + dockerfile: ${{ matrix.container_info[1] }} + run: | + docker build -f ${dockerfile} -t ${container} . - - name: Deploy Container - env: - container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} - run: docker push ${container} + - name: Deploy Container + env: + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + run: docker push ${container} From 2330fda02d53fbd801369bf6ff31bf13f545bbd1 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 11:21:52 -0700 Subject: [PATCH 10/26] Updates Docker CI to run on PRs and only push on worklow_dispatch --- .github/workflows/build_docker_images.yaml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build_docker_images.yaml b/.github/workflows/build_docker_images.yaml index f9855c64..147e46ed 100644 --- a/.github/workflows/build_docker_images.yaml +++ b/.github/workflows/build_docker_images.yaml @@ -1,12 +1,8 @@ name: Build containers for the Thicket Tutorial on: + pull_request: [] workflow_dispatch: - inputs: - tutorial_name: - description: 'Name of the tutorial. Will be used as the container tag.' - required: true - type: string jobs: build-containers: @@ -14,6 +10,7 @@ jobs: strategy: fail-fast: true matrix: + tutorial_name: ["radiuss-2024"] registry_url_base: ["ghcr.io/LLNL"] container_info: [["thicket-tutorial-hub", "docker/Dockerfile.hub"], ["thicket-tutorial-init", "docker/Dockerfile.init"], @@ -35,6 +32,7 @@ jobs: swap-storage: true - name: GHCR Login + if: (github.event.name != 'pull_request') uses: docker/login-action@v3 with: registry: ghcr.io @@ -43,18 +41,19 @@ jobs: - name: Pull existing layers env: - container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ matrix.tutorial_name }} run: | docker pull ${container} || echo "${container} has not been pushed yet" - name: Build Container env: - container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ matrix.tutorial_name }} dockerfile: ${{ matrix.container_info[1] }} run: | docker build -f ${dockerfile} -t ${container} . - name: Deploy Container + if: (github.event_name != 'pull_request') env: - container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ inputs.tutorial_name }} + container: ${{ matrix.registry_url_base }}/${{ matrix.container_info[0] }}:${{ matrix.tutorial_name }} run: docker push ${container} From b6e81e1de8ee8ebe6713edbed4fbf3f0dbfb8905 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 11:25:31 -0700 Subject: [PATCH 11/26] Fixes repo name in Docker CD since that apparently needs to be lowercase --- .github/workflows/build_docker_images.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_docker_images.yaml b/.github/workflows/build_docker_images.yaml index 147e46ed..2c619119 100644 --- a/.github/workflows/build_docker_images.yaml +++ b/.github/workflows/build_docker_images.yaml @@ -11,7 +11,7 @@ jobs: fail-fast: true matrix: tutorial_name: ["radiuss-2024"] - registry_url_base: ["ghcr.io/LLNL"] + registry_url_base: ["ghcr.io/llnl"] container_info: [["thicket-tutorial-hub", "docker/Dockerfile.hub"], ["thicket-tutorial-init", "docker/Dockerfile.init"], ["thicket-tutorial-spawn", "docker/Dockerfile.spawn"]] From 9811769485b5d3272351d93079cf805e002aa6ee Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 11:35:55 -0700 Subject: [PATCH 12/26] Updates the README with instructions for deploying to K8S --- README.md | 972 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 970 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 24aeadc5..953827ae 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ or by clicking the badge at the top of this file. We provide a Dockerfile for users to run the notebooks locally. To run locally *and interactively*, you must first build the Docker container with: ```bash -$ docker build -t thicket-tutorial -f Dockerfile . +$ docker build -t thicket-tutorial -f docker/Dockerfile.spawn . ``` Then, you must create a Docker network with: @@ -59,7 +59,7 @@ $ docker run --rm -it --entrypoint /start.sh -v /var/run/docker.sock:/var/run/do Alternatively, if you want to run the notebooks automatically (i.e., non-interactive), you can simply run the `dev_scripts/autorun.sh` script. This script executes the same commands as above, but it uses the `run_all.sh` script as an entrypoint instead of `start.sh`. -The Docker-based code for running this tutorial locally was derived from the material from the 2023 RADIUSS tutorial for Flux, which can be found here: https://github.com/flux-framework/Tutorials/tree/master/2023-RADIUSS-AWS/JupyterNotebook +The Docker-based code for running this tutorial locally was derived from the material from the 2024 RADIUSS tutorial for Flux, which can be found here: https://github.com/flux-framework/Tutorials/tree/master/2024-RADIUSS-AWS/JupyterNotebook #### Podman @@ -95,6 +95,974 @@ Clean up after you are done: $ podman machine stop ``` +### Deploying the Tutorial with Kubernetes + +This tutorial borrows from the infrastructure created by [Vanessa Sochat](https://github.com/vsoch) and [Dan Milroy](https://github.com/milroy) for the [Flux Tutorial](https://github.com/flux-framework/Tutorials/tree/2024-radiuss-aws/2024-RADIUSS-AWS/JupyterNotebook). Thanks to this infrastructure, this tutorial can be deployed to Kubernetes using the following tools: +* `kubectl` +* `eksctl` (for AWS) +* `gcloud` (for Google Cloud) + +The following instructions describe how to deploy this tutorial to Kubernetes on either Google Cloud or AWS. + +#### 1. Create Cluster + +##### Google Cloud + +Here is how to create the cluster on Google Cloud using [gcloud](https://cloud.google.com/sdk/docs/install) (and assuming you have logged in +with [gcloud auth login](https://cloud.google.com/sdk/gcloud/reference/auth/login): + +```bash +export GOOGLE_PROJECT=myproject +gcloud container clusters create flux-jupyter --project $GOOGLE_PROJECT \ + --zone us-central1-a --machine-type n1-standard-2 \ + --num-nodes=4 --enable-network-policy --enable-intra-node-visibility +``` + +##### AWS + +Here is how to create an equivalent cluster on AWS (EKS). We will be using [eksctl](https://eksctl.io/introduction/), which +you should install. + +```bash +# Create an EKS cluster with autoscaling with default storage +eksctl create cluster --config-file aws/eksctl-config.yaml + +# Create an EKS cluster with io1 node storage but no autoscaling, used for the RADIUSS 2023 tutorial +eksctl create cluster --config-file aws/eksctl-radiuss-tutorial-2023.yaml +``` + +You can find vanilla (manual) instructions [here](https://z2jh.jupyter.org/en/stable/kubernetes/amazon/step-zero-aws-eks.html) if you +are interested in how it works. We emulate the logic there using eksctl. Then generate a secret token - we will add this to [config-aws.yaml](aws/config-aws.yaml) (without SSL) or [config-aws-ssl.yaml](aws/config-aws-ssl.yaml) (with SSL). When your cluster is ready, this will deploy an EBS CSI driver: + +```bash +kubectl apply -k "github.com/kubernetes-sigs/aws-ebs-csi-driver/deploy/kubernetes/overlays/stable/?ref=master" +``` + +And install the cluster-autoscaler: + +```bash +kubectl apply -f aws/cluster-autoscaler-autodiscover.yaml +``` + +If you want to use a different storage class than the default (`gp2`), you also need to create the new storage class (`gp3` here) and set it as the default storage class: + +```bash +kubectl apply -f aws/storageclass.yaml +kubectl patch storageclass gp3 -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' +kubectl patch storageclass gp2 -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}' +``` + +Most of the information I needed to read about this was [here](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/README.md) - the Jupyter documentation wasn't super helpful beyond saying to install it. Also note that I got this (seemingly working) without the `propagateASGTags` set to true, but that is something that I've seen have issue. +You can look at the autoscaler pod logs for information. + +While the spawned containers (e.g., where you run your notebook) don't use these volumes, the hub will. +You can read about [gp2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html) class. +Note that we will be using [config-aws.yaml](aws/config-aws.yaml) if you don't need SSL, and [config-aws-ssl.yaml](aws/config-aws-ssl.yaml) if you do. For the latter, the jupyter spawner will generate let's encrypt certificates for us, given that we have correctly configured DNS. + +#### 2. Deploy JupyterHub + +We will use [helm](https://helm.sh/docs/helm/helm_install/) to install charts and deploy. + +```bash +helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ +helm repo update +``` + +You can see the versions available: + +```bash +helm search repo jupyterhub +``` +```console +NAME CHART VERSION APP VERSION DESCRIPTION +bitnami/jupyterhub 4.2.0 4.0.2 JupyterHub brings the power of notebooks to gro... +jupyterhub/jupyterhub 3.0.2 4.0.2 Multi-user Jupyter installation +jupyterhub/pebble 1.0.1 v2.3.1 This Helm chart bootstraps Pebble: an ACME serv... +``` + +Note that chart versions don't always coincide with software (or "app") versions. At the time of writing this, +we are using the jupyterhub/jupyterhub 3.0.2/4.0.2 versions, and our container bases point to 3.0.2 tags for the +corresponding images. Next, see the values we can set, which likely will come from a config*.yaml that we will choose. + +```bash +helm show values jupyterhub/jupyterhub +``` + +

+ +Example values for the jupyterhub helm chart + +```console +# fullnameOverride and nameOverride distinguishes blank strings, null values, +# and non-blank strings. For more details, see the configuration reference. +fullnameOverride: "" +nameOverride: + +# enabled is ignored by the jupyterhub chart itself, but a chart depending on +# the jupyterhub chart conditionally can make use this config option as the +# condition. +enabled: + +# custom can contain anything you want to pass to the hub pod, as all passed +# Helm template values will be made available there. +custom: {} + +# imagePullSecret is configuration to create a k8s Secret that Helm chart's pods +# can get credentials from to pull their images. +imagePullSecret: + create: false + automaticReferenceInjection: true + registry: + username: + password: + email: +# imagePullSecrets is configuration to reference the k8s Secret resources the +# Helm chart's pods can get credentials from to pull their images. +imagePullSecrets: [] + +# hub relates to the hub pod, responsible for running JupyterHub, its configured +# Authenticator class KubeSpawner, and its configured Proxy class +# ConfigurableHTTPProxy. KubeSpawner creates the user pods, and +# ConfigurableHTTPProxy speaks with the actual ConfigurableHTTPProxy server in +# the proxy pod. +hub: + revisionHistoryLimit: + config: + JupyterHub: + admin_access: true + authenticator_class: dummy + service: + type: ClusterIP + annotations: {} + ports: + nodePort: + extraPorts: [] + loadBalancerIP: + baseUrl: / + cookieSecret: + initContainers: [] + nodeSelector: {} + tolerations: [] + concurrentSpawnLimit: 64 + consecutiveFailureLimit: 5 + activeServerLimit: + deploymentStrategy: + ## type: Recreate + ## - sqlite-pvc backed hubs require the Recreate deployment strategy as a + ## typical PVC storage can only be bound to one pod at the time. + ## - JupyterHub isn't designed to support being run in parallel. More work + ## needs to be done in JupyterHub itself for a fully highly available (HA) + ## deployment of JupyterHub on k8s is to be possible. + type: Recreate + db: + type: sqlite-pvc + upgrade: + pvc: + annotations: {} + selector: {} + accessModes: + - ReadWriteOnce + storage: 1Gi + subPath: + storageClassName: + url: + password: + labels: {} + annotations: {} + command: [] + args: [] + extraConfig: {} + extraFiles: {} + extraEnv: {} + extraContainers: [] + extraVolumes: [] + extraVolumeMounts: [] + image: + name: jupyterhub/k8s-hub + tag: "3.0.2" + pullPolicy: + pullSecrets: [] + resources: {} + podSecurityContext: + fsGroup: 1000 + containerSecurityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + lifecycle: {} + loadRoles: {} + services: {} + pdb: + enabled: false + maxUnavailable: + minAvailable: 1 + networkPolicy: + enabled: true + ingress: [] + egress: [] + egressAllowRules: + cloudMetadataServer: true + dnsPortsCloudMetadataServer: true + dnsPortsKubeSystemNamespace: true + dnsPortsPrivateIPs: true + nonPrivateIPs: true + privateIPs: true + interNamespaceAccessLabels: ignore + allowedIngressPorts: [] + allowNamedServers: false + namedServerLimitPerUser: + authenticatePrometheus: + redirectToServer: + shutdownOnLogout: + templatePaths: [] + templateVars: {} + livenessProbe: + # The livenessProbe's aim to give JupyterHub sufficient time to startup but + # be able to restart if it becomes unresponsive for ~5 min. + enabled: true + initialDelaySeconds: 300 + periodSeconds: 10 + failureThreshold: 30 + timeoutSeconds: 3 + readinessProbe: + # The readinessProbe's aim is to provide a successful startup indication, + # but following that never become unready before its livenessProbe fail and + # restarts it if needed. To become unready following startup serves no + # purpose as there are no other pod to fallback to in our non-HA deployment. + enabled: true + initialDelaySeconds: 0 + periodSeconds: 2 + failureThreshold: 1000 + timeoutSeconds: 1 + existingSecret: + serviceAccount: + create: true + name: + annotations: {} + extraPodSpec: {} + +rbac: + create: true + +# proxy relates to the proxy pod, the proxy-public service, and the autohttps +# pod and proxy-http service. +proxy: + secretToken: + annotations: {} + deploymentStrategy: + ## type: Recreate + ## - JupyterHub's interaction with the CHP proxy becomes a lot more robust + ## with this configuration. To understand this, consider that JupyterHub + ## during startup will interact a lot with the k8s service to reach a + ## ready proxy pod. If the hub pod during a helm upgrade is restarting + ## directly while the proxy pod is making a rolling upgrade, the hub pod + ## could end up running a sequence of interactions with the old proxy pod + ## and finishing up the sequence of interactions with the new proxy pod. + ## As CHP proxy pods carry individual state this is very error prone. One + ## outcome when not using Recreate as a strategy has been that user pods + ## have been deleted by the hub pod because it considered them unreachable + ## as it only configured the old proxy pod but not the new before trying + ## to reach them. + type: Recreate + ## rollingUpdate: + ## - WARNING: + ## This is required to be set explicitly blank! Without it being + ## explicitly blank, k8s will let eventual old values under rollingUpdate + ## remain and then the Deployment becomes invalid and a helm upgrade would + ## fail with an error like this: + ## + ## UPGRADE FAILED + ## Error: Deployment.apps "proxy" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy `type` is 'Recreate' + ## Error: UPGRADE FAILED: Deployment.apps "proxy" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy `type` is 'Recreate' + rollingUpdate: + # service relates to the proxy-public service + service: + type: LoadBalancer + labels: {} + annotations: {} + nodePorts: + http: + https: + disableHttpPort: false + extraPorts: [] + loadBalancerIP: + loadBalancerSourceRanges: [] + # chp relates to the proxy pod, which is responsible for routing traffic based + # on dynamic configuration sent from JupyterHub to CHP's REST API. + chp: + revisionHistoryLimit: + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + image: + name: jupyterhub/configurable-http-proxy + # tag is automatically bumped to new patch versions by the + # watch-dependencies.yaml workflow. + # + tag: "4.5.6" # https://github.com/jupyterhub/configurable-http-proxy/tags + pullPolicy: + pullSecrets: [] + extraCommandLineFlags: [] + livenessProbe: + enabled: true + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 30 + timeoutSeconds: 3 + readinessProbe: + enabled: true + initialDelaySeconds: 0 + periodSeconds: 2 + failureThreshold: 1000 + timeoutSeconds: 1 + resources: {} + defaultTarget: + errorTarget: + extraEnv: {} + nodeSelector: {} + tolerations: [] + networkPolicy: + enabled: true + ingress: [] + egress: [] + egressAllowRules: + cloudMetadataServer: true + dnsPortsCloudMetadataServer: true + dnsPortsKubeSystemNamespace: true + dnsPortsPrivateIPs: true + nonPrivateIPs: true + privateIPs: true + interNamespaceAccessLabels: ignore + allowedIngressPorts: [http, https] + pdb: + enabled: false + maxUnavailable: + minAvailable: 1 + extraPodSpec: {} + # traefik relates to the autohttps pod, which is responsible for TLS + # termination when proxy.https.type=letsencrypt. + traefik: + revisionHistoryLimit: + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + image: + name: traefik + # tag is automatically bumped to new patch versions by the + # watch-dependencies.yaml workflow. + # + tag: "v2.10.4" # ref: https://hub.docker.com/_/traefik?tab=tags + pullPolicy: + pullSecrets: [] + hsts: + includeSubdomains: false + preload: false + maxAge: 15724800 # About 6 months + resources: {} + labels: {} + extraInitContainers: [] + extraEnv: {} + extraVolumes: [] + extraVolumeMounts: [] + extraStaticConfig: {} + extraDynamicConfig: {} + nodeSelector: {} + tolerations: [] + extraPorts: [] + networkPolicy: + enabled: true + ingress: [] + egress: [] + egressAllowRules: + cloudMetadataServer: true + dnsPortsCloudMetadataServer: true + dnsPortsKubeSystemNamespace: true + dnsPortsPrivateIPs: true + nonPrivateIPs: true + privateIPs: true + interNamespaceAccessLabels: ignore + allowedIngressPorts: [http, https] + pdb: + enabled: false + maxUnavailable: + minAvailable: 1 + serviceAccount: + create: true + name: + annotations: {} + extraPodSpec: {} + secretSync: + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + image: + name: jupyterhub/k8s-secret-sync + tag: "3.0.2" + pullPolicy: + pullSecrets: [] + resources: {} + labels: {} + https: + enabled: false + type: letsencrypt + #type: letsencrypt, manual, offload, secret + letsencrypt: + contactEmail: + # Specify custom server here (https://acme-staging-v02.api.letsencrypt.org/directory) to hit staging LE + acmeServer: https://acme-v02.api.letsencrypt.org/directory + manual: + key: + cert: + secret: + name: + key: tls.key + crt: tls.crt + hosts: [] + +# singleuser relates to the configuration of KubeSpawner which runs in the hub +# pod, and its spawning of user pods such as jupyter-myusername. +singleuser: + podNameTemplate: + extraTolerations: [] + nodeSelector: {} + extraNodeAffinity: + required: [] + preferred: [] + extraPodAffinity: + required: [] + preferred: [] + extraPodAntiAffinity: + required: [] + preferred: [] + networkTools: + image: + name: jupyterhub/k8s-network-tools + tag: "3.0.2" + pullPolicy: + pullSecrets: [] + resources: {} + cloudMetadata: + # block set to true will append a privileged initContainer using the + # iptables to block the sensitive metadata server at the provided ip. + blockWithIptables: true + ip: 169.254.169.254 + networkPolicy: + enabled: true + ingress: [] + egress: [] + egressAllowRules: + cloudMetadataServer: false + dnsPortsCloudMetadataServer: true + dnsPortsKubeSystemNamespace: true + dnsPortsPrivateIPs: true + nonPrivateIPs: true + privateIPs: false + interNamespaceAccessLabels: ignore + allowedIngressPorts: [] + events: true + extraAnnotations: {} + extraLabels: + hub.jupyter.org/network-access-hub: "true" + extraFiles: {} + extraEnv: {} + lifecycleHooks: {} + initContainers: [] + extraContainers: [] + allowPrivilegeEscalation: false + uid: 1000 + fsGid: 100 + serviceAccountName: + storage: + type: dynamic + extraLabels: {} + extraVolumes: [] + extraVolumeMounts: [] + static: + pvcName: + subPath: "{username}" + capacity: 10Gi + homeMountPath: /home/jovyan + dynamic: + storageClass: + pvcNameTemplate: claim-{username}{servername} + volumeNameTemplate: volume-{username}{servername} + storageAccessModes: [ReadWriteOnce] + image: + name: jupyterhub/k8s-singleuser-sample + tag: "3.0.2" + pullPolicy: + pullSecrets: [] + startTimeout: 300 + cpu: + limit: + guarantee: + memory: + limit: + guarantee: 1G + extraResource: + limits: {} + guarantees: {} + cmd: jupyterhub-singleuser + defaultUrl: + extraPodConfig: {} + profileList: [] + +# scheduling relates to the user-scheduler pods and user-placeholder pods. +scheduling: + userScheduler: + enabled: true + revisionHistoryLimit: + replicas: 2 + logLevel: 4 + # plugins are configured on the user-scheduler to make us score how we + # schedule user pods in a way to help us schedule on the most busy node. By + # doing this, we help scale down more effectively. It isn't obvious how to + # enable/disable scoring plugins, and configure them, to accomplish this. + # + # plugins ref: https://kubernetes.io/docs/reference/scheduling/config/#scheduling-plugins-1 + # migration ref: https://kubernetes.io/docs/reference/scheduling/config/#scheduler-configuration-migrations + # + plugins: + score: + # These scoring plugins are enabled by default according to + # https://kubernetes.io/docs/reference/scheduling/config/#scheduling-plugins + # 2022-02-22. + # + # Enabled with high priority: + # - NodeAffinity + # - InterPodAffinity + # - NodeResourcesFit + # - ImageLocality + # Remains enabled with low default priority: + # - TaintToleration + # - PodTopologySpread + # - VolumeBinding + # Disabled for scoring: + # - NodeResourcesBalancedAllocation + # + disabled: + # We disable these plugins (with regards to scoring) to not interfere + # or complicate our use of NodeResourcesFit. + - name: NodeResourcesBalancedAllocation + # Disable plugins to be allowed to enable them again with a different + # weight and avoid an error. + - name: NodeAffinity + - name: InterPodAffinity + - name: NodeResourcesFit + - name: ImageLocality + enabled: + - name: NodeAffinity + weight: 14631 + - name: InterPodAffinity + weight: 1331 + - name: NodeResourcesFit + weight: 121 + - name: ImageLocality + weight: 11 + pluginConfig: + # Here we declare that we should optimize pods to fit based on a + # MostAllocated strategy instead of the default LeastAllocated. + - name: NodeResourcesFit + args: + scoringStrategy: + resources: + - name: cpu + weight: 1 + - name: memory + weight: 1 + type: MostAllocated + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + image: + # IMPORTANT: Bumping the minor version of this binary should go hand in + # hand with an inspection of the user-scheduelrs RBAC resources + # that we have forked in + # templates/scheduling/user-scheduler/rbac.yaml. + # + # Debugging advice: + # + # - Is configuration of kube-scheduler broken in + # templates/scheduling/user-scheduler/configmap.yaml? + # + # - Is the kube-scheduler binary's compatibility to work + # against a k8s api-server that is too new or too old? + # + # - You can update the GitHub workflow that runs tests to + # include "deploy/user-scheduler" in the k8s namespace report + # and reduce the user-scheduler deployments replicas to 1 in + # dev-config.yaml to get relevant logs from the user-scheduler + # pods. Inspect the "Kubernetes namespace report" action! + # + # - Typical failures are that kube-scheduler fails to search for + # resources via its "informers", and won't start trying to + # schedule pods before they succeed which may require + # additional RBAC permissions or that the k8s api-server is + # aware of the resources. + # + # - If "successfully acquired lease" can be seen in the logs, it + # is a good sign kube-scheduler is ready to schedule pods. + # + name: registry.k8s.io/kube-scheduler + # tag is automatically bumped to new patch versions by the + # watch-dependencies.yaml workflow. The minor version is pinned in the + # workflow, and should be updated there if a minor version bump is done + # here. We aim to stay around 1 minor version behind the latest k8s + # version. + # + tag: "v1.26.7" # ref: https://github.com/kubernetes/kubernetes/tree/master/CHANGELOG + pullPolicy: + pullSecrets: [] + nodeSelector: {} + tolerations: [] + labels: {} + annotations: {} + pdb: + enabled: true + maxUnavailable: 1 + minAvailable: + resources: {} + serviceAccount: + create: true + name: + annotations: {} + extraPodSpec: {} + podPriority: + enabled: false + globalDefault: false + defaultPriority: 0 + imagePullerPriority: -5 + userPlaceholderPriority: -10 + userPlaceholder: + enabled: true + image: + name: registry.k8s.io/pause + # tag is automatically bumped to new patch versions by the + # watch-dependencies.yaml workflow. + # + # If you update this, also update prePuller.pause.image.tag + # + tag: "3.9" + pullPolicy: + pullSecrets: [] + revisionHistoryLimit: + replicas: 0 + labels: {} + annotations: {} + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + resources: {} + corePods: + tolerations: + - key: hub.jupyter.org/dedicated + operator: Equal + value: core + effect: NoSchedule + - key: hub.jupyter.org_dedicated + operator: Equal + value: core + effect: NoSchedule + nodeAffinity: + matchNodePurpose: prefer + userPods: + tolerations: + - key: hub.jupyter.org/dedicated + operator: Equal + value: user + effect: NoSchedule + - key: hub.jupyter.org_dedicated + operator: Equal + value: user + effect: NoSchedule + nodeAffinity: + matchNodePurpose: prefer + +# prePuller relates to the hook|continuous-image-puller DaemonsSets +prePuller: + revisionHistoryLimit: + labels: {} + annotations: {} + resources: {} + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + extraTolerations: [] + # hook relates to the hook-image-awaiter Job and hook-image-puller DaemonSet + hook: + enabled: true + pullOnlyOnChanges: true + # image and the configuration below relates to the hook-image-awaiter Job + image: + name: jupyterhub/k8s-image-awaiter + tag: "3.0.2" + pullPolicy: + pullSecrets: [] + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + podSchedulingWaitDuration: 10 + nodeSelector: {} + tolerations: [] + resources: {} + serviceAccount: + create: true + name: + annotations: {} + continuous: + enabled: true + pullProfileListImages: true + extraImages: {} + pause: + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + image: + name: registry.k8s.io/pause + # tag is automatically bumped to new patch versions by the + # watch-dependencies.yaml workflow. + # + # If you update this, also update scheduling.userPlaceholder.image.tag + # + tag: "3.9" + pullPolicy: + pullSecrets: [] + +ingress: + enabled: false + annotations: {} + ingressClassName: + hosts: [] + pathSuffix: + pathType: Prefix + tls: [] + +# cull relates to the jupyterhub-idle-culler service, responsible for evicting +# inactive singleuser pods. +# +# The configuration below, except for enabled, corresponds to command-line flags +# for jupyterhub-idle-culler as documented here: +# https://github.com/jupyterhub/jupyterhub-idle-culler#as-a-standalone-script +# +cull: + enabled: true + users: false # --cull-users + adminUsers: true # --cull-admin-users + removeNamedServers: false # --remove-named-servers + timeout: 3600 # --timeout + every: 600 # --cull-every + concurrency: 10 # --concurrency + maxAge: 0 # --max-age + +debug: + enabled: false + +global: + safeToShowValues: false +``` + +
+ +##### Changes You Might Need to Make: + +- Change the config*.yaml image-> name and tag that you deploy to use your images. +- You might want to change the number of user placeholder pods +- Also change the hub->concurrentSpawnLimit +- Change the password, ssl secret, and domain name if applicable +- Change the aws/eksctl-config.yaml autoscaling ranges depending on your needs. +- Remove pullPolicy Always if you don't expect to want to update/re-pull an image every time (ideal for production) + +And here is how to deploy, assuming the default namespace. Please choose your cloud appropriately! + +```bash +# This is for Google Cloud +helm install flux-jupyter jupyterhub/jupyterhub --values gcp/config.yaml + +# This is for Amazon EKS without SSL +helm install flux-jupyter jupyterhub/jupyterhub --values aws/config-aws.yaml + +# This is for Amazon EKS with SSL (assuming DNS is configured) +helm install flux-jupyter jupyterhub/jupyterhub --values aws/config-aws-ssl.yaml +``` + +If you mess something up, you can change the file and run `helm upgrade`: + +```bash +helm upgrade flux-jupyter jupyterhub/jupyterhub --values aws/config-aws-ssl.yaml +``` + +If you REALLY mess something up, you can tear the whole thing down and then install again: + +```bash +helm uninstall flux-jupyter +``` + +Note that in practice of bringing this up and down many times, we have seen the proxy-public +not create a handful of times. If this happens, just tear down everything, wait for all pods +to terminate, and then start freshly. When you run a command, also note that the terminal will hang! +You can see progress in another terminal: + +```bash +$ kubectl get pods +``` + +or try watching: + +```bash +$ kubectl get pods --watch +``` + +When it's done, you should see: + +```bash +$ kubectl get pods +NAME READY STATUS RESTARTS AGE +continuous-image-puller-nvr4g 1/1 Running 0 5m31s +hub-7d59dfb748-mrfdv 1/1 Running 0 5m31s +proxy-d9dfbf77b-v488t 1/1 Running 0 5m31s +user-scheduler-587fcc5479-c4mmk 1/1 Running 0 5m31s +user-scheduler-587fcc5479-x6jmk 1/1 Running 0 5m31s +``` + +(The numbers of each above might vary based on the size of your cluster). And the terminal provides a lot of useful output: + +
+ +Output of Terminal on Completed Install + +```console +NAME: flux-jupyter +LAST DEPLOYED: Sun Aug 27 15:00:15 2023 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +. __ __ __ __ __ + / / __ __ ____ __ __ / /_ ___ _____ / / / / __ __ / /_ + __ / / / / / / / __ \ / / / / / __/ / _ \ / ___/ / /_/ / / / / / / __ \ +/ /_/ / / /_/ / / /_/ / / /_/ / / /_ / __/ / / / __ / / /_/ / / /_/ / +\____/ \__,_/ / .___/ \__, / \__/ \___/ /_/ /_/ /_/ \__,_/ /_.___/ + /_/ /____/ + + You have successfully installed the official JupyterHub Helm chart! + +### Installation info + + - Kubernetes namespace: default + - Helm release name: flux-jupyter + - Helm chart version: 3.0.2 + - JupyterHub version: 4.0.2 + - Hub pod packages: See https://github.com/jupyterhub/zero-to-jupyterhub-k8s/blob/3.0.2/images/hub/requirements.txt + +### Followup links + + - Documentation: https://z2jh.jupyter.org + - Help forum: https://discourse.jupyter.org + - Social chat: https://gitter.im/jupyterhub/jupyterhub + - Issue tracking: https://github.com/jupyterhub/zero-to-jupyterhub-k8s/issues + +### Post-installation checklist + + - Verify that created Pods enter a Running state: + + kubectl --namespace=default get pod + + If a pod is stuck with a Pending or ContainerCreating status, diagnose with: + + kubectl --namespace=default describe pod + + If a pod keeps restarting, diagnose with: + + kubectl --namespace=default logs --previous + + - Verify an external IP is provided for the k8s Service proxy-public. + + kubectl --namespace=default get service proxy-public + + If the external ip remains , diagnose with: + + kubectl --namespace=default describe service proxy-public + + - Verify web based access: + + You have not configured a k8s Ingress resource so you need to access the k8s + Service proxy-public directly. + + If your computer is outside the k8s cluster, you can port-forward traffic to + the k8s Service proxy-public with kubectl to access it from your + computer. + + kubectl --namespace=default port-forward service/proxy-public 8080:http + + Try insecure HTTP access: http://localhost:8080 +``` + +
+ +#### 3. Get Public Proxy + +Then to find the public proxy: + +```bash +kubectl get service proxy-public +``` +```console +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +proxy-public LoadBalancer 10.96.179.168 80:32530/TCP 7m22s +``` +or: + +```bash +kubectl get service proxy-public --output jsonpath='{.status.loadBalancer.ingress[].ip}' +``` + +Note that for Google, it looks like an ip address. For aws you get a string monster! + +```console +a054af2758c1549f780a433e5515a9d4-1012389935.us-east-2.elb.amazonaws.com +``` + +This might take a minute to fully be there - if it doesn't work immediately give it that. +At this point, you should be able to login as any user, open the notebook (nested two levels) +and interact with Flux! Remember that if you don't see the service, try deleting everything and +starting fresh. If that doesn't work, there might be some new error we didn't anticipate, +and you can look at logs. + +#### Clean up + +For both: + +```bash +helm uninstall flux-jupyter +``` + +For Google Cloud: + +```bash +gcloud container clusters delete flux-jupyter +``` + +For AWS: + +```bash +# If you don't do this first, it will tell the pods are un-evictable and loop forever +$ kubectl delete pod --all-namespaces --all --force +# Then delete the cluster +$ eksctl delete cluster --config-file aws/eksctl-config.yaml --wait +``` + +In practice, you'll need to start deleting with `eksctl` and then you will see the pod eviction warning +(because they were re-created) and you'll need to run the command again, and then it will clean up. + ### License This repository is distributed under the terms of the MIT license. From aa3f753a604ce3740e2a84a1f318ea5944895435 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 15:22:56 -0700 Subject: [PATCH 13/26] Typo fix in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 953827ae..3ef06679 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ you should install. eksctl create cluster --config-file aws/eksctl-config.yaml # Create an EKS cluster with io1 node storage but no autoscaling, used for the RADIUSS 2023 tutorial -eksctl create cluster --config-file aws/eksctl-radiuss-tutorial-2023.yaml +eksctl create cluster --config-file aws/eksctl-radiuss-2024.yaml ``` You can find vanilla (manual) instructions [here](https://z2jh.jupyter.org/en/stable/kubernetes/amazon/step-zero-aws-eks.html) if you From 2974737601b840b0368d6b59e1019adeb62c9888 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 15:27:58 -0700 Subject: [PATCH 14/26] Adds note about getting AWS CLI --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3ef06679..8ba236af 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ $ podman machine stop This tutorial borrows from the infrastructure created by [Vanessa Sochat](https://github.com/vsoch) and [Dan Milroy](https://github.com/milroy) for the [Flux Tutorial](https://github.com/flux-framework/Tutorials/tree/2024-radiuss-aws/2024-RADIUSS-AWS/JupyterNotebook). Thanks to this infrastructure, this tutorial can be deployed to Kubernetes using the following tools: * `kubectl` * `eksctl` (for AWS) +* AWS CLI (optional, but recommended) * `gcloud` (for Google Cloud) The following instructions describe how to deploy this tutorial to Kubernetes on either Google Cloud or AWS. From d1277df0e2e80bc764b5ff003537fa2c012ee2a3 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 15:40:34 -0700 Subject: [PATCH 15/26] Adds helm to required packages for K8S deployment --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8ba236af..21ef18b8 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ $ podman machine stop This tutorial borrows from the infrastructure created by [Vanessa Sochat](https://github.com/vsoch) and [Dan Milroy](https://github.com/milroy) for the [Flux Tutorial](https://github.com/flux-framework/Tutorials/tree/2024-radiuss-aws/2024-RADIUSS-AWS/JupyterNotebook). Thanks to this infrastructure, this tutorial can be deployed to Kubernetes using the following tools: * `kubectl` +* `helm` * `eksctl` (for AWS) * AWS CLI (optional, but recommended) * `gcloud` (for Google Cloud) From fc2909cca74d4cd8b704338005e0f4f236a88695 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 15:43:31 -0700 Subject: [PATCH 16/26] Changes flux-jupyter to thicket-tutorial-jupyter --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 21ef18b8..9759739f 100644 --- a/README.md +++ b/README.md @@ -898,13 +898,13 @@ helm install flux-jupyter jupyterhub/jupyterhub --values aws/config-aws-ssl.yaml If you mess something up, you can change the file and run `helm upgrade`: ```bash -helm upgrade flux-jupyter jupyterhub/jupyterhub --values aws/config-aws-ssl.yaml +helm upgrade thicket-tutorial-jupyter jupyterhub/jupyterhub --values aws/config-aws-ssl.yaml ``` If you REALLY mess something up, you can tear the whole thing down and then install again: ```bash -helm uninstall flux-jupyter +helm uninstall thicket-tutorial-jupyter ``` Note that in practice of bringing this up and down many times, we have seen the proxy-public @@ -941,7 +941,7 @@ user-scheduler-587fcc5479-x6jmk 1/1 Running 0 5m31s Output of Terminal on Completed Install ```console -NAME: flux-jupyter +NAME: thicket-tutorial-jupyter LAST DEPLOYED: Sun Aug 27 15:00:15 2023 NAMESPACE: default STATUS: deployed @@ -960,7 +960,7 @@ NOTES: ### Installation info - Kubernetes namespace: default - - Helm release name: flux-jupyter + - Helm release name: thicket-tutorial-jupyter - Helm chart version: 3.0.2 - JupyterHub version: 4.0.2 - Hub pod packages: See https://github.com/jupyterhub/zero-to-jupyterhub-k8s/blob/3.0.2/images/hub/requirements.txt From 73e143d09bb62209b62750cf36460e223285b3fd Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 15:49:48 -0700 Subject: [PATCH 17/26] Removes commands with SSL --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 9759739f..d7b709bf 100644 --- a/README.md +++ b/README.md @@ -890,15 +890,12 @@ helm install flux-jupyter jupyterhub/jupyterhub --values gcp/config.yaml # This is for Amazon EKS without SSL helm install flux-jupyter jupyterhub/jupyterhub --values aws/config-aws.yaml - -# This is for Amazon EKS with SSL (assuming DNS is configured) -helm install flux-jupyter jupyterhub/jupyterhub --values aws/config-aws-ssl.yaml ``` If you mess something up, you can change the file and run `helm upgrade`: ```bash -helm upgrade thicket-tutorial-jupyter jupyterhub/jupyterhub --values aws/config-aws-ssl.yaml +helm upgrade thicket-tutorial-jupyter jupyterhub/jupyterhub --values aws/config-aws.yaml ``` If you REALLY mess something up, you can tear the whole thing down and then install again: From ab2e18ee13f2dcca68ee680d63a8ccaa11658f00 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 16:02:30 -0700 Subject: [PATCH 18/26] Replaces flux-jupyter with thicket-tutorial-jupyter --- README.md | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d7b709bf..50881559 100644 --- a/README.md +++ b/README.md @@ -36,25 +36,25 @@ or by clicking the badge at the top of this file. We provide a Dockerfile for users to run the notebooks locally. To run locally *and interactively*, you must first build the Docker container with: ```bash -$ docker build -t thicket-tutorial -f docker/Dockerfile.spawn . +docker build -t thicket-tutorial -f docker/Dockerfile.spawn . ``` Then, you must create a Docker network with: ```bash -$ docker network create jupyterhub +docker network create jupyterhub ``` Finally, you can launch the tutorial. To launch the tutorial without preserving any changes, run: ```bash -$ docker run --rm -it --entrypoint /start.sh -v /var/run/docker.sock:/var/run/docker.sock --net jupyterhub --name jupyterhub -p 8888:8888 thicket-tutorial +docker run --rm -it --entrypoint /start.sh -v /var/run/docker.sock:/var/run/docker.sock --net jupyterhub --name jupyterhub -p 8888:8888 thicket-tutorial ``` If you would rather your changes be preserved, run: ```bash -$ docker run --rm -it --entrypoint /start.sh -v /var/run/docker.sock:/var/run/docker.sock -v .:/home/jovyan --net jupyterhub --name jupyterhub -p 8888:8888 thicket-tutorial +docker run --rm -it --entrypoint /start.sh -v /var/run/docker.sock:/var/run/docker.sock -v .:/home/jovyan --net jupyterhub --name jupyterhub -p 8888:8888 thicket-tutorial ``` Alternatively, if you want to run the notebooks automatically (i.e., non-interactive), you can simply run the `dev_scripts/autorun.sh` script. This script executes the same commands as above, but it uses the `run_all.sh` script as an entrypoint instead of `start.sh`. @@ -68,31 +68,31 @@ If you want to use podman instead of docker, you can replace "docker" with "podm First initialize and start podman: ```bash -$ podman machine init -$ podman machine start +podman machine init +podman machine start ``` Then build the container: ```bash -$ podman build -t thicket-tutorial -f Dockerfile . +podman build -t thicket-tutorial -f Dockerfile . ``` Then create a network: ```bash -$ podman network create jupyterhub +podman network create jupyterhub ``` Then launch the tutorial: ```bash -$ podman run --rm -it --entrypoint /start.sh -v /var/run/docker.sock:/var/run/docker.sock --net jupyterhub --name jupyterhub -p 8888:8888 thicket-tutorial +podman run --rm -it --entrypoint /start.sh -v /var/run/docker.sock:/var/run/docker.sock --net jupyterhub --name jupyterhub -p 8888:8888 thicket-tutorial ``` Clean up after you are done: ```bash -$ podman machine stop +podman machine stop ``` ### Deploying the Tutorial with Kubernetes @@ -115,7 +115,7 @@ with [gcloud auth login](https://cloud.google.com/sdk/gcloud/reference/auth/logi ```bash export GOOGLE_PROJECT=myproject -gcloud container clusters create flux-jupyter --project $GOOGLE_PROJECT \ +gcloud container clusters create thicket-tutorial-jupyter --project $GOOGLE_PROJECT \ --zone us-central1-a --machine-type n1-standard-2 \ --num-nodes=4 --enable-network-policy --enable-intra-node-visibility ``` @@ -886,10 +886,10 @@ And here is how to deploy, assuming the default namespace. Please choose your cl ```bash # This is for Google Cloud -helm install flux-jupyter jupyterhub/jupyterhub --values gcp/config.yaml +helm install thicket-tutorial-jupyter jupyterhub/jupyterhub --values gcp/config.yaml # This is for Amazon EKS without SSL -helm install flux-jupyter jupyterhub/jupyterhub --values aws/config-aws.yaml +helm install thicket-tutorial-jupyter jupyterhub/jupyterhub --values aws/config-aws.yaml ``` If you mess something up, you can change the file and run `helm upgrade`: @@ -910,19 +910,20 @@ to terminate, and then start freshly. When you run a command, also note that the You can see progress in another terminal: ```bash -$ kubectl get pods +kubectl get pods ``` or try watching: ```bash -$ kubectl get pods --watch +kubectl get pods --watch ``` When it's done, you should see: ```bash -$ kubectl get pods +kubectl get pods + NAME READY STATUS RESTARTS AGE continuous-image-puller-nvr4g 1/1 Running 0 5m31s hub-7d59dfb748-mrfdv 1/1 Running 0 5m31s @@ -1041,13 +1042,13 @@ and you can look at logs. For both: ```bash -helm uninstall flux-jupyter +helm uninstall thicket-tutorial-jupyter ``` For Google Cloud: ```bash -gcloud container clusters delete flux-jupyter +gcloud container clusters delete thicket-tutorial-jupyter ``` For AWS: From 0f35a75c2054b702812a987b5e47a608554823c8 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 16:51:05 -0700 Subject: [PATCH 19/26] Adds platform for pulled image --- docker/Dockerfile.hub | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile.hub b/docker/Dockerfile.hub index 4bf8192e..a80cb926 100644 --- a/docker/Dockerfile.hub +++ b/docker/Dockerfile.hub @@ -1,9 +1,9 @@ ARG JUPYTERHUB_VERSION=3.0.2 -FROM jupyterhub/k8s-hub:$JUPYTERHUB_VERSION +FROM --platform=linux/amd64 jupyterhub/k8s-hub:$JUPYTERHUB_VERSION # Add template override directory and copy our example # Replace the default USER root # RUN mv /usr/local/share/jupyterhub/templates/login.html /usr/local/share/jupyterhub/templates/_login.html # COPY ./docker/login.html /usr/local/share/jupyterhub/templates/login.html -USER jovyan \ No newline at end of file +USER jovyan From cbd03c18301043df48d5072cd07bef840ee877af Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 16:51:25 -0700 Subject: [PATCH 20/26] Adds platform for pulled images --- docker/Dockerfile.init | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile.init b/docker/Dockerfile.init index 06c1f211..0b07be77 100644 --- a/docker/Dockerfile.init +++ b/docker/Dockerfile.init @@ -1,4 +1,4 @@ -FROM alpine/git +FROM --platform=linux/amd64 alpine/git ENV NB_USER=jovyan \ NB_UID=1000 \ @@ -10,4 +10,4 @@ RUN adduser \ -u ${NB_UID} \ -h ${HOME} \ ${NB_USER} -COPY ./docker/init-entrypoint.sh /entrypoint.sh \ No newline at end of file +COPY ./docker/init-entrypoint.sh /entrypoint.sh From 3d2383319ca3916214ce89b2b0f0f23e363398ed Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 16:51:53 -0700 Subject: [PATCH 21/26] Adds platform for pulled image --- docker/Dockerfile.spawn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile.spawn b/docker/Dockerfile.spawn index a163150f..eb536d32 100644 --- a/docker/Dockerfile.spawn +++ b/docker/Dockerfile.spawn @@ -1,4 +1,4 @@ -FROM continuumio/miniconda3:4.12.0 +FROM --platform=linux/amd64 continuumio/miniconda3:4.12.0 USER root From 5395cb3a14dd2ff2332b6394a4f612978f448b13 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 17:07:53 -0700 Subject: [PATCH 22/26] Fixes image names --- aws/config-aws.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aws/config-aws.yaml b/aws/config-aws.yaml index 7ef33f58..38797bc1 100644 --- a/aws/config-aws.yaml +++ b/aws/config-aws.yaml @@ -16,7 +16,7 @@ hub: # This is the image I built based off of jupyterhub/k8s-hub, 3.0.2 at time of writing this image: - name: ghcr.io/LLNL/thicket-jupyter-hub + name: ghcr.io/LLNL/thicket-tutorial-hub tag: "radiuss-2024" pullPolicy: Always @@ -31,7 +31,7 @@ scheduling: # This is the "spawn" image singleuser: image: - name: ghcr.io/LLNL/thicket-jupyter-spawn + name: ghcr.io/LLNL/thicket-tutorial-spawn tag: "radiuss-2024" pullPolicy: Always cpu: @@ -43,7 +43,7 @@ singleuser: # This runs as the root user, who clones and changes ownership to uid 1000 initContainers: - name: init-myservice - image: ghcr.io/LLNL/thicket-jupyter-init:radiuss-2024 + image: ghcr.io/LLNL/thicket-tutorial-init:radiuss-2024 command: ["/entrypoint.sh"] volumeMounts: - name: thicket-tutorial @@ -59,4 +59,4 @@ singleuser: emptyDir: {} extraVolumeMounts: - name: thicket-tutorial - mountPath: /home/jovyan \ No newline at end of file + mountPath: /home/jovyan From 669cf41dfabc51af8fef7b443a8110070347d511 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 17:12:43 -0700 Subject: [PATCH 23/26] Fixes capitalization --- aws/config-aws.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aws/config-aws.yaml b/aws/config-aws.yaml index 38797bc1..87931e52 100644 --- a/aws/config-aws.yaml +++ b/aws/config-aws.yaml @@ -16,7 +16,7 @@ hub: # This is the image I built based off of jupyterhub/k8s-hub, 3.0.2 at time of writing this image: - name: ghcr.io/LLNL/thicket-tutorial-hub + name: ghcr.io/llnl/thicket-tutorial-hub tag: "radiuss-2024" pullPolicy: Always @@ -31,7 +31,7 @@ scheduling: # This is the "spawn" image singleuser: image: - name: ghcr.io/LLNL/thicket-tutorial-spawn + name: ghcr.io/llnl/thicket-tutorial-spawn tag: "radiuss-2024" pullPolicy: Always cpu: @@ -43,7 +43,7 @@ singleuser: # This runs as the root user, who clones and changes ownership to uid 1000 initContainers: - name: init-myservice - image: ghcr.io/LLNL/thicket-tutorial-init:radiuss-2024 + image: ghcr.io/llnl/thicket-tutorial-init:radiuss-2024 command: ["/entrypoint.sh"] volumeMounts: - name: thicket-tutorial From b92fc87b845e678e8743f26b90026e8a8df37f11 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 17:20:16 -0700 Subject: [PATCH 24/26] Fixes perms on init-entrypoint.sh --- docker/init-entrypoint.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 docker/init-entrypoint.sh diff --git a/docker/init-entrypoint.sh b/docker/init-entrypoint.sh old mode 100644 new mode 100755 From 8d71165ff14bd39d2f6841514d2746ae9578de5c Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 17:43:13 -0700 Subject: [PATCH 25/26] Updates pull policy for init --- aws/config-aws.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/aws/config-aws.yaml b/aws/config-aws.yaml index 87931e52..d0c93003 100644 --- a/aws/config-aws.yaml +++ b/aws/config-aws.yaml @@ -43,6 +43,7 @@ singleuser: # This runs as the root user, who clones and changes ownership to uid 1000 initContainers: - name: init-myservice + pullPolicy: Always image: ghcr.io/llnl/thicket-tutorial-init:radiuss-2024 command: ["/entrypoint.sh"] volumeMounts: From 1f43da3b11c34f7abfc3b2e6324052a8bec60e52 Mon Sep 17 00:00:00 2001 From: Ian Lumsden Date: Tue, 6 Aug 2024 17:48:42 -0700 Subject: [PATCH 26/26] Remove pull policy from initContainers --- aws/config-aws.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/aws/config-aws.yaml b/aws/config-aws.yaml index d0c93003..87931e52 100644 --- a/aws/config-aws.yaml +++ b/aws/config-aws.yaml @@ -43,7 +43,6 @@ singleuser: # This runs as the root user, who clones and changes ownership to uid 1000 initContainers: - name: init-myservice - pullPolicy: Always image: ghcr.io/llnl/thicket-tutorial-init:radiuss-2024 command: ["/entrypoint.sh"] volumeMounts: