Kubeasy LogoKubeasy

Challenge Structure

The complete anatomy of a Kubeasy challenge — every file, every field, and how they work together.

Every Kubeasy challenge is a folder in the challenges repository following a consistent structure. This page is the reference for everything a challenge can contain.

Directory layout

challenge-slug/
├── challenge.yaml      # Metadata, description, and validation objectives
├── manifests/          # Kubernetes resources (the initial state)
│   ├── deployment.yaml
│   ├── service.yaml
│   └── ...
├── policies/           # Kyverno policies (bypass prevention)
│   └── protect.yaml
└── image/              # Optional: custom Docker image
    └── Dockerfile

1. challenge.yaml

The challenge.yaml file is the single source of truth for a challenge. It defines everything: metadata, the narrative description, and the validation objectives.

Metadata fields

Prop

Type

Challenge types

The type field defines the nature of the problem:

TypeStarting stateWhat the user does
fixBroken setupInvestigate and repair
buildEmpty or minimalCreate the required resources
migrateWorking but outdatedTransform to a new pattern

See Challenge Types for a detailed explanation of each.

Writing good descriptions

description — describe symptoms, not causes. The user should not know what's wrong before they investigate.

# BAD: reveals the root cause
description: |
  The memory limit is too low for this workload. Increase it to fix the crash.

# GOOD: describes symptoms
description: |
  A data processing service keeps restarting. The team says it worked fine last week.
  No code changes were made — something else must be wrong.

initialSituation — describe what the user will find when they start, not why it's happening.

objective — state the goal, not the method.

# BAD: tells the user what to do
objective: |
  Increase the memory limit to at least 256Mi.

# GOOD: describes the desired outcome
objective: |
  Make the pod run stably without being killed by Kubernetes.

Full example

title: Memory Pressure
description: |
  A data processing service keeps restarting unexpectedly.
  The team says the workload hasn't changed, but something is killing the pod.
theme: resources-scaling
difficulty: easy
type: fix
estimatedTime: 15
initialSituation: |
  A data processing pod is deployed in the cluster.
  It starts successfully but crashes within a few seconds.
  The pod enters CrashLoopBackOff state and keeps restarting.
objective: |
  Make the application run stably without being killed.

objectives:
  - key: pod-ready
    title: "Pod Ready"
    description: "The pod must be running and ready"
    order: 1
    type: condition
    spec:
      target:
        kind: Pod
        labelSelector:
          app: data-processor
      checks:
        - type: Ready
          status: "True"

  - key: stable-operation
    title: "Stable Operation"
    description: "No crash or eviction events in the past 5 minutes"
    order: 2
    type: event
    spec:
      target:
        kind: Pod
        labelSelector:
          app: data-processor
      forbiddenReasons:
        - "OOMKilled"
        - "Evicted"
      sinceSeconds: 300

2. manifests/ directory

Contains the Kubernetes resources that are deployed when a user starts the challenge. These define the initial state — typically broken, minimal, or outdated depending on the challenge type.

Design principles:

  • Minimal — only include what's relevant to the challenge
  • Realistic — mirror real production problems, not artificial puzzles
  • Self-contained — don't reference external resources or services
# manifests/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: data-processor
spec:
  replicas: 1
  selector:
    matchLabels:
      app: data-processor
  template:
    metadata:
      labels:
        app: data-processor
    spec:
      containers:
        - name: processor
          image: python:3.11-slim
          resources:
            limits:
              memory: "32Mi"  # BUG: too low for this workload
              cpu: "100m"

3. policies/ directory

Contains Kyverno policies that prevent bypasses — stopping users from taking shortcuts that avoid solving the actual problem.

Policies use kind: Policy (namespace-scoped) with metadata.namespace set to the challenge slug. A namespace-scoped Policy enforces only within its own namespace, so it never affects the rest of the cluster.

What to protect:

  • Container images (prevent swapping the broken app for a working one)
  • Critical volume configurations
  • Labels used by validation (so resources remain findable)

What NOT to protect:

  • Resource limits/requests (the user needs to change these)
  • Environment variables
  • Probe configurations
  • Most annotations
# policies/protect.yaml
apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: protect-data-processor
  namespace: pod-evicted   # must match the challenge slug (the namespace name)
spec:
  validationFailureAction: Enforce
  rules:
    - name: preserve-image
      match:
        resources:
          kinds: ["Deployment"]
          names: ["data-processor"]
      validate:
        message: "Cannot change the container image"
        pattern:
          spec:
            template:
              spec:
                containers:
                  - name: processor
                    image: "python:3.11-slim"

Use kind: Policy (namespace-scoped), not kind: ClusterPolicy. Set metadata.namespace to the challenge slug — this is the namespace where the challenge is deployed. A namespace-scoped Policy automatically enforces only within its own namespace.

4. image/ directory (optional)

If your challenge requires custom application behavior that a standard image can't provide, add an image/ directory with a Dockerfile.

my-challenge/
├── challenge.yaml
├── manifests/
├── policies/
└── image/
    ├── Dockerfile
    └── app.py

Automatic CI build

When your challenge is merged to main, CI automatically:

  1. Detects challenges with an image/ directory
  2. Builds a multi-arch image (linux/amd64, linux/arm64)
  3. Pushes to ghcr.io/kubeasy-dev/<challenge-slug>:latest

Reference it in your manifests:

containers:
  - name: app
    image: ghcr.io/kubeasy-dev/my-challenge:latest

Local development

During local development with kubeasy dev apply, the CLI automatically builds the image from image/ and loads it into the Kind cluster — no manual docker build or kind load needed.

Best practices

  • Use slim or alpine base images to keep sizes small
  • Keep the Dockerfile simple and deterministic
  • Don't include secrets or credentials
  • Add a .dockerignore to exclude unnecessary files

Themes

The theme field groups challenges in the UI:

ThemeDescription
rbac-securityPermissions, roles, security contexts
networkingServices, Ingress, NetworkPolicies
volumes-secretsStorage, ConfigMaps, Secrets
resources-scalingLimits, requests, HPA, scaling
monitoring-debuggingProbes, logging, events

How it all fits together

  1. User runs kubeasy challenge start <slug> → CLI creates a namespace and deploys manifests/ and policies/
  2. User investigates and fixes the problem using kubectl
  3. User runs kubeasy challenge submit <slug> → CLI runs the objectives from challenge.yaml against the cluster
  4. If all objectives pass → XP awarded, challenge marked complete

Next steps

On this page