diff --git a/ADVENTURES.md b/ADVENTURES.md
index ac3d9c14c..c22d21343 100644
--- a/ADVENTURES.md
+++ b/ADVENTURES.md
@@ -1,8 +1,17 @@
# Adventures
-This file is for anyone creating, syncing, or updating an adventure on offon.dev.
+This file covers both sides of the adventure process: **challenge authors** working in the challenges repo and **website reviewers** completing the PR checklist after a sync.
-Adventures live in a separate repo ([open-source-challenges](https://github.com/off-on-dev/open-source-challenges)) and are pulled into this site via the **Sync Adventure** GitHub Actions workflow. You never write the generated TypeScript files by hand — the workflow and build scripts do that automatically.
+Adventures live in a separate repo ([open-source-challenges](https://github.com/off-on-dev/open-source-challenges)) and are pulled into this site via the **Sync Adventure** GitHub Actions workflow. You never write the generated TypeScript files by hand; the workflow and build scripts do that automatically.
+
+**Jump to:**
+
+- [YAML Templates](#yaml-templates): full field reference for challenge authors
+- [Syncing a New Adventure](#syncing-a-new-adventure): trigger the workflow
+- [Completing the PR Checklist](#completing-the-pr-checklist): what to do after the sync
+- [Architecture Diagrams](#architecture-diagrams): SVG, ASCII art, and prose fields
+- [Re-syncing an Open PR](#re-syncing-an-open-pr): updating an in-progress PR
+- [Adding a Solution Walkthrough](#adding-a-solution-walkthrough): post-challenge write-ups
---
@@ -15,13 +24,213 @@ off-on-dev/open-source-challenges offon.dev website repo
beginner.yaml src/data/adventures//-posts.json
intermediate.yaml ──── npm run generate (prebuild) ──► src/data/adventures/.generated.ts
... src/data/adventures/index.ts
- src/data/adventures/summaries.ts
+ diagrams/ src/data/adventures/summaries.ts
+ -.svg ─────────────────────────────────► src/assets/diagrams/-.svg
```
+The sync workflow also regenerates `public/sitemap.xml`, `react-router.config.ts`, `e2e/smoke.spec.ts`, `src/test/seo.test.ts`, `src/test/prerender.test.ts`, and `scripts/refresh-leaderboard.mjs`. All of these appear in the PR diff; they are managed automatically and should not be edited by hand.
+
The generated TypeScript files are committed so the dev server works without running the generator manually. Never edit `*.generated.ts`, `index.ts`, or `summaries.ts` by hand.
---
+## YAML Templates
+
+Full field reference for challenge authors. All fields are shown with example values. Remove any that do not apply to your adventure.
+
+### `docs/index.yaml` (adventure-level metadata)
+
+```yaml
+# Title of the adventure. Use `title` (preferred) or `name`.
+title: "My Adventure Title"
+emoji: 🚀
+
+# Optional: Lucide icon name to use instead of the emoji icon.
+# Accepts any valid Lucide icon name (e.g. "Shield", "Cpu", "GitBranch").
+# icon: Shield
+
+# Tags drive the tag-filter UI and default `topics` for each level.
+# Use the canonical tool/platform names shown on the OffOn website.
+tags:
+ - Kubernetes
+ - Argo CD
+ - Helm
+
+# Optional: overrides the auto-generated SEO meta description for the adventure page.
+# Keep under 160 characters.
+# meta_description: "Fix broken Kyverno policies to restore proper admission control."
+
+# One or more story paragraphs. Markdown is supported.
+backstory:
+ - "Opening paragraph that sets the scene."
+ - "Second paragraph continuing the story."
+
+# Optional: an overview of the challenge shown before the story.
+# Useful when backstory is long and reviewers need a quick summary.
+overview:
+ - "Brief, direct summary of what the participant will fix or build."
+
+rewards:
+ # ISO 8601 or human-readable: "Tuesday, 1 July 2026 at 23:59 CET"
+ # Supported TZ abbreviations: CET (+01:00), CEST (+02:00), UTC, GMT
+ deadline: "2026-09-01T23:59:00+01:00"
+ tiers:
+ - label: 1st place
+ description: 50% voucher for a Linux Foundation certification
+ - label: Top 3
+ description: Credly badge to showcase the achievement
+ # Optional: overrides the default eligibility text on the rewards card.
+ # eligibility: "Open to all registered participants who submit before the deadline."
+ # Optional: overrides the default ranking note on the rewards card.
+ # ranking_note: "Ranked by verification timestamp; ties broken by submission order."
+```
+
+---
+
+### `docs/.yaml` (level content)
+
+One file per level: `beginner.yaml`, `intermediate.yaml`, `expert.yaml`.
+
+```yaml
+# Required. Must match the filename: beginner | intermediate | expert
+level: beginner
+emoji: 🟢 # 🟢 beginner 🟡 intermediate 🔴 expert
+title: "Level Title"
+
+# Devcontainer folder name in off-on-dev/open-source-challenges/.devcontainer/
+# The generator auto-corrects this if it finds an unambiguous match.
+devcontainer: my-adventure_beginner
+
+# Optional: upgrade the default Codespace machine size.
+# Only set this when the level genuinely needs more RAM or CPU.
+codespaces_machine: 4core
+
+# Optional: estimated completion time shown as a pill on the level card.
+estimated_time: "2-3 hours"
+
+# One sentence shown on the adventure card and the level sidebar.
+summary: "Fix the broken X so that Y works end to end."
+
+# Who this level is for. Markdown, inline code, and are supported.
+# Use ABBR for acronyms on first use.
+audience: >-
+ Platform engineers, SREs, and developers
+ curious about X. No prior experience needed, but familiarity with basic
+ `kubectl` and YAML will help.
+
+# Optional: a short hook shown at the top of the level page, before the story.
+# hook: "The cluster is on fire and the policies that should protect it are broken."
+
+# Optional: the in-world scenario framing the level's context.
+# scenario: "You have been granted emergency access to the broken cluster."
+
+# Level-specific story paragraphs. Markdown is supported.
+backstory:
+ - "What went wrong and why it matters."
+ - "What the participant's role is in fixing it."
+
+# Bullet-point acceptance criteria. Markdown is supported.
+# Keep each item concrete and testable. Use **bold** to call out key terms.
+objective:
+ - "All workloads **missing the `required-label`** are blocked at admission."
+ - "All verification checks pass."
+
+# What skills and concepts the participant will practise.
+# Use [linked text](url) for official docs. Use `backticks` for tool names.
+what_you_learn:
+ - "How [X](https://example.com/docs) works and why it matters."
+ - "How to use `kubectl` logs to trace a silent failure across tools."
+
+# Architecture explanation. Shown under the Architecture heading on the level page.
+# Use an array; each item becomes a separate prose block.
+architecture:
+ - "High-level description of the system the participant is working in."
+ - "Which files or resources they need to touch, and which to leave alone."
+
+# Optional: SVG architecture diagram.
+# Place the SVG at docs/diagrams/-.svg in the challenges repo.
+# The sync auto-fetches it. Name must match the filename exactly.
+architecture_diagram: "my-adventure-beginner.svg"
+diagram_alt: "Left-to-right diagram showing how X connects to Y and Z."
+
+# Optional: ASCII art fallback when no SVG is available.
+# Use a YAML block scalar (|) to preserve whitespace and line breaks.
+# architecture_ascii: |
+# ┌──────────┐ ┌──────────┐
+# │ Client │──────►│ API │
+# └──────────┘ └──────────┘
+
+# Tools the participant will use. Shown as a toolbox on the level page.
+toolbox:
+ - name: Tool Name
+ url: https://example.com/docs
+ description: "What it does in this challenge and how to open it."
+
+# Optional: running services exposed on local ports (Codespace / devcontainer).
+# Omit if there are no local services.
+services:
+ - name: My Service
+ port: 8080
+ credentials: admin / password # omit if no login required
+ description: "What this service is and what to look for in it."
+
+# Step-by-step guide shown in the How to Play tab.
+# Markdown, inline code, , and fenced code blocks are supported in both
+# `title` and `content`. The `id` field is informational only; it is not used
+# by the generator or the website.
+how_to_play:
+ - id: start
+ title: "Start the Environment"
+ content: |
+ Start the platform with `make start`. The first run may take ~30-60 seconds
+ to pull images. Once it's up, leave it running in that terminal.
+ - id: explore
+ title: "Explore the Setup"
+ content: |
+ Open the CLI and inspect the
+ running resources:
+
+ ```bash
+ kubectl get pods -A
+ ```
+
+ Look at what is deployed and note anything that looks broken or missing.
+ - id: fix
+ title: "Fix It"
+ content: |
+ The bug lives in `path/to/file.yaml`. Edit it directly and re-apply:
+
+ ```bash
+ kubectl apply -f path/to/file.yaml
+ ```
+
+ When you think it's fixed, run the verification script:
+
+ ```bash
+ make verify
+ ```
+
+# Optional: further reading shown at the bottom of the level page.
+helpful_links:
+ - title: "Official Docs: Feature Name"
+ url: https://example.com/docs/feature
+ description: "One sentence on why this link is useful for this challenge."
+
+# Optional: refine the default topics (which default to all adventure tags).
+# Only set this when the level uses a subset of the adventure's tools.
+# topics:
+# - Kubernetes
+# - Argo CD
+
+# Optional: override the verification step shown at the end of How to Play.
+# Omit to use the standard verify.sh description.
+# verification:
+# command: make verify
+# description: "What the script checks and what a passing result looks like."
+```
+
+---
+
## Syncing a New Adventure
### 1. Trigger the workflow
@@ -30,25 +239,25 @@ Go to **Actions → Sync Adventure from Challenges Repo → Run workflow**.
| Input | Required | Description |
| --- | --- | --- |
-| `adventure_url` | Yes | GitHub URL of the adventure folder — any branch works. Main: `https://github.com/off-on-dev/open-source-challenges/tree/main/adventures/05-lex-imperfecta`. PR branch: `https://github.com/off-on-dev/open-source-challenges/tree/feat/my-branch/adventures/05-lex-imperfecta`. |
+| `adventure_url` | Yes | GitHub URL of the adventure folder. Any branch works. Main: `https://github.com/off-on-dev/open-source-challenges/tree/main/adventures/05-lex-imperfecta`. PR branch: `https://github.com/off-on-dev/open-source-challenges/tree/feat/my-branch/adventures/05-lex-imperfecta`. |
| `levels` | No | Comma-separated level IDs to make live now (e.g. `beginner` or `beginner,intermediate`). Levels that exist in the challenges repo but are not listed here appear as "Coming Soon" placeholders. Leave blank to make all levels live. |
### 2. What the workflow does
1. Validates the URL points to `off-on-dev/open-source-challenges`.
2. If a PR branch (`feat/adventure-`) already exists, restores `adventure.yaml` from that branch so any manual edits already made survive the re-sync.
-3. Fetches `docs/index.yaml` and all level YAMLs from the challenges repo.
+3. Fetches `docs/index.yaml` and all level YAMLs from the challenges repo. For any level with `architecture_diagram` set, auto-fetches the SVG from `docs/diagrams/` and writes it to `src/assets/diagrams/`.
4. Writes `src/data/adventures//adventure.yaml` and creates `-posts.json` stubs for each new live level.
-5. Runs `generate-adventures.mjs` to regenerate all TypeScript, sitemap entries, prerender entries, test arrays, `public/llms.txt`, and the leaderboard adventure list in `scripts/refresh-leaderboard.mjs`.
+5. Runs `generate-adventures.mjs` to regenerate TypeScript, sitemap entries, prerender entries, and test arrays.
6. Opens (or updates) a PR on `feat/adventure-` with a checklist of steps to complete before merging.
---
## Completing the PR Checklist
-The PR body lists everything that needs to happen before merging. Here is each item explained.
+The PR body lists everything that needs to happen before merging. Complete the items in order; some steps depend on earlier ones.
-### Add contributor block
+### 1. Add contributor block
```yaml
contributor:
@@ -57,40 +266,45 @@ contributor:
about: "One sentence bio."
```
-Add this to `src/data/adventures//adventure.yaml`. The `url` and `about` fields are optional but recommended. Once set, this block survives future re-syncs automatically.
+Add this to `src/data/adventures//adventure.yaml`. The `url` and `about` fields are optional but recommended. This block survives all future re-syncs once set.
-### Confirm month
+### 2. Confirm month
-The `month:` field defaults to the current month when first synced. Correct it if the adventure is planned for a future release. Format: `MMM YYYY` (e.g. `JAN 2026`). This field also survives re-syncs once set.
+The `month:` field defaults to the current month when first synced. Correct it if the adventure is planned for a future release. Format: `MMM YYYY` (e.g. `JAN 2026`). This field survives all future re-syncs once set.
-### Set community_category_id
+### 3. Set community_category_id
1. Look up the Discourse category at `https://community.offon.dev/categories.json`.
2. Find the category for this adventure and copy its `id` integer.
3. Add `community_category_id: ` to `adventure.yaml`.
4. Run `npm run generate` to regenerate TypeScript.
-This field also survives future re-syncs once set.
+This field survives all future re-syncs once set.
-### Update rewards deadline
+### 4. Update rewards deadline
-Change `rewards.deadline:` from `TODO` to either an ISO 8601 datetime or the human-readable format used in the challenges repo:
+Change `rewards.deadline` from `TODO` to an ISO 8601 datetime or the human-readable format accepted by the generator:
```yaml
-# ISO 8601 (preferred for direct edits)
-rewards.deadline: "2026-07-01T23:59:00+01:00"
+rewards:
+ # ISO 8601 (preferred)
+ deadline: "2026-07-01T23:59:00+01:00"
-# Human-readable (accepted; the generator converts it automatically)
-rewards.deadline: "Tuesday, 1 July 2026 at 23:59 CET"
+ # Human-readable (the generator converts it automatically)
+ # deadline: "Tuesday, 1 July 2026 at 23:59 CET"
```
Supported timezone abbreviations: `CET` (+01:00), `CEST` (+02:00), `UTC` (+00:00), `GMT` (+00:00). Unrecognised abbreviations are left as-is and logged as warnings during generation.
-### Review topics
+### 5. Review topics
-Each level's `topics:` list defaults to all adventure tags. Refine it to the subset of technologies that are actually used in that level. This list is preserved on re-sync only if the challenges repo did not set it explicitly (see Re-syncing below).
+Each level's `topics:` list defaults to all adventure tags. Refine it to the subset of technologies actually used in that specific level. The challenges repo value wins on re-sync when set explicitly there; a manually refined value in `adventure.yaml` is only preserved when the challenges repo leaves `topics:` unset. See [What is preserved on re-sync](#what-is-preserved-on-re-sync) for the full rules.
-### Update discussion_url
+### 6. Check architecture diagrams
+
+If the challenge author added an SVG to `docs/diagrams/` in the challenges repo, the sync fetches it automatically and no action is needed. If the sync log shows a warning that a diagram was not found, see [Architecture Diagrams](#architecture-diagrams) for the fallback steps.
+
+### 7. Update discussion_url
Once you have created the Discourse thread for a level, use the **Add Discussion URL to Level** workflow (Actions tab → Add Discussion URL to Level → Run workflow).
@@ -102,26 +316,17 @@ Once you have created the Discourse thread for a level, use the **Add Discussion
The workflow updates `discussion_url` in `adventure.yaml`, fetches the initial posts from Discourse, regenerates TypeScript, and opens a PR. Run it once per level. If the thread is brand-new and has no posts yet, the PR will contain an empty `discussionPosts` array; the hourly `refresh-community-data` workflow will populate it once posts appear.
-`discussion_url` in `adventure.yaml` is a website-only field. It is never in the challenges repo and survives every re-sync automatically.
-
-### Add architecture diagrams (if needed)
+`discussion_url` is a website-only field. It is never in the challenges repo and survives every re-sync automatically.
-If a level has an SVG architecture diagram, the sync strips the `architecture_diagram:` field because the SVG file must be added to `src/assets/diagrams/` manually.
-
-1. Add the SVG file to `src/assets/diagrams/.svg`.
-2. Add `architecture_diagram: .svg` back to the level in `adventure.yaml`.
-
-Once set, `architecture_diagram` survives future re-syncs automatically.
-
-### Run the leaderboard script
+### 8. Run the leaderboard script
```sh
node scripts/refresh-leaderboard.mjs
```
-Run this after `community_category_id` is set. It adds the adventure to the leaderboard data used on the site. Requires `DISCOURSE_API_KEY` and `DISCOURSE_API_USERNAME` in your environment or a `.env` file.
+Run this after `community_category_id` is set. It adds the adventure to the leaderboard data used on the site. See [Refresh Scripts](#refresh-scripts) for credential setup.
-### Verify devcontainer paths
+### 9. Verify devcontainer paths
`generate-adventures.mjs` cross-checks each level's `devcontainer:` value against the actual folder names in [`off-on-dev/open-source-challenges/.devcontainer`](https://github.com/off-on-dev/open-source-challenges/tree/main/.devcontainer) via `gh api`.
@@ -137,11 +342,19 @@ If you see this warning, also fix the `devcontainer:` value upstream in the chal
If `gh` is unavailable or unauthenticated, the check is skipped with a warning and generation proceeds.
-### Verify llms.txt
+### 10. Update llms.txt
+
+`generate-adventures.mjs` patches `public/llms.txt` automatically, but the sync workflow does not commit that file. Run the generator locally and commit the result:
+
+```sh
+npm run generate
+git add public/llms.txt
+git commit -s -m "chore: update llms.txt for "
+```
-`generate-adventures.mjs` automatically patches the adventure entry in `public/llms.txt`. After running `npm run generate`, confirm the adventure appears correctly in the file under the Adventures section with the right title and URL.
+Confirm the adventure appears under the Adventures section in `public/llms.txt` with the correct title and URL before pushing.
-### Run the a11y audit
+### 11. Run the a11y audit
After the build passes, run the accessibility audit against any new or changed pages:
@@ -151,7 +364,7 @@ After the build passes, run the accessibility audit against any new or changed p
Target any new adventure or level detail pages. All severity-weighted findings must be resolved before merging.
-### Final checks
+### 12. Final checks
```sh
npm run lint && npm run lint:reuse && npm test && npm run build && npm run test:e2e
@@ -161,9 +374,64 @@ All checks must pass before merging.
---
+## Architecture Diagrams
+
+Each level can display an SVG diagram, an ASCII art fallback, and one or more prose paragraphs. All are rendered under the **Architecture** heading on the challenge page.
+
+| Field | Type | Renders as |
+| --- | --- | --- |
+| `architecture_diagram` | SVG filename | `` (takes priority over `architecture_ascii`) |
+| `diagram_alt` | string | Accessible alt text for the SVG. Required when `architecture_diagram` is set. |
+| `architecture_ascii` | YAML block scalar (`\|`) | `
` block, shown when no SVG is present |
+| `architecture` | array of Markdown strings | Prose paragraphs always rendered below the diagram or ASCII block |
+
+### SVG in the challenges repo (normal path)
+
+Add the SVG to the challenges repo at:
+
+```text
+adventures//docs/diagrams/-.svg
+```
+
+Name it after the adventure slug and level: `dead-reckoning-intermediate.svg`, `lex-imperfecta-beginner.svg`. Then add the fields to the level YAML in the challenges repo:
+
+```yaml
+architecture_diagram: "dead-reckoning-intermediate.svg"
+diagram_alt: "One sentence describing what the diagram shows."
+architecture:
+ - "Prose paragraph explaining the architecture."
+ - "Second paragraph if needed."
+```
+
+The sync auto-fetches the SVG from `docs/diagrams/` and writes it to `src/assets/diagrams/`. No action is needed on the website side.
+
+### SVG already in the website repo (fallback)
+
+If the SVG exists in `src/assets/diagrams/` on the website repo but not in the challenges repo (added manually before the auto-fetch path existed), the sync recognises it and re-adds `architecture_diagram` to the level automatically. Check that `architecture_diagram` and `diagram_alt` are set for that level in `adventure.yaml`:
+
+```yaml
+architecture_diagram: "-.svg"
+diagram_alt: "One sentence describing what the diagram shows."
+```
+
+### ASCII art fallback
+
+When no SVG is available, use `architecture_ascii` with a YAML block scalar to preserve whitespace:
+
+```yaml
+architecture_ascii: |
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
+ │ Client │──────►│ API │──────►│ DB │
+ └──────────┘ └──────────┘ └──────────┘
+```
+
+All architecture fields survive every re-sync once set.
+
+---
+
## Re-syncing an Open PR
-If the challenges repo is updated while your PR is still open, or you want to promote a "Coming Soon" level to live, just run the workflow again with the same (or updated) inputs. You do not need to close or recreate the PR.
+If the challenges repo is updated while your PR is still open, or you want to promote a "Coming Soon" level to live, run the workflow again with the same (or updated) inputs. You do not need to close or recreate the PR.
### What happens
@@ -180,10 +448,12 @@ If the challenges repo is updated while your PR is still open, or you want to pr
| --- | --- | --- |
| `contributor:` (adventure) | Always | Survives every re-sync once set |
| `community_category_id:` (adventure) | Always | Survives every re-sync once set; position is kept directly after `slug` |
+| `meta_description:` (adventure) | Always | Survives every re-sync once set |
| `month:` (adventure) | Always | Survives every re-sync once set |
| `discussion_url:` / `community_url:` (level) | Always | Website-only fields; never in the challenges repo. Both field aliases are preserved independently |
-| `architecture_diagram:` (level) | Always | Stripped from incoming; preserved once added manually |
-| `topics:` (level) | Only if challenges repo did not set them | If the challenges repo sets `topics:` explicitly, the upstream value wins |
+| `architecture_diagram:` (level) | Always | Auto-fetched from `docs/diagrams/` when present in the challenges repo; otherwise recognised from `src/assets/diagrams/` if the file exists locally |
+| `diagram_alt:` (level) | When upstream omits it | If the challenges repo sets `diagram_alt:` explicitly, the upstream value wins |
+| `topics:` (level) | When upstream omits them | If the challenges repo sets `topics:` explicitly, the upstream value wins |
| `upcoming_levels:` entries for levels not yet upstream | Always | Placeholders for levels not yet authored in the challenges repo survive re-syncs so "Coming Soon" cards are not dropped |
| All other level content | Never | Steps, objectives, toolbox, services, how_to_play, verification, etc. are always refreshed from the challenges repo |
@@ -214,7 +484,7 @@ The fastest way to add a solution is with the Claude Code skill:
/add-solution
```
-Paste or attach the walkthrough content in any format — markdown, YAML, HTML, or plain text. The skill infers the adventure ID, level ID, and contributor name from the content where possible, confirms them with you, and then:
+Paste or attach the walkthrough content in any format: markdown, YAML, HTML, or plain text. The skill infers the adventure ID, level ID, and contributor name from the content where possible, confirms them with you, and then:
1. Parses the input into structured steps (`SolutionBlock[]` arrays with text, code, image, and callout blocks).
2. Downloads any referenced images and converts them to WebP at quality 85 using `cwebp`. Images are saved to `public/solutions//`.
diff --git a/CLAUDE.md b/CLAUDE.md
index a43136381..0d57dfd05 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -242,7 +242,7 @@ When diagnosing a bug, especially in the production build, follow these rules wi
- **Buttons:** use raw `
-
HookContext.getCtx() returns the merged evaluation context (global + transaction + invocation). Use a fixed allowlist of List.of("species", "country", "dose"). Never iterate the whole context: targetingKey joins to PII in real apps, and span attributes are retained for days in Tempo at scale.
+
HookContext.getCtx() returns the merged evaluation context (global + transaction + invocation). Use a fixed allowlist of List.of("species", "country", "dose"). Never iterate the whole context: targetingKey joins to PIIPersonally Identifiable Information in real apps, and span attributes are retained for days in Tempo at scale.
Register ContextSpanHook alongside TracesHook and MetricsHook in OpenFeatureConfig. The verifier searches Tempo for feature_flag.context.dose=underdose once you are done.
` },
{ title: "Turn On the Loadgen", content: `
flags.json has two flags: loadgen_active (off by default) and the misbehaving vision_amplifier_v2. flagd watches the file and picks up changes within about a second.
Flip loadgen_active to on. The k6 loadgen polls it every two seconds and starts five virtual users hammering the lab. Within a minute, latency p99 should climb ~200ms and the 5xx rate ~10% on the dashboard, confirming that the bad arm of vision_amplifier_v2 is active.
` },
diff --git a/src/data/adventures/blind-by-design/adventure.yaml b/src/data/adventures/blind-by-design/adventure.yaml
index f36bbfb73..924d5c196 100644
--- a/src/data/adventures/blind-by-design/adventure.yaml
+++ b/src/data/adventures/blind-by-design/adventure.yaml
@@ -20,7 +20,7 @@ contributor:
name: "Simon Schrottner"
url: "https://schrottner.at/"
about: >-
- CNCF Ambassador and maintainer of OpenFeature and JUnit Pioneer. Helps teams release faster
+ CNCF Ambassador and maintainer of OpenFeature and JUnit Pioneer. Helps teams release faster
and with more confidence through open standards, feature flagging, and the communities that
make both possible. A familiar face at KubeCon EU, Devoxx, ContainerDays, and meetups across Europe.
@@ -31,7 +31,7 @@ backstory:
through the protocol (blurry, sharp, enhanced, or clouded), because subjects don't all arrive
with the same biology, the same dose adherence, or the same trial-jurisdiction baseline. The
flag definitions that drive those readings live in flags.json, watched by a flagd sidecar;
- the OpenFeature SDK is supposed to call that sidecar on every evaluation.
+ the OpenFeature SDK is supposed to call that sidecar on every evaluation.
- >-
It hasn't been. For the past eight months, every subject through the door has been recorded
as "untreated": the integration was never finished, and the lab director assumed the system
@@ -78,8 +78,8 @@ levels:
Platform engineers, SREs, and developers curious about feature flags, with no prior OpenFeature
experience needed, but familiarity with Spring Boot and basic Java will help.
learnings:
- - "How an OpenFeature client and provider work together: the SDK is provider-agnostic and the flagd provider plugs in via dependency only"
- - "What remote provider means in practice: the SDK calls a separate flag service (flagd) over gRPC, not parsing flags.json itself"
+ - "How an OpenFeature client and provider work together: the SDK is provider-agnostic and the flagd provider plugs in via dependency only"
+ - "What remote provider means in practice: the SDK calls a separate flag service (flagd) over gRPC, not parsing flags.json itself"
- "What flags.json looks like for flagd (state, variants, defaultVariant)"
- "Why hot-reload of the flag file matters operationally: configuration without redeploy"
devcontainer: 04-blind-by-design_01-beginner
@@ -199,7 +199,7 @@ levels:
baselines.
- >-
Your shift: teach the lab to read each subject's species off the request, attach the trial's country of
- registration (set on the JVM via the COUNTRY environment variable) to the global context, pass the dose as
+ registration (set on the JVM via the COUNTRY environment variable) to the global context, pass the dose as
invocation context at the moment of the flag evaluation, and register an audit hook that records every dose
with its variant and reason.
architecture_diagram: "blind-by-design-intermediate.svg"
@@ -209,7 +209,7 @@ levels:
- "With COUNTRY=de, curl /?dose=standard returns 'sharp'; with COUNTRY=at, the same call falls through to the default"
- "curl /?dose=underdose returns 'clouded'; curl /?species=zyklop&dose=underdose returns 'enhanced' (species takes precedence)"
- "Every evaluation produces an [AUDIT] log line naming the flag, the resolved variant, the reason, and the attributes that drove the outcome"
- - "The response is never 'untreated' (that fallback only fires when the SDK cannot reach flagd)"
+ - "The response is never 'untreated' (that fallback only fires when the SDK cannot reach flagd)"
toolbox:
- name: "Java 21 (Temurin)"
description: "pre-installed in the devcontainer"
@@ -350,7 +350,7 @@ levels:
- Spring Boot
learnings:
- "How the OpenFeature OpenTelemetry hooks (TracesHook and MetricsHook) join flag evaluations to the rest of an application's telemetry without a separate ingestion path"
- - "How to author your own Hook: a tiny class that copies merged-eval-context attributes onto the active OTel span, closing the loop between why a flag resolved the way it did and what the operator sees in Tempo"
+ - "How to author your own Hook: a tiny class that copies merged-eval-context attributes onto the active OTel span, closing the loop between why a flag resolved the way it did and what the operator sees in Tempo"
- "How fractional rollout in flagd buckets users by targetingKey (same key, same bucket, every request) and how to read that bucketing off a dashboard"
- "How a flag flip is a faster operational lever than a redeploy when a rollout is misbehaving: the difference between a one-line config change and a twenty-minute deployment"
devcontainer: 04-blind-by-design_03-expert
@@ -396,7 +396,7 @@ levels:
description: "sends requests to http://localhost:8080/ to test the lab, and to Prometheus on http://localhost:9090/ to query metrics directly"
url: "https://curl.se/"
- name: "Grafana"
- description: "browser UI at http://localhost:3000 (admin/admin) for the Feature Flag Metrics dashboard and Tempo trace explorer"
+ description: "browser UI at http://localhost:3000 (admin/admin) for the Feature Flag Metrics dashboard and Tempo trace explorer"
- name: "jq"
description: "pretty-prints the JSON evaluation details"
url: "https://jqlang.org/"
@@ -420,7 +420,7 @@ levels:
how_to_play:
- title: "Start Your Challenge"
content: |
- The sibling containers (flagd, Grafana LGTM, k6 loadgen) start automatically as part of the devcontainer compose. Wait ~2-3 minutes for them to be ready before moving on.
+ The sibling containers (flagd, Grafana LGTM, k6 loadgen) start automatically as part of the devcontainer compose. Wait ~2-3 minutes for them to be ready before moving on.
- title: "Start the Lab"
content: |
The sibling containers are already up. Boot the Spring Boot lab by clicking **Run** on `Laboratory` in the Spring Boot Dashboard panel (or press **F5** with `Laboratory.java` open), or from the terminal:
@@ -432,7 +432,7 @@ levels:
Spans start flowing into Tempo on the first request. The trace pipeline is already wired. The metrics pipeline is dead (task 4a), so the Grafana dashboard panels stay empty until you fix it.
- title: "Turn On the Metrics Exporter"
content: |
- OTel ships two parallel pipelines: traces (already flowing into Tempo) and metrics (dead). The OTel Java Agent attached to the lab JVM has both pipelines plumbed and pointed at the LGTM stack, but `otel.properties` (next to `pom.xml`) sets `otel.metrics.exporter=none`, so anything the meter records goes nowhere.
+ OTel ships two parallel pipelines: traces (already flowing into Tempo) and metrics (dead). The OTel Java Agent attached to the lab JVM has both pipelines plumbed and pointed at the LGTM stack, but `otel.properties` (next to `pom.xml`) sets `otel.metrics.exporter=none`, so anything the meter records goes nowhere.
Open `otel.properties` and flip the exporter on. While you're there, look at the export interval. The default makes the next steps harder than they need to be.
@@ -454,7 +454,7 @@ levels:
}
```
- `HookContext.getCtx()` returns the merged evaluation context (global + transaction + invocation). Use a fixed allowlist of `List.of("species", "country", "dose")`. Never iterate the whole context: `targetingKey` joins to PII in real apps, and span attributes are retained for days in Tempo at scale.
+ `HookContext.getCtx()` returns the merged evaluation context (global + transaction + invocation). Use a fixed allowlist of `List.of("species", "country", "dose")`. Never iterate the whole context: `targetingKey` joins to PII in real apps, and span attributes are retained for days in Tempo at scale.
Register `ContextSpanHook` alongside `TracesHook` and `MetricsHook` in `OpenFeatureConfig`. The verifier searches Tempo for `feature_flag.context.dose=underdose` once you are done.
- title: "Turn On the Loadgen"
diff --git a/src/data/adventures/building-cloudhaven.generated.ts b/src/data/adventures/building-cloudhaven.generated.ts
index 0e545b8bf..0c93a150f 100644
--- a/src/data/adventures/building-cloudhaven.generated.ts
+++ b/src/data/adventures/building-cloudhaven.generated.ts
@@ -12,7 +12,7 @@ export const BUILDING_CLOUDHAVEN: Adventure = {
contributor: {
name: "Katharina Sick",
url: "https://ksick.dev/",
- aboutHtml: "DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
+ aboutHtml: "DevRelDeveloper Relations at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
},
backstory: [
"Welcome to CloudHaven, a bustling digital metropolis where every district depends on essential services to thrive. You've just joined the Infrastructure Guild, a team of platform engineers responsible for providing the tools and services that keep the city running.",
@@ -29,7 +29,7 @@ export const BUILDING_CLOUDHAVEN: Adventure = {
topics: ["OpenTofu"],
learnings: [
"Infrastructure as Code with OpenTofu",
- "Remote state management with GCS backend",
+ "Remote state management with GCSGoogle Cloud Storage backend",
"Dynamic resource provisioning with for_each",
"Conditional resources with the enabled meta-argument, new in OpenTofu",
],
@@ -50,7 +50,7 @@ export const BUILDING_CLOUDHAVEN: Adventure = {
"Resolve all TODOs in the code and successfully run tofu apply",
],
toolbox: [
- { name: "tofu", description: "OpenTofu CLI for infrastructure provisioning", url: "https://opentofu.org/" },
+ { name: "tofu", description: "OpenTofu CLICommand Line Interface for infrastructure provisioning", url: "https://opentofu.org/" },
{ name: "gcp-api-mock", description: "mock GCP API running locally to simulate cloud resources without real cloud costs (Cloud Storage and Cloud SQL only)", url: "https://github.com/KatharinaSick/gcp-api-mock" },
],
howToPlay: [
@@ -111,7 +111,7 @@ export const BUILDING_CLOUDHAVEN: Adventure = {
"Three districts deployed with correctly configured infrastructure (vaults and ledgers)",
],
toolbox: [
- { name: "tofu", description: "OpenTofu CLI for infrastructure provisioning", url: "https://opentofu.org/" },
+ { name: "tofu", description: "OpenTofu CLICommand Line Interface for infrastructure provisioning", url: "https://opentofu.org/" },
{ name: "gcp-api-mock", description: "mock GCP API running locally to simulate cloud resources without real cloud costs (Cloud Storage and Cloud SQL only)", url: "https://github.com/KatharinaSick/gcp-api-mock" },
],
howToPlay: [
@@ -174,7 +174,7 @@ make apply
discussionUrl: `${COMMUNITY_URL}/t/adventure-02-building-cloudhaven-expert-the-guardian-protocols/782/8`,
deadline: "2026-02-04T23:59:00+01:00",
intro: [
- "Three broken GitHub Actions workflows stand between CloudHaven and automated infrastructure governance. Fix drift detection that creates PRs, PR validation with Trivy security scanning and service-container integration tests, and automatic apply on merge.",
+ "Three broken GitHub Actions workflows stand between CloudHaven and automated infrastructure governance. Fix drift detection that creates PRs, PRPull Request validation with Trivy security scanning and service-container integration tests, and automatic apply on merge.",
],
backstory: [
"After the Modular Metropolis refactoring, CloudHaven flourished. But with growth came risk. One night, a rogue change slipped through unnoticed and nearly brought down the North Market's trading vaults. The Council was furious: how could this happen without anyone noticing?",
@@ -189,7 +189,7 @@ make apply
"All three workflows must have succeeded at least once",
],
toolbox: [
- { name: "tofu", description: "OpenTofu CLI for infrastructure provisioning", url: "https://opentofu.org/" },
+ { name: "tofu", description: "OpenTofu CLICommand Line Interface for infrastructure provisioning", url: "https://opentofu.org/" },
{ name: "gcp-api-mock", description: "mock GCP API running locally (port set to public so GitHub Actions runners can access it)", url: "https://github.com/KatharinaSick/gcp-api-mock" },
{ name: "GitHub Actions", description: "the workflows you will fix are in .github/workflows/", url: "https://docs.github.com/en/actions" },
],
diff --git a/src/data/adventures/building-cloudhaven/adventure.yaml b/src/data/adventures/building-cloudhaven/adventure.yaml
index 69f1f07c0..90bded5bf 100644
--- a/src/data/adventures/building-cloudhaven/adventure.yaml
+++ b/src/data/adventures/building-cloudhaven/adventure.yaml
@@ -15,7 +15,7 @@ tags:
contributor:
name: Katharina Sick
url: https://ksick.dev/
- about: DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native
+ about: DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native
and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities,
inline skating rinks, and quiz nights across Europe.
backstory:
@@ -38,7 +38,7 @@ levels:
- OpenTofu
learnings:
- Infrastructure as Code with OpenTofu
- - Remote state management with GCS backend
+ - Remote state management with GCS backend
- Dynamic resource provisioning with for_each
- Conditional resources with the enabled meta-argument, new in OpenTofu
devcontainer: 02-building-cloudhaven_01-beginner
@@ -60,7 +60,7 @@ levels:
- Resolve all TODOs in the code and successfully run tofu apply
toolbox:
- name: tofu
- description: OpenTofu CLI for infrastructure provisioning
+ description: OpenTofu CLI for infrastructure provisioning
url: https://opentofu.org/
- name: gcp-api-mock
description: mock GCP API running locally to simulate cloud resources without real cloud costs (Cloud Storage and Cloud
@@ -141,7 +141,7 @@ levels:
- Three districts deployed with correctly configured infrastructure (vaults and ledgers)
toolbox:
- name: tofu
- description: OpenTofu CLI for infrastructure provisioning
+ description: OpenTofu CLI for infrastructure provisioning
url: https://opentofu.org/
- name: gcp-api-mock
description: mock GCP API running locally to simulate cloud resources without real cloud costs (Cloud Storage and Cloud
@@ -224,7 +224,7 @@ levels:
deadline: "2026-02-04T23:59:00+01:00"
intro:
- Three broken GitHub Actions workflows stand between CloudHaven and automated infrastructure governance. Fix
- drift detection that creates PRs, PR validation with Trivy security scanning and service-container integration
+ drift detection that creates PRs, PR validation with Trivy security scanning and service-container integration
tests, and automatic apply on merge.
backstory:
- "After the Modular Metropolis refactoring, CloudHaven flourished. But with growth came risk. One night, a rogue
@@ -244,7 +244,7 @@ levels:
- All three workflows must have succeeded at least once
toolbox:
- name: tofu
- description: OpenTofu CLI for infrastructure provisioning
+ description: OpenTofu CLI for infrastructure provisioning
url: https://opentofu.org/
- name: gcp-api-mock
description: mock GCP API running locally (port set to public so GitHub Actions runners can access it)
diff --git a/src/data/adventures/dead-reckoning.generated.ts b/src/data/adventures/dead-reckoning.generated.ts
index ef6288de8..a72f967ec 100644
--- a/src/data/adventures/dead-reckoning.generated.ts
+++ b/src/data/adventures/dead-reckoning.generated.ts
@@ -14,7 +14,7 @@ export const DEAD_RECKONING: Adventure = {
contributor: {
name: "Katharina Sick",
url: "https://ksick.dev/",
- aboutHtml: "DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
+ aboutHtml: "DevRelDeveloper Relations at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
},
backstory: [
"The Grand Fleet's commission office is buried in complaints. Manifests are filed but nothing comes of them. Vessels that do sail arrive at port with the wrong cargo, and no one along the route can explain why. As the fleet's engineer, your mission is to restore order from keel to quayside and find out what the records are hiding.",
@@ -109,7 +109,7 @@ vessel through it to confirm the repair.
name: "Sea Trial",
difficulty: "Intermediate",
topics: ["Backstage", "Gitea", "Argo Events", "Argo Workflows", "Argo CD"],
- audience: "Platform and DevOps engineers who have met these tools before and want to see how they fit together. You should be comfortable with Kubernetes, YAML, and reading a tool's logs and UI. Prior exposure to Backstage, Gitea, and the Argo projects helps, but the focus here is the integration between them, not any one tool.",
+ audience: "Platform and DevOpsDevelopment and Operations engineers who have met these tools before and want to see how they fit together. You should be comfortable with Kubernetes, YAML, and reading a tool's logs and UIUser Interface. Prior exposure to Backstage, Gitea, and the Argo projects helps, but the focus here is the integration between them, not any one tool.",
learnings: [
"How a Git webhook drives a workflow engine: Argo Events Sensors turn a push into a parameterized workflow run",
"How Argo Workflows runs a multi-step delivery pipeline, and the RBAC its steps need",
diff --git a/src/data/adventures/dead-reckoning/adventure.yaml b/src/data/adventures/dead-reckoning/adventure.yaml
index a1164671a..891901b3c 100644
--- a/src/data/adventures/dead-reckoning/adventure.yaml
+++ b/src/data/adventures/dead-reckoning/adventure.yaml
@@ -25,7 +25,7 @@ rewards:
contributor:
name: Katharina Sick
url: https://ksick.dev/
- about: DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native
+ about: DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native
and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities,
inline skating rinks, and quiz nights across Europe.
levels:
@@ -156,8 +156,8 @@ levels:
- Argo Events
- Argo Workflows
- Argo CD
- audience: Platform and DevOps engineers who have met these tools before and want to see how they fit together. You
- should be comfortable with Kubernetes, YAML, and reading a tool's logs and UI. Prior exposure to Backstage, Gitea,
+ audience: Platform and DevOps engineers who have met these tools before and want to see how they fit together. You
+ should be comfortable with Kubernetes, YAML, and reading a tool's logs and UI. Prior exposure to Backstage, Gitea,
and the Argo projects helps, but the focus here is the integration between them, not any one tool.
backstory:
- "The commission office is back in business: manifests are filed, repositories are created, and every new vessel
diff --git a/src/data/adventures/echoes-lost-in-orbit.generated.ts b/src/data/adventures/echoes-lost-in-orbit.generated.ts
index 6c52410c2..632d0f2cf 100644
--- a/src/data/adventures/echoes-lost-in-orbit.generated.ts
+++ b/src/data/adventures/echoes-lost-in-orbit.generated.ts
@@ -12,10 +12,10 @@ export const ECHOES_LOST_IN_ORBIT: Adventure = {
contributor: {
name: "Katharina Sick",
url: "https://ksick.dev/",
- aboutHtml: "DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
+ aboutHtml: "DevRelDeveloper Relations at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
},
backstory: [
- "Welcome aboard the GitOps Starliner, a multi-species engineering vessel orbiting the vibrant planet of Polaris-9. Life in this quadrant is wonderfully diverse, from the whispering cloud-dwellers of Nebulon to the rhythmic click-speakers of Crustacea Prime.",
+ "Welcome aboard the GitOpsGit Operations Starliner, a multi-species engineering vessel orbiting the vibrant planet of Polaris-9. Life in this quadrant is wonderfully diverse, from the whispering cloud-dwellers of Nebulon to the rhythmic click-speakers of Crustacea Prime.",
"Communication between species used to be seamless, thanks to the Echo Server, a universal translator that instantly echoed your words in the listener's native format.",
"But lately, something's off. Messages are getting scrambled. Some transmissions never arrive. The Echo Server, deployed across the Staging Moonbase and the Production Outpost, is no longer syncing properly. The Argo CD dashboard shows no active deployments, and telemetry is suspiciously quiet.",
"You've been assigned to restore interstellar communication before the next critical mission.",
@@ -27,7 +27,7 @@ export const ECHOES_LOST_IN_ORBIT: Adventure = {
difficulty: "Beginner",
topics: ["Argo CD"],
learnings: [
- "Debug GitOps flows with Argo CD",
+ "Debug GitOpsGit Operations flows with Argo CD",
"ApplicationSet templating & pitfalls",
"Environment isolation & namespaces",
"Sync policies: automated, prune & self-heal",
@@ -49,9 +49,9 @@ export const ECHOES_LOST_IN_ORBIT: Adventure = {
"Confirm that updates roll out automatically without leaving stale resources behind",
],
toolbox: [
- { name: "kubectl", description: "Kubernetes CLI for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
+ { name: "kubectl", description: "Kubernetes CLICommand Line Interface for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
{ name: "kubens", description: "fast way to switch between Kubernetes namespaces", url: "https://github.com/ahmetb/kubectx" },
- { name: "k9s", description: "terminal UI for managing and inspecting your cluster", url: "https://k9scli.io/" },
+ { name: "k9s", description: "terminal UIUser Interface for managing and inspecting your cluster", url: "https://k9scli.io/" },
],
howToPlay: [
{ title: "Wait for Infrastructure", content: "
Wait around 5 minutes for the Codespace to provision a Kubernetes cluster, Argo CD, and the sample app. Press Cmd+Shift+P (or Ctrl+Shift+P on Windows/Linux) and search for 'View Creation Log' to track progress.
" },
@@ -106,9 +106,9 @@ export const ECHOES_LOST_IN_ORBIT: Adventure = {
"All rollouts complete successfully",
],
toolbox: [
- { name: "kubectl", description: "Kubernetes CLI for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
+ { name: "kubectl", description: "Kubernetes CLICommand Line Interface for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
{ name: "kubens", description: "fast way to switch between Kubernetes namespaces", url: "https://github.com/ahmetb/kubectx" },
- { name: "k9s", description: "terminal UI for managing and inspecting your cluster", url: "https://k9scli.io/" },
+ { name: "k9s", description: "terminal UIUser Interface for managing and inspecting your cluster", url: "https://k9scli.io/" },
{ name: "Argo CD CLI", description: "manage Argo CD applications from the command line", url: "https://argo-cd.readthedocs.io/en/latest/user-guide/commands/argocd/" },
{ name: "Argo Rollouts kubectl plugin", description: "extended kubectl commands for managing rollouts", url: "https://argo-rollouts.readthedocs.io/en/stable/features/kubectl-plugin/" },
],
@@ -182,13 +182,13 @@ kubectl argo rollouts get rollout echo-server -n echo-prod --watch
],
objective: [
"Automated rollout progression to HotROD version 1.76.0 driven by observability signals",
- "OpenTelemetry Collector configured with an OTLP receiver for HotROD traces, a Spanmetrics connector converting traces to metrics, trace export to Jaeger, and metrics export to Prometheus",
+ "OpenTelemetry Collector configured with an OTLPOpenTelemetry Protocol receiver for HotROD traces, a Spanmetrics connector converting traces to metrics, trace export to Jaeger, and metrics export to Prometheus",
"Canary analysis with three PromQL queries: traffic detection (at least 0.05 req/s to prevent idle canaries), error rate below 5%, and 95th-percentile latency below 1000ms",
],
toolbox: [
- { name: "kubectl", description: "Kubernetes CLI for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
+ { name: "kubectl", description: "Kubernetes CLICommand Line Interface for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
{ name: "kubens", description: "fast way to switch between Kubernetes namespaces", url: "https://github.com/ahmetb/kubectx" },
- { name: "k9s", description: "terminal UI for managing and inspecting your cluster", url: "https://k9scli.io/" },
+ { name: "k9s", description: "terminal UIUser Interface for managing and inspecting your cluster", url: "https://k9scli.io/" },
{ name: "Argo CD CLI", description: "manage Argo CD applications from the command line", url: "https://argo-cd.readthedocs.io/en/latest/user-guide/commands/argocd/" },
{ name: "Argo Rollouts kubectl plugin", description: "extended kubectl commands for managing rollouts", url: "https://argo-rollouts.readthedocs.io/en/stable/features/kubectl-plugin/" },
],
@@ -215,7 +215,7 @@ argocd app get otel --refresh
Watch rollout progress. The rollout should progress automatically based on analysis metrics:
diff --git a/src/data/adventures/echoes-lost-in-orbit/adventure.yaml b/src/data/adventures/echoes-lost-in-orbit/adventure.yaml
index 623e144bb..11dc069bd 100644
--- a/src/data/adventures/echoes-lost-in-orbit/adventure.yaml
+++ b/src/data/adventures/echoes-lost-in-orbit/adventure.yaml
@@ -14,11 +14,11 @@ tags:
contributor:
name: Katharina Sick
url: https://ksick.dev/
- about: DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native
+ about: DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native
and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities,
inline skating rinks, and quiz nights across Europe.
backstory:
- - Welcome aboard the GitOps Starliner, a multi-species engineering vessel orbiting the vibrant planet of Polaris-9.
+ - Welcome aboard the GitOps Starliner, a multi-species engineering vessel orbiting the vibrant planet of Polaris-9.
Life in this quadrant is wonderfully diverse, from the whispering cloud-dwellers of Nebulon to the rhythmic
click-speakers of Crustacea Prime.
- Communication between species used to be seamless, thanks to the Echo Server, a universal translator that instantly
@@ -34,7 +34,7 @@ levels:
topics:
- Argo CD
learnings:
- - Debug GitOps flows with Argo CD
+ - Debug GitOps flows with Argo CD
- ApplicationSet templating & pitfalls
- Environment isolation & namespaces
- "Sync policies: automated, prune & self-heal"
@@ -55,13 +55,13 @@ levels:
- Confirm that updates roll out automatically without leaving stale resources behind
toolbox:
- name: kubectl
- description: Kubernetes CLI for interacting with the cluster
+ description: Kubernetes CLI for interacting with the cluster
url: https://kubernetes.io/docs/reference/kubectl/
- name: kubens
description: fast way to switch between Kubernetes namespaces
url: https://github.com/ahmetb/kubectx
- name: k9s
- description: terminal UI for managing and inspecting your cluster
+ description: terminal UI for managing and inspecting your cluster
url: https://k9scli.io/
services:
- name: "Argo CD"
@@ -140,13 +140,13 @@ levels:
- All rollouts complete successfully
toolbox:
- name: kubectl
- description: Kubernetes CLI for interacting with the cluster
+ description: Kubernetes CLI for interacting with the cluster
url: https://kubernetes.io/docs/reference/kubectl/
- name: kubens
description: fast way to switch between Kubernetes namespaces
url: https://github.com/ahmetb/kubectx
- name: k9s
- description: terminal UI for managing and inspecting your cluster
+ description: terminal UI for managing and inspecting your cluster
url: https://k9scli.io/
- name: Argo CD CLI
description: manage Argo CD applications from the command line
@@ -280,19 +280,19 @@ levels:
distributed tracing."
objective:
- Automated rollout progression to HotROD version 1.76.0 driven by observability signals
- - OpenTelemetry Collector configured with an OTLP receiver for HotROD traces, a Spanmetrics connector converting
+ - OpenTelemetry Collector configured with an OTLP receiver for HotROD traces, a Spanmetrics connector converting
traces to metrics, trace export to Jaeger, and metrics export to Prometheus
- "Canary analysis with three PromQL queries: traffic detection (at least 0.05 req/s to prevent idle canaries),
error rate below 5%, and 95th-percentile latency below 1000ms"
toolbox:
- name: kubectl
- description: Kubernetes CLI for interacting with the cluster
+ description: Kubernetes CLI for interacting with the cluster
url: https://kubernetes.io/docs/reference/kubectl/
- name: kubens
description: fast way to switch between Kubernetes namespaces
url: https://github.com/ahmetb/kubectx
- name: k9s
- description: terminal UI for managing and inspecting your cluster
+ description: terminal UI for managing and inspecting your cluster
url: https://k9scli.io/
- name: Argo CD CLI
description: manage Argo CD applications from the command line
@@ -363,7 +363,7 @@ levels:
```
- If you changed the OTel Collector config, restart it:
+ If you changed the OTel Collector config, restart it:
```sh
diff --git a/src/data/adventures/lex-imperfecta.generated.ts b/src/data/adventures/lex-imperfecta.generated.ts
index 858d1b49a..a0c187dc7 100644
--- a/src/data/adventures/lex-imperfecta.generated.ts
+++ b/src/data/adventures/lex-imperfecta.generated.ts
@@ -13,7 +13,7 @@ export const LEX_IMPERFECTA: Adventure = {
contributor: {
name: "Katharina Sick",
url: "https://ksick.dev/",
- aboutHtml: "DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
+ aboutHtml: "DevRelDeveloper Relations at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
},
backstory: [
"The Roman Republic has built a sophisticated legal system to protect its citizens — but the laws were written in haste, and the exceptions were written too generously. Policies go unenforced, the wrong citizens are exempt, and something has slipped through the gates unnoticed. As a newly appointed Praetor, your mission is to restore order before chaos takes hold.",
@@ -37,9 +37,9 @@ export const LEX_IMPERFECTA: Adventure = {
name: "The Twelve Tables",
difficulty: "Beginner",
topics: ["Kyverno", "Kubernetes"],
- audience: "Platform engineers, SREsSite Reliability Engineers, and developers curious about Kubernetes security — no prior Kyverno experience needed, but familiarity with basic kubectl and YAML will help.",
+ audience: "Platform engineers, SREsSite Reliability Engineers, and developers curious about Kubernetes security — no prior Kyverno experience needed, but familiarity with basic kubectl and YAML will help.",
learnings: [
- "How Kyverno ValidatingPolicy resources and CEL validation expressions work",
+ "How Kyverno ValidatingPolicy resources and CELCommon Expression Language validation expressions work",
"The difference between Audit, Deny, and Warn validation actions",
"How to use custom label keys to enforce workload identity standards",
"How Kyverno MutatingPolicy resources automatically patch incoming workloads at admission",
@@ -61,14 +61,14 @@ export const LEX_IMPERFECTA: Adventure = {
],
architecture: [
"
The Twelve Tables enforced Roman law at the gates — before a citizen could act, not after the damage was done. Kyverno works the same way: it intercepts every workload request before it reaches the cluster. A misconfigured policy doesn't just fail to enforce — it fails silently, letting non-compliant workloads slip through while you assume everything is fine.
",
- "
Your Codespace comes with a Kubernetes cluster and Kyverno pre-installed. Three broken policies are already deployed in manifests/policies/ — two ValidatingPolicy resources and one MutatingPolicy. Edit them directly and re-apply with kubectl. The pods in manifests/pods/ are for reference only — no GitOps, no dashboards.
",
+ "
Your Codespace comes with a Kubernetes cluster and Kyverno pre-installed. Three broken policies are already deployed in manifests/policies/ — two ValidatingPolicy resources and one MutatingPolicy. Edit them directly and re-apply with kubectl. The pods in manifests/pods/ are for reference only — no GitOpsGit Operations, no dashboards.
",
],
architectureDiagram: lexImperfectaBeginner,
diagramAlt: "Workload request flows through Kyverno's admission webhook before reaching the Kubernetes cluster. Two ValidatingPolicy resources block non-compliant workloads, and one MutatingPolicy automatically patches admitted workloads with required labels.",
toolbox: [
{ name: "kubectl", description: "Apply and inspect cluster resources", url: "https://kubernetes.io/docs/reference/kubectl/" },
{ name: "kyverno CLI", description: "Test and lint policies locally before applying", url: "https://kyverno.io/docs/kyverno-cli/" },
- { name: "k9s", description: "Explore cluster resources in a terminal UI", url: "https://k9scli.io/" },
+ { name: "k9s", description: "Explore cluster resources in a terminal UIUser Interface", url: "https://k9scli.io/" },
],
howToPlay: [
{ title: "Explore the Cluster", content: `
When your Codespace is ready, four pods are already running — or trying to. Open a terminal and check what's going on:
@@ -91,7 +91,7 @@ kubectl get mutatingpolicy stamp-travel-permit -o yaml
Navigate to ValidatingPolicy resources with :validatingpolicies and MutatingPolicy resources with :mutatingpolicies to inspect all three policies.
` },
{ title: "Fix the Policies", content: `
Review the Objective and investigate what's wrong in manifests/policies/.
All three broken policies are in manifests/policies/. Read them carefully — each has a different kind of misconfiguration.
-
Test Locally with the Kyverno CLI
+
Test Locally with the Kyverno CLICommand Line Interface
Before applying to the cluster, you can use the kyverno CLI to test your policy changes locally against the workload manifests:
kyverno apply manifests/policies/require-labels.yaml --resource manifests/pods/missing-labels.yaml
kyverno apply manifests/policies/no-privileged-containers.yaml --resource manifests/pods/privileged.yaml
@@ -121,7 +121,7 @@ kyverno apply manifests/policies/stamp-travel-permit.yaml --resource manifests/p
name: "Governing the Provinces",
difficulty: "Intermediate",
topics: ["Kyverno", "Policy Reporter", "Kubernetes"],
- audience: "Platform engineers and SREsSite Reliability Engineers who have some familiarity with Kyverno, ideally after completing the Beginner level. You should be comfortable reading Kubernetes YAML and basic kubectl commands.",
+ audience: "Platform engineers and SREsSite Reliability Engineers who have some familiarity with Kyverno, ideally after completing the Beginner level. You should be comfortable reading Kubernetes YAML and basic kubectl commands.",
learnings: [
"How to scope policies using ValidatingPolicy (cluster-wide) and NamespacedValidatingPolicy (per-namespace), and when to use each",
"How CEL expressions in ValidatingPolicy and PolicyException express fine-grained admission conditions",
@@ -153,7 +153,7 @@ kyverno apply manifests/policies/stamp-travel-permit.yaml --resource manifests/p
toolbox: [
{ name: "kubectl", description: "Apply and inspect cluster resources, check namespace labels and policy status", url: "https://kubernetes.io/docs/reference/kubectl/" },
{ name: "kyverno CLI", description: "Test and lint policies locally before applying to the cluster", url: "https://kyverno.io/docs/kyverno-cli/" },
- { name: "k9s", description: "Explore cluster resources and policy reports in a terminal UI", url: "https://k9scli.io/" },
+ { name: "k9s", description: "Explore cluster resources and policy reports in a terminal UIUser Interface", url: "https://k9scli.io/" },
],
howToPlay: [
{ title: "Explore the Estate", content: `
When your Codespace is ready, the policy estate is already deployed, but something is wrong.
@@ -184,7 +184,7 @@ kubectl get policyreports -A
manifests/exceptions/.
Think about what each policy is supposed to cover, and compare that against what it is actually
matching. The namespace labels you saw with kubectl get ns --show-labels are a key part of the picture.
-
Test locally with the Kyverno CLI before applying:
+
Test locally with the Kyverno CLICommand Line Interface before applying:
@@ -201,7 +201,7 @@ policy reports, the OpenReports data behind Policy Reporter, as the audit of rec
helpfulLinks: [
{ title: "Kyverno ValidatingPolicy", url: "https://kyverno.io/docs/policy-types/validating-policy/", description: "Reference docs for ValidatingPolicy and NamespacedValidatingPolicy: the policy types you'll fix" },
{ title: "Kyverno PolicyException", url: "https://kyverno.io/docs/guides/exceptions/", description: "How to write and scope a PolicyException to exempt specific workloads" },
- { title: "CEL Validation Expressions", url: "https://kubernetes.io/docs/reference/using-api/cel/", description: "How CEL expressions work in Kubernetes admission, including accessing namespace context" },
+ { title: "CEL Validation Expressions", url: "https://kubernetes.io/docs/reference/using-api/cel/", description: "How CEL expressions work in Kubernetes admission, including accessing namespace context" },
{ title: "Policy Reporter", url: "https://kyverno.github.io/policy-reporter/", description: "How to use Policy Reporter to audit and visualise policy results across the cluster" },
{ title: "OpenReports Format", url: "https://openreports.io/", description: "The OpenReports standard that Kyverno uses to emit PolicyReport resources" },
],
@@ -216,7 +216,7 @@ policy reports, the OpenReports data behind Policy Reporter, as the audit of rec
name: "Quis Custodiet",
difficulty: "Expert",
topics: ["Kyverno", "Policy Reporter", "Kubernetes"],
- audience: "Security engineers and platform engineers who want to explore the boundary between admission control and runtime security. Completing the Intermediate level first is helpful but not required. You should be comfortable reading Kyverno ValidatingPolicies and CEL expressions. No prior Falco experience required.",
+ audience: "Security engineers and platform engineers who want to explore the boundary between admission control and runtime security. Completing the Intermediate level first is helpful but not required. You should be comfortable reading Kyverno ValidatingPolicies and CELCommon Expression Language expressions. No prior Falco experience required.",
learnings: [
"How Falco rules are structured: conditions, output, and kernel-level fields, and how to write a rule targeting a specific runtime behaviour",
"Why privileged: false is not enough: how Linux capabilities grant host-level access without the privileged flag",
@@ -235,13 +235,13 @@ policy reports, the OpenReports data behind Policy Reporter, as the audit of rec
"And while the Guard slept, an intruder crept in. It declared valid labels, passed the census, and presented itself as a loyal citizen of the Republic. Its papers were in order. Its power was not. Once inside, it reached straight for the census archive: the imperial rolls of every citizen, sealed records it had no right to touch. It reads them on a loop and tries to send them out of the Republic.",
],
objective: [
- "The Praetorian Guard awake: Falco fires an alert every time an unauthorized process reads the census archive, with live alerts streaming into the Falcosidekick UI",
+ "The Praetorian Guard awake: Falco fires an alert every time an unauthorized process reads the census archive, with live alerts streaming into the Falcosidekick UIUser Interface",
"The gate closed: the intruder is denied re-admission. The policy that kept privileged containers out now covers every path to unchecked host power",
"The archive sealed: the census-archive secret is inaccessible to any workload that does not bear the Archivist role",
"The empire-wide laws holding: all intermediate-level checks still green across every province",
],
architecture: [
- "
The estate inherits the full intermediate topology: four province namespaces (gallia, hispania, britannia, aegyptus) and one infra namespace (castra), each labelled as before. Alongside the Kyverno stack and Policy Reporter, the cluster now runs Falco (eBPFextended Berkeley Packet Filter-based, as a DaemonSet) and Falcosidekick with its UI at port 30111. At startup, an intruder pod is already running in one of the provinces, quietly reading the census archive: imperial rolls that only workloads bearing the republic.rome/role: archivist label are permitted to access.
",
+ "
The estate inherits the full intermediate topology: four province namespaces (gallia, hispania, britannia, aegyptus) and one infra namespace (castra), each labelled as before. Alongside the Kyverno stack and Policy Reporter, the cluster now runs Falco (eBPFextended Berkeley Packet Filter-based, as a DaemonSet) and Falcosidekick with its UI at port 30111. At startup, an intruder pod is already running in one of the provinces, quietly reading the census archive: imperial rolls that only workloads bearing the republic.rome/role: archivist label are permitted to access.
",
"
Your working directory is the challenge root. manifests/secrets/ and manifests/workloads/ are already in place. They define the estate and the intruder, and need no changes. Everything else is yours to investigate and fix.
",
],
toolbox: [
@@ -277,8 +277,8 @@ to share expressions across validations, a pattern worth exploring.` },
{ title: "Act 3: Seal the Archive", content: `
Open manifests/policies/: something is missing; the other policies show the structure, write
what's needed, then run make apply.
Going further: even with the archive sealed at admission, any workload admitted with the
-Archivist role can read the secret. Kubernetes RBACRole-Based Access Control can restrict which service accounts may
-get a secret at the APIApplication Programming Interface level, a complementary layer that admission control alone cannot
+Archivist role can read the secret. Kubernetes RBACRole-Based Access Control can restrict which service accounts may
+get a secret at the APIApplication Programming Interface level, a complementary layer that admission control alone cannot
provide.
` },
],
helpfulLinks: [
diff --git a/src/data/adventures/lex-imperfecta/adventure.yaml b/src/data/adventures/lex-imperfecta/adventure.yaml
index a50ae3c3a..4e56e3a6b 100644
--- a/src/data/adventures/lex-imperfecta/adventure.yaml
+++ b/src/data/adventures/lex-imperfecta/adventure.yaml
@@ -26,7 +26,7 @@ rewards:
contributor:
name: Katharina Sick
url: https://ksick.dev/
- about: DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native
+ about: DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native
and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities,
inline skating rinks, and quiz nights across Europe.
levels:
@@ -57,7 +57,7 @@ levels:
**`republic.rome/travel-permit: granted`** label"
- "**All other workloads** deploy and run successfully in the cluster"
what_you_learn:
- - How Kyverno [ValidatingPolicy](https://kyverno.io/docs/policy-types/validating-policy/) resources and [CEL
+ - How Kyverno [ValidatingPolicy](https://kyverno.io/docs/policy-types/validating-policy/) resources and [CEL
validation expressions](https://kubernetes.io/docs/reference/using-api/cel/) work
- The difference between [Audit, Deny, and Warn](https://kyverno.io/docs/policy-types/validating-policy/)
validation actions
@@ -72,7 +72,7 @@ levels:
through while you assume everything is fine."
- Your Codespace comes with a Kubernetes cluster and Kyverno pre-installed. Three broken policies are already
deployed in `manifests/policies/` — two `ValidatingPolicy` resources and one `MutatingPolicy`. Edit them
- directly and re-apply with `kubectl`. The pods in `manifests/pods/` are for reference only — no GitOps, no
+ directly and re-apply with `kubectl`. The pods in `manifests/pods/` are for reference only — no GitOps, no
dashboards.
toolbox:
- name: kubectl
@@ -83,7 +83,7 @@ levels:
description: Test and lint policies locally before applying
- name: k9s
url: https://k9scli.io/
- description: Explore cluster resources in a terminal UI
+ description: Explore cluster resources in a terminal UI
services: []
how_to_play:
- id: explore
@@ -151,7 +151,7 @@ levels:
misconfiguration.
- **Test Locally with the Kyverno CLI**
+ **Test Locally with the Kyverno CLI**
Before applying to the cluster, you can use the `kyverno` CLI to test your policy changes locally against the
@@ -269,7 +269,7 @@ levels:
description: Test and lint policies locally before applying to the cluster
- name: k9s
url: https://k9scli.io/
- description: Explore cluster resources and policy reports in a terminal UI
+ description: Explore cluster resources and policy reports in a terminal UI
how_to_play:
- id: explore
title: Explore the Estate
@@ -316,7 +316,7 @@ levels:
matching. The namespace labels you saw with `kubectl get ns --show-labels` are a key part of the picture.
- **Test locally with the Kyverno CLI before applying:**
+ **Test locally with the Kyverno CLI before applying:**
```bash
@@ -360,7 +360,7 @@ levels:
description: How to write and scope a PolicyException to exempt specific workloads
- title: CEL Validation Expressions
url: https://kubernetes.io/docs/reference/using-api/cel/
- description: How CEL expressions work in Kubernetes admission, including accessing namespace context
+ description: How CEL expressions work in Kubernetes admission, including accessing namespace context
- title: Policy Reporter
url: https://kyverno.github.io/policy-reporter/
description: How to use Policy Reporter to audit and visualise policy results across the cluster
@@ -384,7 +384,7 @@ levels:
unauthorized access.
audience: Security engineers and platform engineers who want to explore the boundary between admission control and
runtime security. Completing the Intermediate level first is helpful but not required. You should be comfortable
- reading Kyverno ValidatingPolicies and CEL expressions. No prior Falco experience required.
+ reading Kyverno ValidatingPolicies and CEL expressions. No prior Falco experience required.
backstory:
- "The Republic's defences have always rested on the law: block the wrong workloads at the gate, and nothing bad
gets in. But the Senate's Praetorian Guard was built for a different threat: the workload that slips through and
@@ -396,7 +396,7 @@ levels:
It reads them on a loop and tries to send them out of the Republic."
objective:
- "**The Praetorian Guard awake**: Falco fires an alert every time an unauthorized process reads the census
- archive, with live alerts streaming into the Falcosidekick UI"
+ archive, with live alerts streaming into the Falcosidekick UI"
- "**The gate closed**: the intruder is denied re-admission. The policy that kept privileged containers out now
covers every path to unchecked host power"
- "**The archive sealed**: the census-archive secret is inaccessible to any workload that does not bear the
diff --git a/src/data/adventures/summaries.ts b/src/data/adventures/summaries.ts
index 207734e60..e0ccbbcf4 100644
--- a/src/data/adventures/summaries.ts
+++ b/src/data/adventures/summaries.ts
@@ -11,7 +11,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
contributor: {
name: "Katharina Sick",
url: "https://ksick.dev/",
- aboutHtml: "DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
+ aboutHtml: "DevRelDeveloper Relations at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
},
isLive: true,
icon: "Compass",
@@ -63,7 +63,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
contributor: {
name: "Katharina Sick",
url: "https://ksick.dev/",
- aboutHtml: "DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
+ aboutHtml: "DevRelDeveloper Relations at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
},
icon: "Scale",
levels: [
@@ -73,7 +73,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
difficulty: "Beginner",
topics: ["Kyverno", "Kubernetes"],
learnings: [
- "How Kyverno ValidatingPolicy resources and CEL validation expressions work",
+ "How Kyverno ValidatingPolicy resources and CELCommon Expression Language validation expressions work",
"The difference between Audit, Deny, and Warn validation actions",
"How to use custom label keys to enforce workload identity standards",
"How Kyverno MutatingPolicy resources automatically patch incoming workloads at admission",
@@ -115,7 +115,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
contributor: {
name: "Simon Schrottner",
url: "https://schrottner.at/",
- aboutHtml: "CNCF Ambassador and maintainer of OpenFeature and JUnit Pioneer. Helps teams release faster and with more confidence through open standards, feature flagging, and the communities that make both possible. A familiar face at KubeCon EU, Devoxx, ContainerDays, and meetups across Europe.",
+ aboutHtml: "CNCFCloud Native Computing Foundation Ambassador and maintainer of OpenFeature and JUnit Pioneer. Helps teams release faster and with more confidence through open standards, feature flagging, and the communities that make both possible. A familiar face at KubeCon EU, Devoxx, ContainerDays, and meetups across Europe.",
},
icon: "FlaskConical",
levels: [
@@ -125,8 +125,8 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
difficulty: "Beginner",
topics: ["OpenFeature", "flagd", "Spring Boot"],
learnings: [
- "How an OpenFeature client and provider work together: the SDK is provider-agnostic and the flagd provider plugs in via dependency only",
- "What remote provider means in practice: the SDK calls a separate flag service (flagd) over gRPC, not parsing flags.json itself",
+ "How an OpenFeature client and provider work together: the SDKSoftware Development Kit is provider-agnostic and the flagd provider plugs in via dependency only",
+ "What remote provider means in practice: the SDK calls a separate flag service (flagd) over gRPCGoogle Remote Procedure Call, not parsing flags.json itself",
"What flags.json looks like for flagd (state, variants, defaultVariant)",
"Why hot-reload of the flag file matters operationally: configuration without redeploy",
],
@@ -149,7 +149,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
topics: ["OpenFeature", "OpenTelemetry", "Grafana", "Spring Boot"],
learnings: [
"How the OpenFeature OpenTelemetry hooks (TracesHook and MetricsHook) join flag evaluations to the rest of an application's telemetry without a separate ingestion path",
- "How to author your own Hook: a tiny class that copies merged-eval-context attributes onto the active OTel span, closing the loop between why a flag resolved the way it did and what the operator sees in Tempo",
+ "How to author your own Hook: a tiny class that copies merged-eval-context attributes onto the active OTelOpenTelemetry span, closing the loop between why a flag resolved the way it did and what the operator sees in Tempo",
"How fractional rollout in flagd buckets users by targetingKey (same key, same bucket, every request) and how to read that bucketing off a dashboard",
"How a flag flip is a faster operational lever than a redeploy when a rollout is misbehaving: the difference between a one-line config change and a twenty-minute deployment",
],
@@ -165,7 +165,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
contributor: {
name: "Katharina Sick",
url: "https://ksick.dev/",
- aboutHtml: "DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
+ aboutHtml: "DevRelDeveloper Relations at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
},
icon: "Telescope",
levels: [
@@ -185,7 +185,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
difficulty: "Intermediate",
topics: ["OpenTelemetry", "OpenLLMetry", "Jaeger", "Prometheus"],
learnings: [
- "Instrument RAG pipelines with OpenLLMetry",
+ "Instrument RAGRetrieval-Augmented Generation pipelines with OpenLLMetry",
"Create custom OpenTelemetry metrics in Python",
"Write PromQL queries & recording rules in Prometheus",
],
@@ -197,7 +197,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
topics: ["OpenTelemetry", "OpenLLMetry", "Jaeger"],
learnings: [
"OpenTelemetry GenAI semantic conventions",
- "Tail sampling in the OTel Collector",
+ "Tail sampling in the OTelOpenTelemetry Collector",
],
},
],
@@ -211,7 +211,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
contributor: {
name: "Katharina Sick",
url: "https://ksick.dev/",
- aboutHtml: "DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
+ aboutHtml: "DevRelDeveloper Relations at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
},
icon: "Building2",
levels: [
@@ -222,7 +222,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
topics: ["OpenTofu"],
learnings: [
"Infrastructure as Code with OpenTofu",
- "Remote state management with GCS backend",
+ "Remote state management with GCSGoogle Cloud Storage backend",
"Dynamic resource provisioning with for_each",
"Conditional resources with the enabled meta-argument, new in OpenTofu",
],
@@ -261,7 +261,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
contributor: {
name: "Katharina Sick",
url: "https://ksick.dev/",
- aboutHtml: "DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
+ aboutHtml: "DevRelDeveloper Relations at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
},
icon: "Satellite",
levels: [
@@ -271,7 +271,7 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
difficulty: "Beginner",
topics: ["Argo CD"],
learnings: [
- "Debug GitOps flows with Argo CD",
+ "Debug GitOpsGit Operations flows with Argo CD",
"ApplicationSet templating & pitfalls",
"Environment isolation & namespaces",
"Sync policies: automated, prune & self-heal",
diff --git a/src/data/adventures/the-ai-observatory.generated.ts b/src/data/adventures/the-ai-observatory.generated.ts
index e1a83b4ec..d82871c23 100644
--- a/src/data/adventures/the-ai-observatory.generated.ts
+++ b/src/data/adventures/the-ai-observatory.generated.ts
@@ -12,7 +12,7 @@ export const THE_AI_OBSERVATORY: Adventure = {
contributor: {
name: "Katharina Sick",
url: "https://ksick.dev/",
- aboutHtml: "DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
+ aboutHtml: "DevRelDeveloper Relations at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities, inline skating rinks, and quiz nights across Europe.",
},
backstory: [
"You are stationed at Perimeter Alpha, a research outpost on the newly discovered planet HB-7742. The station is run by HubSystem, a central AI that manages everything from life support to data analysis.",
@@ -58,9 +58,9 @@ export const THE_AI_OBSERVATORY: Adventure = {
],
toolbox: [
{ name: "python", description: "programming language used for the HubSystem application" },
- { name: "kubectl", description: "Kubernetes CLI for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
+ { name: "kubectl", description: "Kubernetes CLICommand Line Interface for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
{ name: "kubens", description: "fast way to switch between Kubernetes namespaces", url: "https://github.com/ahmetb/kubectx" },
- { name: "k9s", description: "terminal UI for managing and inspecting your cluster", url: "https://k9scli.io/" },
+ { name: "k9s", description: "terminal UIUser Interface for managing and inspecting your cluster", url: "https://k9scli.io/" },
],
howToPlay: [
{ title: "Wait for Infrastructure", content: "
Wait ~10 minutes for all infrastructure to initialize.
Port 30103: Jaeger. Analyze the traces sent by HubSystem.
` },
- { title: "Instrument the App", content: `
The application code is in ./hubsystem.py. Add OpenTelemetry instrumentation using OpenLLMetry. The OTel
+ { title: "Instrument the App", content: `
The application code is in ./hubsystem.py. Add OpenTelemetry instrumentation using OpenLLMetry. The OTelOpenTelemetry
Collector and Jaeger are already configured correctly; you only need to instrument the app. You do not need to
interact with Kubernetes directly. The cluster is already running, so focus on the Python code.
` },
{ title: "Run and Investigate", content: `
Run the application, interact with the AI to generate traces, then check Jaeger:
@@ -92,7 +92,7 @@ interact with Kubernetes directly. The cluster is already running, so focus on t
difficulty: "Intermediate",
topics: ["OpenTelemetry", "OpenLLMetry", "Jaeger", "Prometheus"],
learnings: [
- "Instrument RAG pipelines with OpenLLMetry",
+ "Instrument RAGRetrieval-Augmented Generation pipelines with OpenLLMetry",
"Create custom OpenTelemetry metrics in Python",
"Write PromQL queries & recording rules in Prometheus",
],
@@ -100,7 +100,7 @@ interact with Kubernetes directly. The cluster is already running, so focus on t
discussionUrl: `${COMMUNITY_URL}/t/instrument-debug-a-rag-pipeline-adventure-03-intermediate-is-live/936/2`,
deadline: "2026-03-08T23:59:00+01:00",
intro: [
- "ART's RAG pipeline is retrieving entertainment data instead of navigation coordinates and won't calculate your jump. Instrument the full retrieval pipeline with OpenLLMetry, build a custom OTel metric to quantify the distraction, and write a Prometheus recording rule to prove it.",
+ "ART's RAG pipeline is retrieving entertainment data instead of navigation coordinates and won't calculate your jump. Instrument the full retrieval pipeline with OpenLLMetry, build a custom OTelOpenTelemetry metric to quantify the distraction, and write a Prometheus recording rule to prove it.",
],
backstory: [
"You're a rogue SecUnit who just escaped from Preservation Station after being identified. A researcher helped you flee aboard the Perihelion, a university research vessel with a very opinionated AI.",
@@ -121,14 +121,14 @@ interact with Kubernetes directly. The cluster is already running, so focus on t
"Restore the navigation system so ART successfully calculates jump coordinates to RaviHyral",
],
architecture: [
- "
The ART Pilot System runs as a local Python application outside Kubernetes, using a RAG (Retrieval-Augmented Generation) architecture. AI infrastructure (Ollama for LLM, Qdrant for vector storage) and observability tools (OpenTelemetry Collector, Jaeger, Prometheus) run inside Kubernetes.
",
+ "
The ART Pilot System runs as a local Python application outside Kubernetes, using a RAG (Retrieval-Augmented Generation) architecture. AI infrastructure (Ollama for LLMLarge Language Model, Qdrant for vector storage) and observability tools (OpenTelemetry Collector, Jaeger, Prometheus) run inside Kubernetes.
",
"
This setup lets you focus on observability patterns: edit Python code, run it, and see traces and metrics immediately without a build or deploy cycle.
",
],
toolbox: [
{ name: "python", description: "programming language used for the ART application" },
- { name: "kubectl", description: "Kubernetes CLI for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
+ { name: "kubectl", description: "Kubernetes CLICommand Line Interface for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
{ name: "kubens", description: "fast way to switch between Kubernetes namespaces", url: "https://github.com/ahmetb/kubectx" },
- { name: "k9s", description: "terminal UI for managing and inspecting your cluster", url: "https://k9scli.io/" },
+ { name: "k9s", description: "terminal UIUser Interface for managing and inspecting your cluster", url: "https://k9scli.io/" },
],
howToPlay: [
{ title: "Wait for Infrastructure", content: "
Wait ~15 minutes for all infrastructure to initialize.
" },
@@ -166,7 +166,7 @@ make traffic
topics: ["OpenTelemetry", "OpenLLMetry", "Jaeger"],
learnings: [
"OpenTelemetry GenAI semantic conventions",
- "Tail sampling in the OTel Collector",
+ "Tail sampling in the OTelOpenTelemetry Collector",
],
codespacesUrl: `${CODESPACES_BASE}?devcontainer_path=.devcontainer%2F03-the-ai-observatory_03-expert%2Fdevcontainer.json&quickstart=1`,
discussionUrl: `${COMMUNITY_URL}/t/reduce-telemetry-noise-adventure-03-expert-is-live/999/1`,
@@ -193,13 +193,13 @@ make traffic
"Configure tail sampling in the OpenTelemetry Collector to keep only traces that contain errors or take longer than 5 seconds",
],
architecture: [
- "
Same setup as the intermediate level: the ART Pilot System runs as a local Python application outside Kubernetes with a RAG architecture. AI infrastructure (Ollama, Qdrant) and observability tools (OpenTelemetry Collector, Jaeger) run inside Kubernetes.
",
+ "
Same setup as the intermediate level: the ART Pilot System runs as a local Python application outside Kubernetes with a RAGRetrieval-Augmented Generation architecture. AI infrastructure (Ollama, Qdrant) and observability tools (OpenTelemetry Collector, Jaeger) run inside Kubernetes.
",
],
toolbox: [
{ name: "python", description: "programming language used for the ART application" },
- { name: "kubectl", description: "Kubernetes CLI for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
+ { name: "kubectl", description: "Kubernetes CLICommand Line Interface for interacting with the cluster", url: "https://kubernetes.io/docs/reference/kubectl/" },
{ name: "kubens", description: "fast way to switch between Kubernetes namespaces", url: "https://github.com/ahmetb/kubectx" },
- { name: "k9s", description: "terminal UI for managing and inspecting your cluster", url: "https://k9scli.io/" },
+ { name: "k9s", description: "terminal UIUser Interface for managing and inspecting your cluster", url: "https://k9scli.io/" },
],
howToPlay: [
{ title: "Wait for Infrastructure", content: "
Wait ~15 minutes for all infrastructure to initialize.
" },
diff --git a/src/data/adventures/the-ai-observatory/adventure.yaml b/src/data/adventures/the-ai-observatory/adventure.yaml
index 5849d684f..11f18b30a 100644
--- a/src/data/adventures/the-ai-observatory/adventure.yaml
+++ b/src/data/adventures/the-ai-observatory/adventure.yaml
@@ -14,7 +14,7 @@ tags:
contributor:
name: Katharina Sick
url: https://ksick.dev/
- about: DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native
+ about: DevRel at Dynatrace and co-organizer of Cloud Native Linz. Passionate about building user-friendly Cloud Native
and Kubernetes solutions, with a background in mobile and backend development. Found in tech and sports communities,
inline skating rinks, and quiz nights across Europe.
backstory:
@@ -68,13 +68,13 @@ levels:
- name: python
description: programming language used for the HubSystem application
- name: kubectl
- description: Kubernetes CLI for interacting with the cluster
+ description: Kubernetes CLI for interacting with the cluster
url: https://kubernetes.io/docs/reference/kubectl/
- name: kubens
description: fast way to switch between Kubernetes namespaces
url: https://github.com/ahmetb/kubectx
- name: k9s
- description: terminal UI for managing and inspecting your cluster
+ description: terminal UI for managing and inspecting your cluster
url: https://k9scli.io/
services:
- name: "Jaeger"
@@ -85,7 +85,7 @@ levels:
content: Wait ~10 minutes for all infrastructure to initialize.
- title: Instrument the App
content: |-
- The application code is in `./hubsystem.py`. Add OpenTelemetry instrumentation using OpenLLMetry. The OTel
+ The application code is in `./hubsystem.py`. Add OpenTelemetry instrumentation using OpenLLMetry. The OTel
Collector and Jaeger are already configured correctly; you only need to instrument the app. You do not need to
interact with Kubernetes directly. The cluster is already running, so focus on the Python code.
- title: Run and Investigate
@@ -115,7 +115,7 @@ levels:
- Jaeger
- Prometheus
learnings:
- - Instrument RAG pipelines with OpenLLMetry
+ - Instrument RAG pipelines with OpenLLMetry
- Create custom OpenTelemetry metrics in Python
- Write PromQL queries & recording rules in Prometheus
devcontainer: 03-the-ai-observatory_02-intermediate
@@ -123,7 +123,7 @@ levels:
deadline: "2026-03-08T23:59:00+01:00"
intro:
- ART's RAG pipeline is retrieving entertainment data instead of navigation coordinates and won't calculate your
- jump. Instrument the full retrieval pipeline with OpenLLMetry, build a custom OTel metric to quantify the
+ jump. Instrument the full retrieval pipeline with OpenLLMetry, build a custom OTel metric to quantify the
distraction, and write a Prometheus recording rule to prove it.
backstory:
- You're a rogue SecUnit who just escaped from Preservation Station after being identified. A researcher helped
@@ -149,7 +149,7 @@ levels:
- Restore the navigation system so ART successfully calculates jump coordinates to RaviHyral
architecture:
- The ART Pilot System runs as a local Python application outside Kubernetes, using a RAG (Retrieval-Augmented
- Generation) architecture. AI infrastructure (Ollama for LLM, Qdrant for vector storage) and observability tools
+ Generation) architecture. AI infrastructure (Ollama for LLM, Qdrant for vector storage) and observability tools
(OpenTelemetry Collector, Jaeger, Prometheus) run inside Kubernetes.
- "This setup lets you focus on observability patterns: edit Python code, run it, and see traces and metrics
immediately without a build or deploy cycle."
@@ -157,13 +157,13 @@ levels:
- name: python
description: programming language used for the ART application
- name: kubectl
- description: Kubernetes CLI for interacting with the cluster
+ description: Kubernetes CLI for interacting with the cluster
url: https://kubernetes.io/docs/reference/kubectl/
- name: kubens
description: fast way to switch between Kubernetes namespaces
url: https://github.com/ahmetb/kubectx
- name: k9s
- description: terminal UI for managing and inspecting your cluster
+ description: terminal UI for managing and inspecting your cluster
url: https://k9scli.io/
services:
- name: "Prometheus"
@@ -221,7 +221,7 @@ levels:
- Jaeger
learnings:
- OpenTelemetry GenAI semantic conventions
- - Tail sampling in the OTel Collector
+ - Tail sampling in the OTel Collector
devcontainer: 03-the-ai-observatory_03-expert
discussion_url: /t/reduce-telemetry-noise-adventure-03-expert-is-live/999/1
deadline: "2026-03-08T23:59:00+01:00"
@@ -256,19 +256,19 @@ levels:
than 5 seconds
architecture:
- "Same setup as the intermediate level: the ART Pilot System runs as a local Python application outside
- Kubernetes with a RAG architecture. AI infrastructure (Ollama, Qdrant) and observability tools (OpenTelemetry
+ Kubernetes with a RAG architecture. AI infrastructure (Ollama, Qdrant) and observability tools (OpenTelemetry
Collector, Jaeger) run inside Kubernetes."
toolbox:
- name: python
description: programming language used for the ART application
- name: kubectl
- description: Kubernetes CLI for interacting with the cluster
+ description: Kubernetes CLI for interacting with the cluster
url: https://kubernetes.io/docs/reference/kubectl/
- name: kubens
description: fast way to switch between Kubernetes namespaces
url: https://github.com/ahmetb/kubectx
- name: k9s
- description: terminal UI for managing and inspecting your cluster
+ description: terminal UI for managing and inspecting your cluster
url: https://k9scli.io/
services:
- name: "Jaeger"
diff --git a/src/pages/ChallengeDetail.tsx b/src/pages/ChallengeDetail.tsx
index ee504aced..3ebc02ef5 100644
--- a/src/pages/ChallengeDetail.tsx
+++ b/src/pages/ChallengeDetail.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, type JSX } from "react";
+import { useState, useEffect, useRef, type JSX } from "react";
import { useParams, useLoaderData, Link } from "react-router";
import type { MetaFunction, LoaderFunctionArgs, LinksFunction } from "react-router";
import { Check, Clock, ExternalLink, ArrowRight } from "lucide-react";
@@ -28,6 +28,7 @@ import { stripHtml } from "@/lib/markdown";
import { isDeadlinePast, isSolutionUnlocked, resolveDiscussionUrl, escapeHtmlAttr } from "@/lib/utils";
import { InlineProse } from "@/components/InlineProse";
import { Abbr } from "@/components/Abbr";
+import { useAbbrTooltips } from "@/hooks/useAbbrTooltips";
export const links: LinksFunction = () => [
{ rel: "preload", href: `${import.meta.env.BASE_URL}fonts/jetbrains-mono-latin-400-normal.woff2`, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
@@ -113,6 +114,8 @@ const StructuredLayout = ({ adventure, level, rewardsBelowFold, hasSolution }: S
const { intro, objective, toolbox, backstory, architecture, architectureDiagram, diagramAlt, architectureAscii, howToPlay, helpfulLinks, verification } = level;
const levelUrl = `${SITE_URL}/adventures/${adventure.id}/levels/${level.id}/`;
const linkedinShareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(levelUrl)}`;
+ const objectiveLearningsRef = useRef(null);
+ useAbbrTooltips(objectiveLearningsRef, [level.learnings, level.objective]);
return (
<>
{/* Header */}
@@ -144,7 +147,7 @@ const StructuredLayout = ({ adventure, level, rewardsBelowFold, hasSolution }: S
Open in GitHub Codespaces. The devcontainerdevelopment container: a portable, reproducible coding environment defined by a configuration file is pre-configured and starts automatically. When you push from Codespaces, GitHub forks the repository to your account automatically.
`,
+ `
Open in GitHub Codespaces. The devcontainer is pre-configured and starts automatically. When you push from Codespaces, GitHub forks the repository to your account automatically.
`,
`
Prefer working locally? Clone the repo and open it in any editor that supports the Dev Containers specification (VS CodeVisual Studio Code, JetBrains, and others). The devcontainer config will be detected automatically.