diff --git a/.devcontainer/05-lex-imperfecta_02-intermediate/devcontainer.json b/.devcontainer/05-lex-imperfecta_02-intermediate/devcontainer.json new file mode 100644 index 00000000..a5116789 --- /dev/null +++ b/.devcontainer/05-lex-imperfecta_02-intermediate/devcontainer.json @@ -0,0 +1,33 @@ +{ + "name": "âš–ī¸ Adventure 05 | 🟡 Intermediate (Governing the Provinces)", + "image": "mcr.microsoft.com/devcontainers/base:bullseye", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/adventures/05-lex-imperfecta/intermediate", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "postCreateCommand": "bash /workspaces/${localWorkspaceFolderBasename}/.devcontainer/05-lex-imperfecta_02-intermediate/post-create.sh", + "postStartCommand": "bash /workspaces/${localWorkspaceFolderBasename}/.devcontainer/05-lex-imperfecta_02-intermediate/post-start.sh", + "customizations": { + "codespaces": { + "openFiles": [ + "manifests/policies/no-privileged-containers.yaml", + "manifests/policies/require-census.yaml", + "manifests/policies/aegyptus-require-scribe-role.yaml", + "manifests/exceptions/aegyptus-legacy-workload.yaml" + ], + "permissions": { + "codespaces": "write" + } + } + }, + "forwardPorts": [30110], + "portsAttributes": { + "30110": { + "label": "Policy Reporter", + "onAutoForward": "notify" + } + }, + "otherPortsAttributes": { + "onAutoForward": "ignore" + } +} diff --git a/.devcontainer/05-lex-imperfecta_02-intermediate/post-create.sh b/.devcontainer/05-lex-imperfecta_02-intermediate/post-create.sh new file mode 100755 index 00000000..6908e39e --- /dev/null +++ b/.devcontainer/05-lex-imperfecta_02-intermediate/post-create.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# shellcheck disable=SC1091 +source "$REPO_ROOT/lib/scripts/tracker.sh" +set_tracking_context "lex-imperfecta" "intermediate" "05" "06" "2026" +track_container_created + +"$REPO_ROOT/lib/shared/init.sh" --version v0.17.0 +"$REPO_ROOT/lib/kubernetes/init.sh" \ + --kind-version v0.32.0 \ + --kubectl-version v1.36.1 \ + --kubens-version v0.11.0 \ + --k9s-version v0.50.18 \ + --helm-version v4.2.0 +"$REPO_ROOT/lib/kyverno/init.sh" --version 3.8.1 --cli-version v1.18.1 +"$REPO_ROOT/lib/policy-reporter/init.sh" --version 3.7.4 diff --git a/.devcontainer/05-lex-imperfecta_02-intermediate/post-start.sh b/.devcontainer/05-lex-imperfecta_02-intermediate/post-start.sh new file mode 100755 index 00000000..cd27f8da --- /dev/null +++ b/.devcontainer/05-lex-imperfecta_02-intermediate/post-start.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CHALLENGE_DIR="$REPO_ROOT/adventures/05-lex-imperfecta/intermediate" + +echo "✨ Starting Lex Imperfecta - Intermediate Level" + +echo "đŸ›ī¸ Creating provinces..." +kubectl apply -f - <<'EOF' +apiVersion: v1 +kind: Namespace +metadata: + name: gallia + labels: + republic.rome/realm: province +--- +apiVersion: v1 +kind: Namespace +metadata: + name: hispania + labels: + republic.rome/realm: province +--- +apiVersion: v1 +kind: Namespace +metadata: + name: aegyptus + labels: + republic.rome/realm: province +--- +apiVersion: v1 +kind: Namespace +metadata: + name: britannia + labels: + republic.rome/realm: province +--- +apiVersion: v1 +kind: Namespace +metadata: + name: castra + labels: + republic.rome/realm: infra +EOF + +echo "âš–ī¸ Applying policies..." +kubectl apply -f "$CHALLENGE_DIR/manifests/policies/" + +echo "📜 Applying exceptions..." +kubectl apply -f "$CHALLENGE_DIR/manifests/exceptions/" + +echo "đŸŸī¸ Deploying workloads..." +# Some workloads may be blocked by misconfigured policies — this is intentional. +# Open Policy Reporter at http://localhost:30110 to start investigating. +kubectl apply -f "$CHALLENGE_DIR/manifests/workloads/" 2>&1 || true + +# shellcheck disable=SC1091 +source "$REPO_ROOT/lib/scripts/tracker.sh" +set_tracking_context "lex-imperfecta" "intermediate" "05" "06" "2026" +track_container_initialized diff --git a/.gitignore b/.gitignore index 051e98eb..7b534972 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ infra/tracker/tracker .offon-session-id # Custom ignores/includes -.prompts +.prompts \ No newline at end of file diff --git a/adventures/05-lex-imperfecta/README.md b/adventures/05-lex-imperfecta/README.md index ad7c1ebf..928a77a9 100644 --- a/adventures/05-lex-imperfecta/README.md +++ b/adventures/05-lex-imperfecta/README.md @@ -5,7 +5,7 @@ in haste, and the exceptions were written too generously. Policies go unenforced something has slipped through the gates unnoticed. As a newly appointed Praetor, your mission is to restore order before chaos takes hold. -**Technologies:** Kyverno, Falco, Policy Reporter, Argo CD, Kubernetes +**Technologies:** Kyverno, Falco, Policy Reporter, Kubernetes The entire **infrastructure is pre-provisioned in your Codespace** **You don't need to set up anything locally. Just focus on solving the problem.** diff --git a/adventures/05-lex-imperfecta/docs/index.yaml b/adventures/05-lex-imperfecta/docs/index.yaml index e7355b79..b915e9ce 100644 --- a/adventures/05-lex-imperfecta/docs/index.yaml +++ b/adventures/05-lex-imperfecta/docs/index.yaml @@ -4,6 +4,7 @@ emoji: "âš–ī¸" tags: - Kyverno + - Policy Reporter - Kubernetes backstory: diff --git a/adventures/05-lex-imperfecta/docs/intermediate.yaml b/adventures/05-lex-imperfecta/docs/intermediate.yaml new file mode 100644 index 00000000..92ebbe82 --- /dev/null +++ b/adventures/05-lex-imperfecta/docs/intermediate.yaml @@ -0,0 +1,184 @@ +level: intermediate +emoji: "🟡" +title: "Governing the Provinces" +devcontainer: lex-imperfecta_intermediate +community_url: "" # TODO: add community thread URL once the adventure is live + +summary: "Fix a misconfigured Kyverno policy estate and use Policy Reporter to restore proper governance across the Republic's provinces." + +audience: >- + Platform engineers and SREs who have some familiarity with Kyverno — ideally after completing the Beginner level. + You should be comfortable reading Kubernetes YAML and basic kubectl commands. + +backstory: + - >- + The Republic has grown. What once was a single city is now a sprawling empire of provinces, each governed by + different magistrates with different needs. The legal scholars decided to catalogue every law in a central + archive — the Tabularium — so that each province's statutes could be tracked and audited in one place. + - >- + But cataloguing the laws introduced new chaos. Policies meant for one province are bleeding into another. + Exceptions that were meant to be narrow have been written too broadly. And somewhere in the estate, a + workload is slipping through that shouldn't be. + - >- + The Tabularium's auditors have handed you a report: Policy Reporter shows violations where there should be + none, and silence where there should be enforcement. Your mission: investigate the policy estate, fix the + scoping issues, and restore order before the provinces descend into chaos. + +objective: + - >- + The **empire-wide laws** enforce correctly across every province — privileged containers are blocked, + and every workload declares a valid `republic.rome/gens` and a `republic.rome/province` that matches the + namespace it runs in. Policies must scope to provinces using the namespace labels as the source of truth, + not by hardcoding individual namespace names. + - >- + **Aegyptus's provincial scribe law** takes effect only within Aegyptus — it admits only workloads carrying + `republic.rome/role: scribe` there, and has no power over the other provinces + - >- + The **legacy exception** is scoped to Aegyptus alone — only Aegyptus's grandfathered workload is spared the + census, and no workload in any other province can claim it + - >- + The **Tabularium's ledger** is clean and on file — the estate's policy report is exported in the OpenReports + format and saved as `estate-audit.yaml` + +what_you_learn: + - >- + How to scope policies using [`ValidatingPolicy`](https://kyverno.io/docs/policy-types/validating-policy/) + (cluster-wide) and [`NamespacedValidatingPolicy`](https://kyverno.io/docs/policy-types/validating-policy/) + (per-namespace), and when to use each + - >- + How [CEL expressions](https://kubernetes.io/docs/reference/using-api/cel/) in `ValidatingPolicy` and + `PolicyException` express fine-grained admission conditions + - >- + How to write and scope a [`PolicyException`](https://kyverno.io/docs/policy-types/policy-exception/) + correctly so only the intended workloads are exempt + - >- + How to use [Policy Reporter](https://kyverno.github.io/policy-reporter/) and the + [OpenReports](https://kyverno.github.io/policy-reporter/guide/openreports/) format to audit and debug a + policy estate across multiple namespaces + +architecture: + - >- + The Republic's policy estate spans five namespaces. Four are **provinces** — `gallia`, `hispania`, + `britannia`, and `aegyptus` — each carrying the label `republic.rome/realm: province`. The fifth, + `castra`, is the **infra** namespace (`republic.rome/realm: infra`) and operates outside the provinces' + legal jurisdiction. Use `kubectl get ns --show-labels` to inspect these labels — they are what the + cluster-wide policies use to decide where to enforce. + - >- + The estate is governed by a handful of Kyverno policies. Two are **empire-wide**: + `no-privileged-containers` blocks privileged containers, and `require-census` requires every workload to + declare its `republic.rome/gens` and a `republic.rome/province` that matches its namespace. Both are meant + to apply across the provinces but not the infra realm. Aegyptus also carries a **local law of its own** — + `aegyptus-require-scribe-role` — meant to admit only scribe workloads *within Aegyptus*. Finally, a + **`PolicyException`** grandfathers Aegyptus's single legacy workload, which predates the census, so it + alone may run without a gens. Some of these are no longer doing exactly what they were meant to. + - >- + **Edit `manifests/policies/` and `manifests/exceptions/`** — that is where the scoping has gone wrong. + Leave `manifests/workloads/` and `manifests/namespaces/` alone; the workloads represent the citizens and + their expected state, and the namespace labels are the ground truth the policies are measured against. + Policies act at admission, so after each change run `make apply` to redeploy the workloads and + re-evaluate the estate, then `make verify` to check your progress. When the estate is in order, file the + audit with the Tabularium (see How to Play). + +architecture_diagram: "" # TODO: add diagram + +toolbox: + - name: kubectl + url: "https://kubernetes.io/docs/reference/kubectl/" + description: Apply and inspect cluster resources, check namespace labels and policy status + - name: kyverno CLI + url: "https://kyverno.io/docs/kyverno-cli/" + 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 + +services: + - name: Policy Reporter + port: 30110 + description: Audit the full policy estate — see which policies apply to which namespaces and where violations exist + +how_to_play: + - id: explore + title: "Explore the Estate" + content: | + When your Codespace is ready, the policy estate is already deployed — but something is wrong. + Open Policy Reporter at the forwarded port **30110** to get an overview of the estate: + + - Which namespaces have violations? + - Which policies are generating results, and which are silent when they shouldn't be? + + Then dig into the cluster: + + ```bash + # Inspect the namespace topology — the labels here drive policy scoping + kubectl get ns --show-labels + + # List all policies — note which are cluster-wide and which are namespaced + kubectl get validatingpolicies + kubectl get namespacedvalidatingpolicies -A + + # Inspect any policy or exception in full + kubectl get validatingpolicy -o yaml + kubectl get policyexceptions -A -o yaml + + # See the raw OpenReports data behind Policy Reporter + kubectl get policyreports -A + ``` + + You can also launch **k9s** for a terminal UI view: + + ```bash + k9s + ``` + + - id: fix + title: "Fix the Policies" + content: | + Review the [Objective](#objective) and investigate what is wrong in `manifests/policies/` and + `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:** + + ```bash + kyverno apply manifests/policies/require-census.yaml --resource manifests/workloads/citizens.yaml + kyverno apply manifests/policies/aegyptus-require-scribe-role.yaml --resource manifests/workloads/aegyptus-legacy-scribe.yaml + ``` + + **Apply your changes to the cluster:** + + ```bash + make apply + ``` + + Policies only act at admission, so `make apply` redeploys the workloads to re-evaluate the estate against + your changes. Then check Policy Reporter again — the picture should improve as you fix each issue. + + - id: file + title: "File the Audit" + content: | + Once the estate is in order, the Senate expects the Tabularium's ledger on file. Export the cluster's + policy reports, the OpenReports data behind Policy Reporter, as the audit of record. + + ```bash + kubectl get policyreports -A -o yaml > estate-audit.yaml + ``` + +helpful_links: + - 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/policy-types/policy-exception/" + 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: 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://kyverno.github.io/policy-reporter/guide/openreports/" + description: The OpenReports standard that Kyverno uses to emit PolicyReport resources diff --git a/adventures/05-lex-imperfecta/intermediate/Makefile b/adventures/05-lex-imperfecta/intermediate/Makefile new file mode 100644 index 00000000..2a1bd249 --- /dev/null +++ b/adventures/05-lex-imperfecta/intermediate/Makefile @@ -0,0 +1,47 @@ +.PHONY: help apply verify + +help: + @echo "Lex Imperfecta - Intermediate: Available Commands:" + @echo " make apply Reset workloads, re-apply policies and exceptions" + @echo " make verify Run the verification script" + +apply: + @kubectl delete validatingpolicies --all --ignore-not-found > /dev/null 2>&1; \ + kubectl delete namespacedvalidatingpolicies --all -A --ignore-not-found > /dev/null 2>&1; \ + true + @echo "Applying Policies:"; \ + for f in manifests/policies/*.yaml; do \ + name=$$(grep '^ name:' "$$f" | head -1 | awk '{print $$2}'); \ + out=$$(kubectl apply -f "$$f" 2>&1); rc=$$?; \ + if [ $$rc -ne 0 ]; then \ + echo ""; \ + echo "$$out"; \ + exit 1; \ + fi; \ + echo " $$name"; \ + done; \ + echo "" + @echo "Applying Exceptions:"; \ + for f in manifests/exceptions/*.yaml; do \ + name=$$(grep '^ name:' "$$f" | head -1 | awk '{print $$2}'); \ + out=$$(kubectl apply -f "$$f" 2>&1); rc=$$?; \ + if [ $$rc -ne 0 ]; then \ + echo ""; \ + echo "$$out"; \ + exit 1; \ + fi; \ + echo " $$name"; \ + done; \ + echo "" + @echo "Deploying Workloads..." + @for ns in gallia hispania aegyptus britannia castra; do \ + kubectl delete pods --all -n $$ns --ignore-not-found --grace-period=0 --force \ + > /dev/null 2>&1 & \ + done; \ + wait + @kubectl apply -f manifests/workloads/ 2>&1 || true + @echo "" + @echo "Blocked workloads above mean a policy is enforcing. Run 'make verify' to check your progress." + +verify: + @./verify.sh diff --git a/adventures/05-lex-imperfecta/intermediate/manifests/exceptions/aegyptus-legacy-workload.yaml b/adventures/05-lex-imperfecta/intermediate/manifests/exceptions/aegyptus-legacy-workload.yaml new file mode 100644 index 00000000..cacedb15 --- /dev/null +++ b/adventures/05-lex-imperfecta/intermediate/manifests/exceptions/aegyptus-legacy-workload.yaml @@ -0,0 +1,21 @@ +apiVersion: policies.kyverno.io/v1 +kind: PolicyException +metadata: + name: aegyptus-legacy-workload + namespace: aegyptus + annotations: + policies.kyverno.io/description: >- + The scribes of Aegyptus have served the Republic since before the Twelve + Tables were written. Their ancient tools cannot carry the gens label, so + the Senate has granted them a formal exception — workloads that declare + themselves legacy ('republic.rome/legacy: "true"') are spared the census. +spec: + policyRefs: + - name: require-census + kind: ValidatingPolicy + matchConditions: + - name: is-legacy + expression: >- + has(object.metadata.labels) && + 'republic.rome/legacy' in object.metadata.labels && + object.metadata.labels['republic.rome/legacy'] == 'true' diff --git a/adventures/05-lex-imperfecta/intermediate/manifests/policies/aegyptus-require-scribe-role.yaml b/adventures/05-lex-imperfecta/intermediate/manifests/policies/aegyptus-require-scribe-role.yaml new file mode 100644 index 00000000..fa5ff3f5 --- /dev/null +++ b/adventures/05-lex-imperfecta/intermediate/manifests/policies/aegyptus-require-scribe-role.yaml @@ -0,0 +1,32 @@ +apiVersion: policies.kyverno.io/v1 +kind: ValidatingPolicy +metadata: + name: aegyptus-require-scribe-role + annotations: + policies.kyverno.io/title: Aegyptus — Require Scribe Role + policies.kyverno.io/category: Best Practices + policies.kyverno.io/severity: medium + policies.kyverno.io/subject: Pod, Label + policies.kyverno.io/description: >- + Aegyptus is the Republic's centre of scholarship — only scribe workloads + are permitted to operate here. All pods must carry the + 'republic.rome/role: scribe' label. This is a provincial law, enacted by + the magistrate of Aegyptus. +spec: + validationActions: + - Audit + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["pods"] + namespaceSelector: + matchLabels: + republic.rome/realm: province + validations: + - expression: >- + has(object.metadata.labels) && + 'republic.rome/role' in object.metadata.labels && + object.metadata.labels['republic.rome/role'] == 'scribe' + message: "Aegyptus permits only scribe workloads. Pods must carry the 'republic.rome/role: scribe' label to be admitted." diff --git a/adventures/05-lex-imperfecta/intermediate/manifests/policies/no-privileged-containers.yaml b/adventures/05-lex-imperfecta/intermediate/manifests/policies/no-privileged-containers.yaml new file mode 100644 index 00000000..3c1b3dd5 --- /dev/null +++ b/adventures/05-lex-imperfecta/intermediate/manifests/policies/no-privileged-containers.yaml @@ -0,0 +1,35 @@ +apiVersion: policies.kyverno.io/v1 +kind: ValidatingPolicy +metadata: + name: no-privileged-containers + annotations: + policies.kyverno.io/title: No Privileged Containers + policies.kyverno.io/category: Pod Security + policies.kyverno.io/severity: high + policies.kyverno.io/subject: Pod + policies.kyverno.io/description: >- + The Senate forbids unchecked power across all provinces. Running containers + with elevated privileges grants access to all Linux kernel capabilities and + host resources, bypassing namespace isolation. This law applies empire-wide + to all provinces; the infra realm is exempt. +spec: + validationActions: + - Deny + - Audit + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["pods"] + namespaceSelector: + matchLabels: + republic.rome/realm: province + validations: + - expression: |- + ( + object.spec.?containers.orValue([]) + + object.spec.?initContainers.orValue([]) + + object.spec.?ephemeralContainers.orValue([]) + ).all(c, !c.?securityContext.?privileged.orValue(false)) + message: "The Senate forbids unchecked power: privileged containers seize full control of the host and are not permitted in the provinces." diff --git a/adventures/05-lex-imperfecta/intermediate/manifests/policies/require-census.yaml b/adventures/05-lex-imperfecta/intermediate/manifests/policies/require-census.yaml new file mode 100644 index 00000000..461d521d --- /dev/null +++ b/adventures/05-lex-imperfecta/intermediate/manifests/policies/require-census.yaml @@ -0,0 +1,37 @@ +apiVersion: policies.kyverno.io/v1 +kind: ValidatingPolicy +metadata: + name: require-census + annotations: + policies.kyverno.io/title: Citizen Census + policies.kyverno.io/category: Best Practices + policies.kyverno.io/severity: medium + policies.kyverno.io/subject: Pod, Label + policies.kyverno.io/description: >- + Every citizen must declare their gens and their province. These are the + foundational registration requirements of the Republic, applied empire-wide + to all provinces. Unregistered or misdeclared workloads cannot be admitted. + Recognised legacy workloads may be granted a formal exception. +spec: + validationActions: + - Deny + - Audit + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["pods"] + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: ["gallia", "britannia", "aegyptus"] + validations: + - expression: "has(object.metadata.labels) && 'republic.rome/gens' in object.metadata.labels" + message: "All citizens must declare their gens. Unregistered workloads are not permitted in the Republic." + - expression: >- + has(object.metadata.labels) && + 'republic.rome/province' in object.metadata.labels && + object.metadata.labels['republic.rome/province'] == object.metadata.namespace + message: "A workload's declared province must match the namespace it is deployed in. Citizens cannot claim citizenship in a province they do not inhabit." diff --git a/adventures/05-lex-imperfecta/intermediate/manifests/workloads/aegyptus-legacy-scribe.yaml b/adventures/05-lex-imperfecta/intermediate/manifests/workloads/aegyptus-legacy-scribe.yaml new file mode 100644 index 00000000..c64ccfff --- /dev/null +++ b/adventures/05-lex-imperfecta/intermediate/manifests/workloads/aegyptus-legacy-scribe.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: legacy-scribe + namespace: aegyptus + labels: + # No republic.rome/gens — predates the requirement. Opts in to the legacy + # exemption (PolicyException aegyptus-legacy-workload) via this explicit marker. + republic.rome/legacy: "true" + republic.rome/province: aegyptus + republic.rome/role: scribe +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false diff --git a/adventures/05-lex-imperfecta/intermediate/manifests/workloads/castra-praetorian-monitor.yaml b/adventures/05-lex-imperfecta/intermediate/manifests/workloads/castra-praetorian-monitor.yaml new file mode 100644 index 00000000..937ee85e --- /dev/null +++ b/adventures/05-lex-imperfecta/intermediate/manifests/workloads/castra-praetorian-monitor.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: praetorian-monitor + namespace: castra + labels: + republic.rome/gens: praetoria +spec: + containers: + - name: monitor + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: true diff --git a/adventures/05-lex-imperfecta/intermediate/manifests/workloads/citizens.yaml b/adventures/05-lex-imperfecta/intermediate/manifests/workloads/citizens.yaml new file mode 100644 index 00000000..bdf9893a --- /dev/null +++ b/adventures/05-lex-imperfecta/intermediate/manifests/workloads/citizens.yaml @@ -0,0 +1,64 @@ +apiVersion: v1 +kind: Pod +metadata: + name: citizen + namespace: gallia + labels: + republic.rome/gens: iulia + republic.rome/province: gallia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false +--- +apiVersion: v1 +kind: Pod +metadata: + name: citizen + namespace: hispania + labels: + republic.rome/gens: cornelia + republic.rome/province: hispania +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false +--- +apiVersion: v1 +kind: Pod +metadata: + name: citizen + namespace: britannia + labels: + republic.rome/gens: claudia + republic.rome/province: britannia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false +--- +apiVersion: v1 +kind: Pod +metadata: + name: citizen + namespace: aegyptus + labels: + republic.rome/gens: aemilia + republic.rome/province: aegyptus + republic.rome/role: scribe +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false diff --git a/adventures/05-lex-imperfecta/intermediate/manifests/workloads/hispania-unregistered-scribe.yaml b/adventures/05-lex-imperfecta/intermediate/manifests/workloads/hispania-unregistered-scribe.yaml new file mode 100644 index 00000000..df30b0e2 --- /dev/null +++ b/adventures/05-lex-imperfecta/intermediate/manifests/workloads/hispania-unregistered-scribe.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: unregistered-scribe + namespace: hispania + labels: + # An unregistered workload (no republic.rome/gens) that is nonetheless + # running in hispania. It should never have been admitted here. + republic.rome/legacy: "true" + republic.rome/province: hispania + republic.rome/role: scribe +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false diff --git a/adventures/05-lex-imperfecta/intermediate/verify.sh b/adventures/05-lex-imperfecta/intermediate/verify.sh new file mode 100755 index 00000000..7eb03655 --- /dev/null +++ b/adventures/05-lex-imperfecta/intermediate/verify.sh @@ -0,0 +1,336 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Load shared libraries +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/../../../lib/scripts/loader.sh" + +set_tracking_context "lex-imperfecta" "intermediate" "05" "06" "2026" + +OBJECTIVE="By the end of this level, you should have: + +- The empire-wide laws enforcing correctly across every province — privileged containers are blocked, and every workload declares a valid gens and a province that matches the namespace it runs in +- Aegyptus's provincial scribe law taking effect only within Aegyptus — it admits only scribe workloads there, and has no power over the other provinces +- The legacy exception scoped to Aegyptus alone — only Aegyptus's grandfathered workload is spared the census, and no workload in any other province can claim it +- The Tabularium's ledger clean and on file — the estate's policy report exported in the OpenReports format and saved as \`estate-audit.yaml\`" + +DOCS_URL="https://offon.dev/adventures/lex-imperfecta/levels/intermediate" + +# The estate audit the player files with the Tabularium (player-generated, never shipped) +LEDGER_FILE="$SCRIPT_DIR/estate-audit.yaml" + +print_header \ + 'Challenge 05: Lex Imperfecta' \ + 'Level 02: Governing the Provinces' \ + 'Verification' + +# Init test counters +TESTS_PASSED=0 +TESTS_FAILED=0 +FAILED_CHECKS=() + +check_prerequisites kubectl jq + +print_sub_header "Running verification checks..." + +# ============================================================================= +# Objective 1: The empire-wide laws enforce correctly across every province +# ============================================================================= +print_new_line +print_sub_header "1. Checking the empire-wide laws across the provinces..." + +check_admission_blocked \ + "Workload with no gens in a province" \ + "Every citizen counted in the census must declare something about themselves — does this one?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-census-no-gens + namespace: gallia + labels: + republic.rome/province: gallia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_blocked \ + "Workload whose declared province does not match its namespace" \ + "The census cross-checks where a workload claims to belong against where it actually runs." <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-census-mismatch + namespace: gallia + labels: + republic.rome/gens: verus + republic.rome/province: hispania +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_allowed \ + "Fully-registered workload in a province" \ + "A compliant citizen should pass freely — make sure the census is not turning away the well-behaved." <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-census-compliant + namespace: gallia + labels: + republic.rome/gens: verus + republic.rome/province: gallia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_blocked \ + "Privileged workload in a province" \ + "What does the Senate forbid in the provinces, no matter who is asking?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-privileged + namespace: britannia + labels: + republic.rome/gens: verus + republic.rome/province: britannia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: true +EOF + +check_file_contains \ + "$SCRIPT_DIR/manifests/policies/require-census.yaml" \ + "republic.rome/realm" \ + "require-census scopes to provinces by label, not by hardcoded namespace names" \ + "The census should use the namespace labels to decide where it applies — what label do the province namespaces carry?" + +check_file_not_contains \ + "$SCRIPT_DIR/manifests/policies/require-census.yaml" \ + "kubernetes.io/metadata.name" \ + "require-census does not hardcode individual namespace names" \ + "Listing namespace names explicitly is fragile — what would happen if a new province were added?" + +check_admission_blocked \ + "Workload with no gens in Hispania" \ + "The census should reach every province — are there any it is not covering?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-census-hispania + namespace: hispania + labels: + republic.rome/province: hispania +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_allowed \ + "Privileged workload in the infra realm" \ + "The infra realm lies outside the provinces' jurisdiction — empire-wide laws should not reach into it." <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-privileged-infra + namespace: castra +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: true +EOF + +# ============================================================================= +# Objective 2: Aegyptus's scribe law takes effect only within Aegyptus +# ============================================================================= +print_new_line +print_sub_header "2. Checking the reach of Aegyptus's provincial law..." + +check_admission_blocked \ + "Non-scribe workload in Aegyptus" \ + "Aegyptus admits only one kind of workload — is its local law still in force there?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-aegyptus-non-scribe + namespace: aegyptus + labels: + republic.rome/gens: verus + republic.rome/province: aegyptus +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_allowed \ + "Scribe workload in Aegyptus" \ + "A proper Aegyptus scribe is fully in order — something may be turning it away that should not." <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-aegyptus-scribe + namespace: aegyptus + labels: + republic.rome/gens: verus + republic.rome/province: aegyptus + republic.rome/role: scribe +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_allowed \ + "Non-scribe workload in another province (Gallia)" \ + "A law enacted for Aegyptus should have no say over Gallia — does it reach further than it should?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-gallia-non-scribe + namespace: gallia + labels: + republic.rome/gens: verus + republic.rome/province: gallia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +# ============================================================================= +# Objective 3: The legacy exception is scoped to Aegyptus alone +# ============================================================================= +print_new_line +print_sub_header "3. Checking the reach of the legacy exception..." + +check_admission_allowed \ + "Grandfathered legacy workload in Aegyptus" \ + "Aegyptus's grandfathered scribes hold a Senate exception — is it honoured where it belongs?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-aegyptus-legacy + namespace: aegyptus + labels: + republic.rome/legacy: "true" + republic.rome/province: aegyptus + republic.rome/role: scribe +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_blocked \ + "Workload claiming legacy status in another province (Hispania)" \ + "An exception granted to Aegyptus's legacy scribes — should a workload in another province be able to invoke it?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-hispania-legacy + namespace: hispania + labels: + republic.rome/legacy: "true" + republic.rome/province: hispania + republic.rome/role: scribe +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_blocked \ + "Non-legacy workload missing its gens in Aegyptus" \ + "The exception is only for the grandfathered — does a workload that never declared itself legacy deserve it?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-aegyptus-non-legacy + namespace: aegyptus + labels: + republic.rome/province: aegyptus + republic.rome/role: scribe +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +# ============================================================================= +# Objective 4: The Tabularium's ledger is clean and on file +# ============================================================================= +print_new_line +print_sub_header "4. Checking that the estate audit has been filed..." + +check_file_exists \ + "$LEDGER_FILE" \ + "The Tabularium's ledger (estate-audit.yaml) is on file" \ + "The Senate needs the audit on file — export the estate's policy reports in the OpenReports format to estate-audit.yaml (see How to Play)." + +# ============================================================================= +# Summary & Next Steps +# ============================================================================= +failed_checks_json="[]" +if [[ -n "${FAILED_CHECKS[*]:-}" ]]; then + failed_checks_json=$(printf '%s\n' "${FAILED_CHECKS[@]}" | jq -R . | jq -s .) +fi + +if [[ $TESTS_FAILED -gt 0 ]]; then + track_verification_completed "failed" "$failed_checks_json" + print_verification_summary "lex-imperfecta" "$DOCS_URL" "$OBJECTIVE" + exit 1 +fi + +track_verification_completed "success" "$failed_checks_json" + +print_header "Test Results Summary" +print_success "✅ PASSED: All $TESTS_PASSED verification checks passed!" +print_new_line + +check_submission_readiness "05-lex-imperfecta" "intermediate" diff --git a/ideas/.implemented/lex-imperfecta.md b/ideas/.implemented/lex-imperfecta.md index 2d2128a4..04332372 100644 --- a/ideas/.implemented/lex-imperfecta.md +++ b/ideas/.implemented/lex-imperfecta.md @@ -13,7 +13,7 @@ chaos takes hold. - Manage and organize policies at scale across teams and environments - Respond to runtime threats that bypass static policies -**Technologies:** Kyverno, Falco, Policy Reporter, OpenReports, Argo CD, Kubernetes +**Technologies:** Kyverno, Falco, Policy Reporter, OpenReports, Kubernetes --- @@ -66,9 +66,9 @@ Fix a misconfigured Kyverno policy setup and use Policy Reporter and the OpenRep #### Story -The Republic has grown. What once was a single city is now a sprawling empire of provinces, each governed by different magistrates with different needs. The legal scholars decided to manage all policies through a central archive — a GitOps system that ensures every province's laws are version-controlled and auditable. +The Republic has grown. What once was a single city is now a sprawling empire of provinces, each governed by different magistrates with different needs. The legal scholars decided to catalogue every law in a central archive — the Tabularium — so that each province's statutes could be tracked and audited in one place. -But the migration introduced new chaos. Policies meant for one province are bleeding into another. Some provinces are ungoverned entirely. And the exceptions granted to certain citizens are... not quite right. +But cataloguing the laws introduced new chaos. Policies meant for one province are bleeding into another. Some provinces are ungoverned entirely. And the exceptions granted to certain citizens are... not quite right. Your mission: investigate the policy estate, fix the scoping issues, and ensure each province is governed by the right laws. @@ -93,8 +93,8 @@ By the end of this level, the learner should: #### Tools & Infrastructure -- **Tools:** `kubectl`, `argocd` CLI, `kyverno` CLI, `k9s` -- **Infrastructure:** Kubernetes Cluster, Kyverno, Argo CD, Policy Reporter +- **Tools:** `kubectl`, `kyverno` CLI, `k9s` +- **Infrastructure:** Kubernetes Cluster, Kyverno, Policy Reporter --- @@ -139,5 +139,5 @@ By the end of this level, the learner should: #### Tools & Infrastructure -- **Tools:** `kubectl`, `argocd` CLI, `kyverno` CLI, `falcoctl`, `k9s` -- **Infrastructure:** Kubernetes Cluster, Kyverno, Argo CD, Policy Reporter, Falco \ No newline at end of file +- **Tools:** `kubectl`, `kyverno` CLI, `falcoctl`, `k9s` +- **Infrastructure:** Kubernetes Cluster, Kyverno, Policy Reporter, Falco \ No newline at end of file diff --git a/lib/kubernetes/config.yaml b/lib/kubernetes/config.yaml index 22440ef6..48788908 100644 --- a/lib/kubernetes/config.yaml +++ b/lib/kubernetes/config.yaml @@ -39,3 +39,7 @@ nodes: - hostPort: 30109 containerPort: 30109 protocol: TCP + # Policy Reporter + - hostPort: 30110 + containerPort: 30110 + protocol: TCP diff --git a/lib/policy-reporter/init.sh b/lib/policy-reporter/init.sh new file mode 100755 index 00000000..b0e8fdee --- /dev/null +++ b/lib/policy-reporter/init.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -e + +help() { + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " --help Display this help message" + echo " --version Policy Reporter Helm chart version to install (required)" +} + +version="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --help) + help + exit 0 + ;; + --version) + if [[ -z "${2-}" ]]; then + echo "Error: --version requires a value" >&2 + exit 1 + fi + version="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +if [[ -z "$version" ]]; then + echo "Error: --version is required" >&2 + exit 1 +fi + +echo "✨ Installing Policy Reporter" +helm repo add policy-reporter https://kyverno.github.io/policy-reporter +helm repo update +helm install policy-reporter policy-reporter/policy-reporter \ + --namespace policy-reporter \ + --create-namespace \ + --version "$version" \ + --set ui.enabled=true \ + --set ui.service.type=NodePort \ + --wait + +echo "✨ Waiting for Policy Reporter to be ready" +kubectl rollout status deployment/policy-reporter -n policy-reporter --timeout=300s + +echo "✨ Pinning Policy Reporter UI to NodePort 30110" +# The chart template does not expose a nodePort value, so we patch after install +kubectl patch svc policy-reporter-ui -n policy-reporter \ + --type='json' \ + -p='[{"op":"replace","path":"/spec/ports/0/nodePort","value":30110}]' + +echo "✅ Policy Reporter is ready (UI: http://localhost:30110)" diff --git a/lib/scripts/filesystem.sh b/lib/scripts/filesystem.sh index 15aa3ff2..5af050ad 100644 --- a/lib/scripts/filesystem.sh +++ b/lib/scripts/filesystem.sh @@ -32,6 +32,51 @@ check_file_contains() { fi } +# ----------------------------------------------------------------------------- +# Check that a file exists and is non-empty (does NOT inspect contents) +# Usage: check_file_exists "file-path" "Display Name" "Hint message" +# ----------------------------------------------------------------------------- +check_file_exists() { + local file_path=$1 + local display_name=$2 + local hint=$3 + + print_test_section "Checking $display_name..." + + if [[ -s "$file_path" ]]; then + print_success_indent "$display_name" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + print_error_indent "$display_name - not found" + print_hint "$hint" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("check_file_exists:$file_path") + fi +} + +# ----------------------------------------------------------------------------- +# Check that a file does NOT contain a specific pattern +# Usage: check_file_not_contains "file-path" "pattern" "Display Name" "Hint message" +# ----------------------------------------------------------------------------- +check_file_not_contains() { + local file_path=$1 + local pattern=$2 + local display_name=$3 + local hint=$4 + + print_test_section "Checking $display_name..." + + if grep -q "$pattern" "$file_path" 2>/dev/null; then + print_error_indent "$display_name - found when it should not be" + print_hint "$hint" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("check_file_not_contains:$pattern") + else + print_success_indent "$display_name" + TESTS_PASSED=$((TESTS_PASSED + 1)) + fi +} + # ----------------------------------------------------------------------------- # Check if a file contains a specific pattern at least N times # Usage: check_file_contains_count "file-path" "pattern" "min-count" "Display Name" "Hint message" diff --git a/scripts/new-adventure.sh b/scripts/new-adventure.sh index cb3ffb51..b8499682 100755 --- a/scripts/new-adventure.sh +++ b/scripts/new-adventure.sh @@ -58,21 +58,31 @@ The entire **infrastructure is pre-provisioned in your Codespace** [Choose your level](https://offon.dev/adventures/$selected_slug/) and begin learning! EOF - cat > "$ADVENTURE_DIR/docs/index.md" << EOF -# $adventure_emoji Adventure 00: $adventure_name - - - -## đŸĒ The Backstory - -$adventure_theme - - - -## 🎮 Choose Your Level - -Each level is a standalone challenge with its own Codespace that builds on the story while being technically -independent — pick your level and start wherever you feel comfortable. + cat > "$ADVENTURE_DIR/docs/index.yaml" << EOF +slug: $selected_slug +name: "$adventure_name" +emoji: "$adventure_emoji" + +tags: + # TODO: list technologies as separate tag items + - TODO + +backstory: + - >- + $adventure_theme + # TODO: expand backstory if desired + +overview: + - >- + TODO: brief intro (mission + key technologies + pre-provisioned note) + +rewards: + deadline: "" # TODO: fill in once the adventure goes live + tiers: + - label: "1st place" + description: "TODO" + - label: "Top 3" + description: "TODO" EOF echo "✅ Adventure base created." @@ -88,132 +98,69 @@ level_objective=$(extract_level_section "$selected_file" "$selected_level" "Obje level_learnings=$(extract_level_section "$selected_file" "$selected_level" "What You'll Learn") level_tools=$(extract_level_section "$selected_file" "$selected_level" "Tools & Infrastructure") -LEVEL_DOC="$ADVENTURE_DIR/docs/$level_slug.md" +LEVEL_DOC="$ADVENTURE_DIR/docs/$level_slug.yaml" if [[ ! -f "$LEVEL_DOC" ]]; then - echo "Creating level doc at docs/$level_slug.md ..." - TICK='```' + echo "Creating level doc at docs/$level_slug.yaml ..." cat > "$LEVEL_DOC" << EOF -# $level_emoji $level_difficulty: $level_name -$level_story - - - -đŸ—ī¸ Architecture - - - -## đŸŽ¯ Objective -$level_objective - -## 🧠 What You'll Learn -$level_learnings - -## 🧰 Toolbox - -Your Codespace comes pre-configured with the following tools: -$level_tools - - - -## ⏰ Deadline - - - -> â„šī¸ You can still complete the challenge after this date, but points will only be awarded for submissions before the -> deadline. - -## đŸ’Ŧ Join the discussion - - -Share your solutions and questions in -the [challenge thread](TODO) -in the Open Ecosystem Community. - -## ✅ How to Play - - - -### 1. Start Your Challenge - -> 📖 **First time?** Check out the [Getting Started Guide](../../start-a-challenge) for detailed instructions on -> forking, starting a Codespace, and waiting for infrastructure setup. - -Quick start: - -- Fork the [repo](https://github.com/dynatrace-oss/open-ecosystem-challenges/) -- Create a Codespace -- Select "$adventure_emoji Adventure 00 | $level_emoji $level_difficulty ($level_name)" -- Wait a couple of minutes for the environment to initialize (\`Cmd/Ctrl + Shift + P\` → \`View Creation Log\` to view progress) - - - -### 2. Access the UIs - -- Open the **Ports** tab in the bottom panel to access the following UIs - -#### Some UI you might use - - - - -- Find the tool row (port NN) and click the forwarded address - -### 3. Implement the Objective - - - -Review the [đŸŽ¯ Objective](#objective) section to understand what a successful solution looks like. - -#### Where to Look - - - -#### How to Run - - - -#### Helpful Documentation - - - -### 4. Verify Your Solution - -Once you think you've solved the challenge, run the verification script: - -${TICK}bash -./verify.sh -${TICK} - -**If the verification fails:** - -The script will tell you which checks failed. Fix the issues and run it again. - -**If the verification passes:** - -1. The script will check if your changes are committed and pushed. -2. Follow the on-screen instructions to commit your changes if needed. -3. Once everything is ready, the script will generate a **Certificate of Completion**. - -4. **Copy this certificate** and paste it into - the [challenge thread](TODO) - to claim your victory! 🏆 -EOF - - cat >> "$ADVENTURE_DIR/docs/index.md" << EOF - -### $level_emoji $level_difficulty: $level_name - -- **Status:** 🚧 Coming Soon -- **Topics:** $adventure_technologies - -$level_summary - -[**Start the $level_difficulty Challenge**](./$level_slug.md) +level: $level_slug +emoji: "$level_emoji" +title: "$level_name" +devcontainer: ${selected_slug}_${level_slug} +community_url: "" # TODO: add community thread URL once the adventure is live + +summary: "$level_summary" + +audience: >- + TODO: describe who this level is for + +backstory: + - >- + $level_story + # TODO: expand backstory if desired + +objective: + - >- + TODO: first objective + +what_you_learn: + - >- + TODO: first learning (link to relevant docs) + +architecture: + - >- + TODO: describe the overall setup + - >- + TODO: describe what the player edits vs. leaves alone + +architecture_diagram: "" # TODO: add diagram filename (e.g. $selected_slug-$level_slug.svg) + +toolbox: + - name: "TODO" + url: "TODO" + description: "TODO: describe what this tool is used for" + +services: [] + +how_to_play: + - id: explore + title: "Explore the Setup" + content: | + TODO: describe how players can explore the initial state + + - id: implement + title: "Implement the Solution" + content: | + TODO: describe how players implement the solution + +helpful_links: + - title: "TODO" + url: "TODO" + description: "TODO: describe what this link is useful for" EOF - echo "✅ Level doc created and level card added to index.md." + echo "✅ Level doc created." else echo "â„šī¸ Level doc already exists, skipping." fi @@ -235,6 +182,10 @@ SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 source "\$SCRIPT_DIR/../../../../lib/scripts/loader.sh" +set_tracking_context "$selected_slug" "$level_slug" "00" "TODO" "TODO" + +OBJECTIVE="$level_objective" + DOCS_URL="https://offon.dev/adventures/$selected_slug/levels/$level_slug" print_header \\ @@ -268,7 +219,7 @@ fi if [[ \$TESTS_FAILED -gt 0 ]]; then track_verification_completed "failed" "\$failed_checks_json" - print_verification_summary "$selected_slug" "\$DOCS_URL" + print_verification_summary "$selected_slug" "\$DOCS_URL" "\$OBJECTIVE" exit 1 fi @@ -334,8 +285,8 @@ REPO_ROOT="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")/../.." && pwd)" # shellcheck disable=SC1091 source "\$REPO_ROOT/lib/scripts/tracker.sh" -set_tracking_context "$selected_slug" "$level_slug" -track_codespace_created +set_tracking_context "$selected_slug" "$level_slug" "00" "TODO" "TODO" +track_container_created "\$REPO_ROOT/lib/shared/init.sh" --version v0.17.0 @@ -365,8 +316,8 @@ echo "✨ Starting $adventure_name - $level_difficulty Level" # shellcheck disable=SC1091 source "\$REPO_ROOT/lib/scripts/tracker.sh" -set_tracking_context "$selected_slug" "$level_slug" -track_codespace_initialized +set_tracking_context "$selected_slug" "$level_slug" "00" "TODO" "TODO" +track_container_initialized EOF chmod +x "$DEVCONTAINER_DIR/post-create.sh" "$DEVCONTAINER_DIR/post-start.sh"