From 88ab0cf9e1300477e49af838454308df26ceb87d Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Tue, 7 May 2024 11:49:52 +1000 Subject: [PATCH 01/22] Add readiness probe to operators. --- .../config/10-secrets-manager/07-deployments.yaml | 12 ++++++++++++ .../config/11-session-manager/07-deployments.yaml | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml b/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml index 805d48d9b..7c12076c5 100644 --- a/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml +++ b/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml @@ -37,7 +37,19 @@ spec: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + httpGet: + path: /healthz + port: 8080 livenessProbe: + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 httpGet: path: /healthz port: 8080 diff --git a/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml b/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml index 8ad051628..e21db5cf1 100644 --- a/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml +++ b/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml @@ -37,7 +37,19 @@ spec: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + httpGet: + path: /healthz + port: 8080 livenessProbe: + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 httpGet: path: /healthz port: 8080 From af82e46c5c92d7a0c59dc736f578d264366f9406 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Tue, 7 May 2024 11:50:29 +1000 Subject: [PATCH 02/22] Ensure DNS can resolve DNS names before starting operators. --- secrets-manager/main.py | 43 ++++++++++++++++++++++++++++++++++--- session-manager/main.py | 47 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/secrets-manager/main.py b/secrets-manager/main.py index f9a9524e5..1c60e5344 100644 --- a/secrets-manager/main.py +++ b/secrets-manager/main.py @@ -2,12 +2,51 @@ import contextlib import logging import signal +import socket +import time from threading import Thread, Event import kopf import pykube + +logger = logging.getLogger("educates") + + +def check_dns_is_ready(): + # Check that DNS is actually ready and able to resolve the DNS for the + # Kubernetes control plane. This is a workaround for the fact that the DNS + # service may not be ready when the pod starts. Check at intervals but bail + # out and raise the original exception if we can't resolve the DNS name + # after 60 seconds. + + logger.info("Checking DNS resolution for Kubernetes control plane.") + + start_time = time.time() + + while True: + try: + socket.getaddrinfo("kubernetes.default.svc", 0, flags=socket.AI_CANONNAME) + break + except socket.gaierror: + if time.time() - start_time > 60: + raise + + # Wait for 1 second before trying again. + + logger.info("DNS resolution for Kubernetes control plane is not ready yet, sleeping...") + + time.sleep(1) + + logger.info("DNS resolution for Kubernetes control plane is ready.") + + +# Check that DNS is actually ready before importing the handlers. + +check_dns_is_ready() + + from handlers import namespace from handlers import secret from handlers import secretcopier @@ -18,8 +57,6 @@ _event_loop = None # pylint: disable=invalid-name -logger = logging.getLogger("educates") - _stop_flag = Event() @@ -36,7 +73,7 @@ def login_fn(**kwargs): return kopf.login_via_pykube(**kwargs) -@kopf.on.probe(id='api') +@kopf.on.probe(id="api") def check_api_access(**kwargs): try: api = pykube.HTTPClient(pykube.KubeConfig.from_env()) diff --git a/session-manager/main.py b/session-manager/main.py index b972945e5..fc098c5a8 100644 --- a/session-manager/main.py +++ b/session-manager/main.py @@ -7,12 +7,52 @@ import contextlib import logging import signal +import socket +import time from threading import Thread, Event import kopf import pykube + +logger = logging.getLogger("educates") +logger.setLevel(logging.DEBUG) + + +def check_dns_is_ready(): + # Check that DNS is actually ready and able to resolve the DNS for the + # Kubernetes control plane. This is a workaround for the fact that the DNS + # service may not be ready when the pod starts. Check at intervals but bail + # out and raise the original exception if we can't resolve the DNS name + # after 60 seconds. + + logger.info("Checking DNS resolution for Kubernetes control plane.") + + start_time = time.time() + + while True: + try: + socket.getaddrinfo("kubernetes.default.svc", 0, flags=socket.AI_CANONNAME) + break + except socket.gaierror: + if time.time() - start_time > 60: + raise + + # Wait for 1 second before trying again. + + logger.info("DNS resolution for Kubernetes control plane is not ready yet, sleeping...") + + time.sleep(1) + + logger.info("DNS resolution for Kubernetes control plane is ready.") + + +# Check that DNS is actually ready before importing the handlers. + +check_dns_is_ready() + + from handlers import workshopenvironment from handlers import workshopsession from handlers import workshopallocation @@ -20,10 +60,7 @@ from handlers import daemons -_event_loop = None # pylint: disable=invalid-name - -logger = logging.getLogger("educates") -logger.setLevel(logging.DEBUG) +_event_loop = None # pylint: disable=invalid-nam _stop_flag = Event() @@ -41,7 +78,7 @@ def login_fn(**kwargs): return kopf.login_via_pykube(**kwargs) -@kopf.on.probe(id='api') +@kopf.on.probe(id="api") def check_api_access(**kwargs): try: api = pykube.HTTPClient(pykube.KubeConfig.from_env()) From 5e4f1f4d02c4fb1e1f586e9fc8a69be93f7438ee Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Tue, 7 May 2024 12:09:09 +1000 Subject: [PATCH 03/22] Add change note and DNS issues on startup. --- project-docs/release-notes/version-2.7.1.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/project-docs/release-notes/version-2.7.1.md b/project-docs/release-notes/version-2.7.1.md index 91f82c0e4..fb5aeb63f 100644 --- a/project-docs/release-notes/version-2.7.1.md +++ b/project-docs/release-notes/version-2.7.1.md @@ -20,3 +20,9 @@ Bugs Fixed use `netcat` by installing `netcat` package instead of `nc`. The `ncat` package is also installed if want newer variant of `nc`, but you will need to use the `ncat` command explicitly. + +* If the cluster DNS server was slow to start resolving DNS names after a new + node was started, the session manager could fail on startup and enter crash + loop back off state. To remedy both session manager and secrets manager now + ensure DNS is able to resolve cluster control plane DNS name before starting + up. Readiness probes also added to these two operators. From c3c9e3a4047851f2f414581b9d154e4709410e9b Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Tue, 7 May 2024 20:56:05 +1000 Subject: [PATCH 04/22] Readiness probe should have been a startup probe. --- .../bundle/config/10-secrets-manager/07-deployments.yaml | 4 ++-- .../bundle/config/11-session-manager/07-deployments.yaml | 4 ++-- project-docs/release-notes/version-2.7.1.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml b/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml index 7c12076c5..0bb3d8d2e 100644 --- a/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml +++ b/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml @@ -37,7 +37,7 @@ spec: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] - readinessProbe: + startupProbe: initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 @@ -46,7 +46,7 @@ spec: path: /healthz port: 8080 livenessProbe: - initialDelaySeconds: 15 + initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 failureThreshold: 3 diff --git a/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml b/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml index e21db5cf1..3fecadb8d 100644 --- a/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml +++ b/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml @@ -37,7 +37,7 @@ spec: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] - readinessProbe: + startupProbe: initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 @@ -46,7 +46,7 @@ spec: path: /healthz port: 8080 livenessProbe: - initialDelaySeconds: 15 + initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 failureThreshold: 3 diff --git a/project-docs/release-notes/version-2.7.1.md b/project-docs/release-notes/version-2.7.1.md index fb5aeb63f..0fba7c900 100644 --- a/project-docs/release-notes/version-2.7.1.md +++ b/project-docs/release-notes/version-2.7.1.md @@ -25,4 +25,4 @@ Bugs Fixed node was started, the session manager could fail on startup and enter crash loop back off state. To remedy both session manager and secrets manager now ensure DNS is able to resolve cluster control plane DNS name before starting - up. Readiness probes also added to these two operators. + up. Startup probes have also been added to these two operators. From 23c50d1ba768f0006a772334508ccd7b3af6c455 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Wed, 8 May 2024 09:08:36 +1000 Subject: [PATCH 05/22] Adjust startup/liveness probes configuration. --- .../bundle/config/10-secrets-manager/07-deployments.yaml | 8 ++++---- .../bundle/config/11-session-manager/07-deployments.yaml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml b/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml index 0bb3d8d2e..3b98a4005 100644 --- a/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml +++ b/carvel-packages/training-platform/bundle/config/10-secrets-manager/07-deployments.yaml @@ -38,12 +38,12 @@ spec: capabilities: drop: ["ALL"] startupProbe: - initialDelaySeconds: 5 + initialDelaySeconds: 15 periodSeconds: 5 successThreshold: 1 - failureThreshold: 3 + failureThreshold: 4 httpGet: - path: /healthz + path: /healthz?probe=startup port: 8080 livenessProbe: initialDelaySeconds: 5 @@ -51,7 +51,7 @@ spec: successThreshold: 1 failureThreshold: 3 httpGet: - path: /healthz + path: /healthz?probe=liveness port: 8080 volumeMounts: - name: config diff --git a/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml b/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml index 3fecadb8d..03aa8372e 100644 --- a/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml +++ b/carvel-packages/training-platform/bundle/config/11-session-manager/07-deployments.yaml @@ -38,12 +38,12 @@ spec: capabilities: drop: ["ALL"] startupProbe: - initialDelaySeconds: 5 + initialDelaySeconds: 15 periodSeconds: 5 successThreshold: 1 - failureThreshold: 3 + failureThreshold: 4 httpGet: - path: /healthz + path: /healthz?probe=startup port: 8080 livenessProbe: initialDelaySeconds: 5 @@ -51,7 +51,7 @@ spec: successThreshold: 1 failureThreshold: 3 httpGet: - path: /healthz + path: /healthz?probe=liveness port: 8080 volumeMounts: - name: config From 3822460303ea2af90913818f65b090c72344fe4e Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Mon, 13 May 2024 09:32:26 +1000 Subject: [PATCH 06/22] Use default cluster domain where cluster DNS doesn't include it. --- session-manager/handlers/operator_config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/session-manager/handlers/operator_config.py b/session-manager/handlers/operator_config.py index a330ce701..266d470ca 100644 --- a/session-manager/handlers/operator_config.py +++ b/session-manager/handlers/operator_config.py @@ -42,7 +42,11 @@ RUNTIME_CLASS = xget(config_values, "clusterRuntime.class", "") CLUSTER_DOMAIN = socket.getaddrinfo("kubernetes.default.svc", 0, flags=socket.AI_CANONNAME)[0][3] -CLUSTER_DOMAIN = CLUSTER_DOMAIN.replace("kubernetes.default.svc.", "") + +if CLUSTER_DOMAIN.startswith("kubernetes.default.svc."): + CLUSTER_DOMAIN = CLUSTER_DOMAIN.replace("kubernetes.default.svc.", "") +else: + CLUSTER_DOMAIN = "cluster.local" INGRESS_DOMAIN = xget(config_values, "clusterIngress.domain", "educates-local-dev.test") INGRESS_CLASS = xget(config_values, "clusterIngress.class", "") From cdd1ecaacd2548c1a397a6dcc810875f324f1535 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Mon, 13 May 2024 10:11:58 +1000 Subject: [PATCH 07/22] Add change notes for cluster domain FQDN fix. --- project-docs/release-notes/version-2.7.1.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/project-docs/release-notes/version-2.7.1.md b/project-docs/release-notes/version-2.7.1.md index 0fba7c900..268c4758b 100644 --- a/project-docs/release-notes/version-2.7.1.md +++ b/project-docs/release-notes/version-2.7.1.md @@ -26,3 +26,9 @@ Bugs Fixed loop back off state. To remedy both session manager and secrets manager now ensure DNS is able to resolve cluster control plane DNS name before starting up. Startup probes have also been added to these two operators. + +* If the cluster DNS didn't return a FQDN for the `kubernetes.default.svc` when + queried by that name, the value of the `CLUSTER_DOMAIN` variable provided to + the workshop sessions would be incorrect. This was occuring when Educates was + installed into some versions of a virtual cluster. When the returned host name + is not a FQDN, then `cluster.local` will now be used. From a0f64e8b1e6d8454d88ce9e2d9e452ed9a83ab27 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Mon, 13 May 2024 10:19:27 +1000 Subject: [PATCH 08/22] Add notes about Hugo renderer and embedding Javascript. --- .../workshop-content/workshop-instructions.md | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/project-docs/workshop-content/workshop-instructions.md b/project-docs/workshop-content/workshop-instructions.md index 69a8c312e..cd872d3a7 100644 --- a/project-docs/workshop-content/workshop-instructions.md +++ b/project-docs/workshop-content/workshop-instructions.md @@ -1188,7 +1188,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin justo. ``` -If using the ``classic`` render and AsciiDoc, HTML can be embedded by using a passthrough block. +If using the ``classic`` renderer and AsciiDoc, HTML can be embedded by using a passthrough block. ``` Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin justo. @@ -1218,7 +1218,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin justo. ``` -If using the ``hugo`` renderer, it provides as standard various shortcodes for embedding different custom HTML snippets, such as embedding videos or images. If you have a custom requirement of your own, you will need to provide your own shortcode by placing it in the ``workshop/layouts/shortcodes`` directory. +If using the ``hugo`` renderer, it provides as standard various shortcodes for embedding different custom HTML snippets, such as embedding videos or images. If you have a custom requirement of your own, you will need to provide your own shortcode by placing it in the ``workshop/layouts/shortcodes`` directory and referencing that in your instructions. In all cases it is recommended that the HTML consist of only a single HTML element. If you have more than one, include them all in a ``div`` element. The latter is necessary if any of the HTML elements are marked as hidden and the embedded HTML will be a part of a collapsible section. If you don't ensure the hidden HTML element is placed under the single top level ``div`` element, the hidden HTML element will end up being made visible when the collapsible section is expanded. @@ -1232,6 +1232,8 @@ Triggering actions from Javascript Clickable actions can be embedded in workshop instructions and reduce the manual steps that workhop users need to perform. If further automation is required, a subset of the underlying tasks which can be triggered through clickable actions can be executed from Javascript code embedded within the workshop instructions page. This can be used for tasks such as ensuring that a dashboard tab is made visible immediately a page in the workshop instructions is viewed. +If using the ``classic`` renderer and Markdown, the Javascript can be embedded directly within the Markdown document. + ``` ``` +If using the ``classic`` renderer and AsciiDoc, HTML can be embedded by using a passthrough block. + +``` +++++ + +++++ +``` + +If using the ``hugo`` renderer you will need to provide your own shortcode for embedding custom Javascript into a page by placing it in the ``workshop/layouts/shortcodes`` directory and referencing that in your instructions. + All accessible functions are defined within the scope of the `educates` object. The available API is described by: ``` From 04a9a05e2e23718e09ddea4ca26d2323d01b5842 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Mon, 13 May 2024 10:27:13 +1000 Subject: [PATCH 09/22] Add note that Learning Center workshop images will need to be rebuilt for Educates. --- project-docs/workshop-migration/learning-center.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project-docs/workshop-migration/learning-center.md b/project-docs/workshop-migration/learning-center.md index e60efaa2d..d33c5ce2a 100644 --- a/project-docs/workshop-migration/learning-center.md +++ b/project-docs/workshop-migration/learning-center.md @@ -88,6 +88,8 @@ Note that whereas Learning Center only bundled a single workshop base image, Edu ``conda-environment:*`` - A tagged version of the ``conda-environment`` workshop image which has been matched with the current version of the Educates operator. +Note that any custom workshop images you may have created for Learning Center will need to be rebuilt using the corresponding workshop base image from Educates, as existing Learning Center based images will not work in Educates. + Downloading of workshop content ------------------------------- From 3bbd029c7ac8781327332f7d92ddde9705a63c54 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 16 May 2024 13:06:12 +1000 Subject: [PATCH 10/22] Allow per session override of workshop session container configuration. --- session-manager/handlers/workshopsession.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/session-manager/handlers/workshopsession.py b/session-manager/handlers/workshopsession.py index 917225f19..75703a40b 100644 --- a/session-manager/handlers/workshopsession.py +++ b/session-manager/handlers/workshopsession.py @@ -1336,6 +1336,22 @@ def resolve_security_policy(name): time.sleep(0.1) continue + # Work out the name of the workshop config secret to use for a session. This + # will usually be the common workshop-config secret created with the + # workshop environment, but if the request.objects contains a secret with + # name same as $(session_name)-config, then use that instead. + + workshop_config_secret_name = "workshop-config" + + request_objects = workshop_spec.get("request", {}).get("objects", []) + + for object_body in request_objects: + if object_body["kind"] == "Secret": + object_name = substitute_variables(object_body["metadata"]["name"], session_variables) + if object_name == f"{session_name}-config": + workshop_config_secret_name = f"{session_name}-config" + break + # Next setup the deployment resource for the workshop dashboard. Note that # spec.content.image is deprecated and should use spec.workshop.image. We # will check both. @@ -1566,7 +1582,7 @@ def resolve_security_policy(name): "volumes": [ { "name": "workshop-config", - "secret": {"secretName": "workshop-config"}, + "secret": {"secretName": workshop_config_secret_name}, }, { "name": "workshop-theme", From 247b219a7173ab3feb8207d44160cef6206108a5 Mon Sep 17 00:00:00 2001 From: Daniel Bodky Date: Thu, 16 May 2024 21:25:52 +0200 Subject: [PATCH 11/22] Add admonition shortcodes to workshop-content docs (#342) --- developer-docs/build-instructions.md | 10 ++++++ project-docs/workshop-content/admonitions.png | Bin 0 -> 19190 bytes .../workshop-content/workshop-instructions.md | 31 ++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 project-docs/workshop-content/admonitions.png diff --git a/developer-docs/build-instructions.md b/developer-docs/build-instructions.md index 4060149db..a8f147be5 100644 --- a/developer-docs/build-instructions.md +++ b/developer-docs/build-instructions.md @@ -257,3 +257,13 @@ make prune-all Note that this will run `docker system prune` rather than `docker image prune`, which will also result in unused docker networks and volumes being cleaned up. Also note that this doesn't reclaim space used by the image cache of `containerd` on the Kubernetes cluster nodes. If you are doing a lot of work on Educates, especially changes to the workshop base images and you deploy workshops using many successive versions of the images, eventually you can run out of storage space due to the `containerd` image cache. In this case there isn't really anything simple you do can except for deleting the Kubernetes cluster and starting over. + +Building docs.educates.dev locally +---------------------------------- + +If you're working on updates or additions to the project documentation served at [docs.educates.dev](https://docs.educates.dev), you might want to preview your changes locally before opening a PR. To build and preview the docs locally, you can run: + +``` +make build-project-docs +make open-project-docs +``` diff --git a/project-docs/workshop-content/admonitions.png b/project-docs/workshop-content/admonitions.png new file mode 100644 index 0000000000000000000000000000000000000000..bd557adbe8063d27fa40090778a7ac1135c584fb GIT binary patch literal 19190 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 D$_K>b literal 0 HcmV?d00001 diff --git a/project-docs/workshop-content/workshop-instructions.md b/project-docs/workshop-content/workshop-instructions.md index cd872d3a7..e5aeebef0 100644 --- a/project-docs/workshop-content/workshop-instructions.md +++ b/project-docs/workshop-content/workshop-instructions.md @@ -1155,6 +1155,37 @@ The shortcode for selecting based on the pathway is for example implemented as: {{ end }} ``` +Adding admonitions with shortcodes +---------------------------------- + +Since Educates v2.6.0, a range of custom admonitions is supported when using the ``hugo`` renderer. Currently, three types of admonitions exist: + +- **note** - rendered as blue text box +- **warning** - rendered as yellow text box +- **danger** - rendered as red text box + +The shortcodes can be used like this, with the respective admonition name as shortcode: + +``` +{{< note >}} +A friendly admonition. +{{< /note >}} + +{{< warning >}} +Consider this admonition. +{{< /warning >}} + +{{< danger >}} +You better consider this admonition! +{{< /danger >}} +``` + +The rendered version looks like this: + +![Rendered admonitions supported by Educates](admonitions.png) + +More information on shortcodes can be found in the [Hugo documentation](https://gohugo.io/content-management/shortcodes/). + Embedding custom HTML content ----------------------------- From 8715e718320085712c086147e5b89b46c12906d4 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Fri, 17 May 2024 12:30:34 +1000 Subject: [PATCH 12/22] Remove use of environment variables by operator to configure workshop session container. --- session-manager/handlers/workshopsession.py | 43 +------------------ .../opt/eduk8s/etc/profile.d/02-console.sh | 2 +- .../opt/eduk8s/sbin/start-container | 30 ++++++------- .../opt/eduk8s/sbin/start-gateway | 4 +- 4 files changed, 19 insertions(+), 60 deletions(-) diff --git a/session-manager/handlers/workshopsession.py b/session-manager/handlers/workshopsession.py index 75703a40b..491504a2a 100644 --- a/session-manager/handlers/workshopsession.py +++ b/session-manager/handlers/workshopsession.py @@ -1991,61 +1991,20 @@ def _apply_environment_patch(patch): _apply_environment_patch(spec["session"].get("env", [])) - # Set environment variable to specify location of workshop content - # and to denote whether applications are enabled. + # Add additional labels for any applications which have been enabled. additional_env = [] additional_labels = {} - files = workshop_spec.get("content", {}).get("files") - - if files: - additional_env.append({"name": "DOWNLOAD_URL", "value": files}) - for application in applications: - application_tag = application.upper().replace("-", "_") if applications.is_enabled(application): - additional_env.append( - {"name": "ENABLE_" + application_tag, "value": "true"} - ) additional_labels[ f"training.{OPERATOR_API_GROUP}/session.applications.{application.lower()}" ] = "true" - else: - additional_env.append( - {"name": "ENABLE_" + application_tag, "value": "false"} - ) - - # Add in extra configuration for workshop. - - if applications.is_enabled("workshop") or applications.property("workshop", "url"): - additional_env.append( - { - "name": "WORKSHOP_LAYOUT", - "value": applications.property("workshop", "layout", "default"), - } - ) - - # Add in extra configuration for terminal. - - if applications.is_enabled("terminal"): - additional_env.append( - { - "name": "TERMINAL_LAYOUT", - "value": applications.property("terminal", "layout", "default"), - } - ) # Add in extra configuation for web console. if applications.is_enabled("console"): - additional_env.append( - { - "name": "CONSOLE_VENDOR", - "value": applications.property("console", "vendor", "kubernetes"), - } - ) - if applications.property("console", "vendor", "kubernetes") == "kubernetes": secret_body = { "apiVersion": "v1", diff --git a/workshop-images/base-environment/opt/eduk8s/etc/profile.d/02-console.sh b/workshop-images/base-environment/opt/eduk8s/etc/profile.d/02-console.sh index 929cefa73..9a6813bab 100644 --- a/workshop-images/base-environment/opt/eduk8s/etc/profile.d/02-console.sh +++ b/workshop-images/base-environment/opt/eduk8s/etc/profile.d/02-console.sh @@ -17,7 +17,7 @@ if [ x"$ENABLE_CONSOLE" != x"true" -o ! -f $HOME/.kube/config ]; then return fi -CONSOLE_VENDOR=${CONSOLE_VENDOR:-$(workshop-definition -r '(.spec.session.applications.console.vendor // "kubernetes")')} +CONSOLE_VENDOR=$(workshop-definition -r '(.spec.session.applications.console.vendor // "kubernetes")') case $CONSOLE_VENDOR in octant) diff --git a/workshop-images/base-environment/opt/eduk8s/sbin/start-container b/workshop-images/base-environment/opt/eduk8s/sbin/start-container index efd6e913c..b865798b9 100755 --- a/workshop-images/base-environment/opt/eduk8s/sbin/start-container +++ b/workshop-images/base-environment/opt/eduk8s/sbin/start-container @@ -105,7 +105,7 @@ elif [ -d /opt/assets/files ]; then else # In this case downloading workshop the old way which has been deprecated. - DOWNLOAD_URL=${DOWNLOAD_URL:-$(workshop-definition -r '(.spec.content.files // "")')} + DOWNLOAD_URL=$(workshop-definition -r '(.spec.content.files // "")') if [ x"$DOWNLOAD_URL" != x"" ]; then (download-workshop "$DOWNLOAD_URL" || touch $DOWNLOAD_FAILED) 2>&1 | tee -a $DOWNLOAD_LOGFILE @@ -125,20 +125,20 @@ ENABLE_GATEWAY=${ENABLE_GATEWAY:-true} ENABLE_DASHBOARD=${ENABLE_DASHBOARD:-true} -ENABLE_CONSOLE=${ENABLE_CONSOLE:-$(application-enabled console false)} -ENABLE_DOCKER=${ENABLE_DOCKER:-$(application-enabled docker false)} -ENABLE_EDITOR=${ENABLE_EDITOR:-$(application-enabled editor false)} -ENABLE_EXAMINER=${ENABLE_EXAMINER:-$(application-enabled examiner false)} -ENABLE_GIT=${ENABLE_GIT:-$(application-enabled git false)} -ENABLE_FILES=${ENABLE_FILES:-$(application-enabled files false)} -ENABLE_REGISTRY=${ENABLE_REGISTRY:-$(application-enabled registry false)} -ENABLE_SLIDES=${ENABLE_SLIDES:-$(application-enabled slides false)} -ENABLE_SSHD=${ENABLE_SSHD:-$(application-enabled sshd false)} -ENABLE_TERMINAL=${ENABLE_TERMINAL:-$(application-enabled terminal true)} -ENABLE_UPLOADS=${ENABLE_UPLOADS:-$(application-enabled uploads false)} -ENABLE_VCLUSTER=${ENABLE_VCLUSTER:-$(application-enabled vcluster false)} -ENABLE_WEBDAV=${ENABLE_WEBDAV:-$(application-enabled webdav false)} -ENABLE_WORKSHOP=${ENABLE_WORKSHOP:-$(application-enabled workshop true)} +ENABLE_CONSOLE=$(application-enabled console false) +ENABLE_DOCKER=$(application-enabled docker false) +ENABLE_EDITOR=$(application-enabled editor false) +ENABLE_EXAMINER=$(application-enabled examiner false) +ENABLE_GIT=$(application-enabled git false) +ENABLE_FILES=$(application-enabled files false) +ENABLE_REGISTRY=$(application-enabled registry false) +ENABLE_SLIDES=$(application-enabled slides false) +ENABLE_SSHD=$(application-enabled sshd false) +ENABLE_TERMINAL=$(application-enabled terminal true) +ENABLE_UPLOADS=$(application-enabled uploads false) +ENABLE_VCLUSTER=$(application-enabled vcluster false) +ENABLE_WEBDAV=$(application-enabled webdav false) +ENABLE_WORKSHOP=$(application-enabled workshop true) if [ x"$SUPERVISOR_ONLY" == x"true" ]; then ENABLE_GATEWAY=false diff --git a/workshop-images/base-environment/opt/eduk8s/sbin/start-gateway b/workshop-images/base-environment/opt/eduk8s/sbin/start-gateway index 6d173b4c6..8811dc38d 100755 --- a/workshop-images/base-environment/opt/eduk8s/sbin/start-gateway +++ b/workshop-images/base-environment/opt/eduk8s/sbin/start-gateway @@ -6,10 +6,10 @@ set -x XDG_CONFIG_HOME=/tmp/.config export XDG_CONFIG_HOME -WORKSHOP_LAYOUT=${WORKSHOP_LAYOUT=`workshop-definition -r '(.spec.session.applications.workshop.layout // "default")'`} +WORKSHOP_LAYOUT=$(workshop-definition -r '(.spec.session.applications.workshop.layout // "default")') export WORKSHOP_LAYOUT -TERMINAL_LAYOUT=${TERMINAL_LAYOUT=`workshop-definition -r '(.spec.session.applications.terminal.layout // "default")'`} +TERMINAL_LAYOUT=$(workshop-definition -r '(.spec.session.applications.terminal.layout // "default")') export TERMINAL_LAYOUT EXERCISES_DIR=${EXERCISES_DIR:-exercises} From 0d94ee4dff02cd3cdb33fbe40ab55b856ab52b61 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Fri, 17 May 2024 23:20:45 +1000 Subject: [PATCH 13/22] Add change note about workshop config override fixes. --- project-docs/release-notes/version-2.7.1.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/project-docs/release-notes/version-2.7.1.md b/project-docs/release-notes/version-2.7.1.md index 268c4758b..dc6066828 100644 --- a/project-docs/release-notes/version-2.7.1.md +++ b/project-docs/release-notes/version-2.7.1.md @@ -32,3 +32,8 @@ Bugs Fixed the workshop sessions would be incorrect. This was occuring when Educates was installed into some versions of a virtual cluster. When the returned host name is not a FQDN, then `cluster.local` will now be used. + +* Workshop session dashboard configuration could not in some cases be overridden + from inside of the workshop session by modifying the injected workshop + definition. This included not being able to change workshop/terminal layout + and whether the dashboard tabs for the editor and console were displayed. From 05eafecfd823792a20645f4474e315fb73f6c5fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 05:26:24 +0000 Subject: [PATCH 14/22] --- updated-dependencies: - dependency-name: requests dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- training-portal/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/training-portal/requirements.txt b/training-portal/requirements.txt index 1427d06b0..96824d245 100644 --- a/training-portal/requirements.txt +++ b/training-portal/requirements.txt @@ -7,7 +7,7 @@ warpdrive>=0.34.0 wrapt==1.16.0 django-oauth-toolkit==2.3.0 django-cors-headers==4.3.1 -requests==2.31.0 +requests==2.32.0 django-csp==3.7 kopf[full-auth]==1.36.2 pykube-ng==23.6.0 From 6220526316e6bd4c94d7cbe049d0bcf9a2f95bff Mon Sep 17 00:00:00 2001 From: Daniel Bodky Date: Wed, 22 May 2024 16:13:28 +0200 Subject: [PATCH 15/22] Update CSP directives targeting Google Analytics (#378) --- project-docs/release-notes/version-2.7.1.md | 5 +++++ training-portal/src/project/settings.py | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/project-docs/release-notes/version-2.7.1.md b/project-docs/release-notes/version-2.7.1.md index dc6066828..00b54e0f7 100644 --- a/project-docs/release-notes/version-2.7.1.md +++ b/project-docs/release-notes/version-2.7.1.md @@ -37,3 +37,8 @@ Bugs Fixed from inside of the workshop session by modifying the injected workshop definition. This included not being able to change workshop/terminal layout and whether the dashboard tabs for the editor and console were displayed. + +* The builtin Google Analytics integration was broken due to the `TrainingPortal` + Content Security Policy (CSP) directives declaring outdated sources. The CSPs + now allow for `*.google-analytics.com` and `*.googletagmanager.com` to be + referenced. diff --git a/training-portal/src/project/settings.py b/training-portal/src/project/settings.py index f67769858..18bbd82ca 100644 --- a/training-portal/src/project/settings.py +++ b/training-portal/src/project/settings.py @@ -227,7 +227,7 @@ CSP_CONNECT_SRC = ( "'self'", f"*.{INGRESS_DOMAIN}", - "www.google-analytics.com", + "*.google-analytics.com", "*.clarity.ms", "c.bing.com", "*.amplitude.com", @@ -240,8 +240,8 @@ CSP_IMG_SRC = ( "'self'", "data:", - "www.google-analytics.com", - "www.googletagmanager.com", + "*.google-analytics.com", + "*.googletagmanager.com", ) CSP_FONT_SRC = ("'self'",) CSP_FRAME_SRC = ("'self'",) From 73d757343a62e35225512c85369f960afa81ef95 Mon Sep 17 00:00:00 2001 From: Daniel Bodky Date: Wed, 22 May 2024 16:25:17 +0200 Subject: [PATCH 16/22] Adjust CSRF_ALLOWED_ORIGINS setting for TrainingPortal `CSRF_ALLOWED_ORIGINS` was configured only for default TrainingPortal names. If instead a custom `PORTAL_HOSTNAME` had been provided, CSRF verification would break. We now default to `PORTAL_HOSTNAME` for `CSRF_ALLOWED_ORIGINS` and only fall back to the previous default implementation if none was provided. Fixes #379. --- project-docs/release-notes/version-2.7.1.md | 6 ++++++ training-portal/src/project/settings.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/project-docs/release-notes/version-2.7.1.md b/project-docs/release-notes/version-2.7.1.md index 00b54e0f7..939144ced 100644 --- a/project-docs/release-notes/version-2.7.1.md +++ b/project-docs/release-notes/version-2.7.1.md @@ -42,3 +42,9 @@ Bugs Fixed Content Security Policy (CSP) directives declaring outdated sources. The CSPs now allow for `*.google-analytics.com` and `*.googletagmanager.com` to be referenced. + +* The `CSRF_ALLOWED_ORIGINS` setting for the `TrainingPortal` Django backend was + breaking CSRF verification for any `TrainingPortal` with a custom + `PORTAL_HOSTNAME` configured. We now use the `PORTAL_HOSTNAME` as allowed + CSRF origin and only fall back to the previous implementation if no custom + hostname was provided. diff --git a/training-portal/src/project/settings.py b/training-portal/src/project/settings.py index 18bbd82ca..f59893de2 100644 --- a/training-portal/src/project/settings.py +++ b/training-portal/src/project/settings.py @@ -248,7 +248,7 @@ CSP_INCLUDE_NONCE_IN = ("script-src",) CSP_FRAME_ANCESTORS = ("'self'",) -CSRF_TRUSTED_ORIGINS = [f"{INGRESS_PROTOCOL}://{TRAINING_PORTAL}-ui.{INGRESS_DOMAIN}"] +CSRF_TRUSTED_ORIGINS = [f"{INGRESS_PROTOCOL}://{PORTAL_HOSTNAME}"] FRAME_ANCESTORS = os.environ.get("FRAME_ANCESTORS", "") From f2f95a72cd97ff0360696575ccd5558fced8418d Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 23 May 2024 10:05:26 +1000 Subject: [PATCH 17/22] Workshop title in TOC was not being populated correctly. --- project-docs/release-notes/version-2.7.1.md | 4 ++++ .../opt/eduk8s/etc/templates/workshop-variables.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/project-docs/release-notes/version-2.7.1.md b/project-docs/release-notes/version-2.7.1.md index 939144ced..800b3ee25 100644 --- a/project-docs/release-notes/version-2.7.1.md +++ b/project-docs/release-notes/version-2.7.1.md @@ -48,3 +48,7 @@ Bugs Fixed `PORTAL_HOSTNAME` configured. We now use the `PORTAL_HOSTNAME` as allowed CSRF origin and only fall back to the previous implementation if no custom hostname was provided. + +* The workshop title in the dropdown TOC of the workshop instructions was not + being populated with the workshop title from the workshop definition when the + Hugo renderer was being used. diff --git a/workshop-images/base-environment/opt/eduk8s/etc/templates/workshop-variables.yaml b/workshop-images/base-environment/opt/eduk8s/etc/templates/workshop-variables.yaml index 5576e5da0..cb0904600 100644 --- a/workshop-images/base-environment/opt/eduk8s/etc/templates/workshop-variables.yaml +++ b/workshop-images/base-environment/opt/eduk8s/etc/templates/workshop-variables.yaml @@ -36,7 +36,7 @@ #@ path = pathways.get(pathway_name, {}) -#@ workshop_title = xgetattr(data.values, "workshop_name", "Workshop") +#@ workshop_title = xgetattr(data.values, "workshop_title", "Workshop") #@ workshop_title = path.get("title", workshop_title) #@ workshop_description = xgetattr(data.values, "workshop_description", "") From d1ede5e3dd9157b145a140e2e8a7c65ac61dc64f Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 23 May 2024 10:17:32 +1000 Subject: [PATCH 18/22] Remove duplicate config setting. --- training-portal/src/project/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/training-portal/src/project/settings.py b/training-portal/src/project/settings.py index f59893de2..1f4a3f519 100644 --- a/training-portal/src/project/settings.py +++ b/training-portal/src/project/settings.py @@ -235,7 +235,6 @@ CSP_DEFAULT_SRC = ("'none'",) CSP_STYLE_SRC = ("'self'",) -CSP_SCRIPT_SRC = ("'self'",) CSP_SCRIPT_SRC = ("'self'", "www.clarity.ms", "cdn.amplitude.com") CSP_IMG_SRC = ( "'self'", From d391301afca3bee60985af62482c047c2fcf1da5 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 23 May 2024 10:41:37 +1000 Subject: [PATCH 19/22] Increase timeout for workshop session registration. --- project-docs/release-notes/version-2.7.1.md | 9 +++++++++ session-manager/handlers/workshopallocation.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/project-docs/release-notes/version-2.7.1.md b/project-docs/release-notes/version-2.7.1.md index 800b3ee25..83d7a621c 100644 --- a/project-docs/release-notes/version-2.7.1.md +++ b/project-docs/release-notes/version-2.7.1.md @@ -52,3 +52,12 @@ Bugs Fixed * The workshop title in the dropdown TOC of the workshop instructions was not being populated with the workshop title from the workshop definition when the Hugo renderer was being used. + +* If a workshop session had not been registered by the session manager within 30 + seconds of creation and a workshop allocation was pending, the workshop + allocation would not progress properly to the allocated state and any request + objects associated with the workshop session would not be created. From the + perspective of a workshop user the session would still appear to work as the + workshop dashboard would still be accessible, but request objects would be + missing. Timeout for workshop session registration has been increased to 90 + seconds. diff --git a/session-manager/handlers/workshopallocation.py b/session-manager/handlers/workshopallocation.py index 49575744b..11608496a 100644 --- a/session-manager/handlers/workshopallocation.py +++ b/session-manager/handlers/workshopallocation.py @@ -91,7 +91,7 @@ def workshop_allocation_create( parameters_name = f"{session_name}-request" if not (None, environment_name) in workshop_environment_index: - if runtime.total_seconds() >= 30: + if runtime.total_seconds() >= 45: patch["status"] = { OPERATOR_STATUS_KEY: { "phase": "Failed", @@ -122,7 +122,7 @@ def workshop_allocation_create( environment_instance, *_ = workshop_environment_index[(None, environment_name)] if not (None, session_name) in workshop_session_index: - if runtime.total_seconds() >= 30: + if runtime.total_seconds() >= 90: patch["status"] = { OPERATOR_STATUS_KEY: { "phase": "Failed", From 0276aec74164cb79639cc07cfd3768bcac70d2aa Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 23 May 2024 10:53:57 +1000 Subject: [PATCH 20/22] Adjust padding around login form. --- training-portal/src/project/static/styles/project.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/training-portal/src/project/static/styles/project.css b/training-portal/src/project/static/styles/project.css index 778c443e3..3d641abed 100644 --- a/training-portal/src/project/static/styles/project.css +++ b/training-portal/src/project/static/styles/project.css @@ -16,3 +16,8 @@ padding-top: 0px; padding-bottom: 15px; } + +.login { + padding-top: 15px; + padding-bottom: 15px; +} From 47f1c729853145fd0d94ff48cc6f566804cfeb9d Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 23 May 2024 16:38:21 +1000 Subject: [PATCH 21/22] Add requirements for Docker Desktop. --- project-docs/getting-started/quick-start-guide.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/project-docs/getting-started/quick-start-guide.md b/project-docs/getting-started/quick-start-guide.md index 2fb20c921..38d5fc8f3 100644 --- a/project-docs/getting-started/quick-start-guide.md +++ b/project-docs/getting-started/quick-start-guide.md @@ -15,7 +15,7 @@ To deploy Educates on your local machine using the Educates command line tool th * You need to be running macOS or Linux. If using Windows you will need WSL (Windows subsystem for Linux). The Educates command line tool has primarily been tested on macOS. -* You need to have a working `docker` environment. The Educates command line tool has primarily been tested with Docker Desktop. +* You need to have a working `docker` environment. The Educates command line tool has primarily been tested with Docker Desktop on macOS. * You need to have sufficient memory and disk resources allocated to the `docker` environment to run Kubernetes, Educates etc. @@ -27,6 +27,14 @@ To deploy Educates on your local machine using the Educates command line tool th * You need to have port 5001 available on the local machine as this will be used for a local image registry. +If you are using Docker Desktop, you need to have the following enabled: + +* Use kernel networking for UDP (Settings->Resources->Network). + +* Allow the default Docker socket to be used (Settings->Advanced). + +* Allow privileged port mapping (Settings->Advanced). + Downloading the CLI ----------------------- From e397f70a8f05770d406e70e39a20c72a467ee0b9 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Thu, 23 May 2024 17:26:55 +1000 Subject: [PATCH 22/22] Update VS Code version to 1.89.1. --- project-docs/release-notes/version-2.7.1.md | 5 +++++ workshop-images/base-environment/Dockerfile | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/project-docs/release-notes/version-2.7.1.md b/project-docs/release-notes/version-2.7.1.md index 83d7a621c..3034b6db4 100644 --- a/project-docs/release-notes/version-2.7.1.md +++ b/project-docs/release-notes/version-2.7.1.md @@ -1,6 +1,11 @@ Version 2.7.1 ============= +Features Changed +---------------- + +* Updated VS Code to version 1.89.1. + Bugs Fixed ---------- diff --git a/workshop-images/base-environment/Dockerfile b/workshop-images/base-environment/Dockerfile index aead80d03..dec21fe45 100644 --- a/workshop-images/base-environment/Dockerfile +++ b/workshop-images/base-environment/Dockerfile @@ -361,9 +361,9 @@ EOF RUN <