Skip to content
↗ Astrona.io Cloud Native learning, open to everyone.

Authoring a Lab

A lab is a directory with a config.yaml (or whatever --file points at) plus whatever scripts/manifests/docs it references. This walks through building one from scratch, using examples/k8s-basics-01 as the worked reference.

Scaffolding New Content (For Teachers & Authors)

To make content creation fast, consistent, and collaborative, Astrona provides a built-in blueprint scaffolding command. By default, it dynamically pulls the template blueprint from a remote git repository based on the required <type> argument and replaces placeholders in filenames and contents with your customized arguments.

astrona content init <type> [path] [flags]
  • <type> (Required): Must be either ats (Astrona Training Series - hands-on sandbox lab) or atp (Astrona Training Path - course path/section).
  • [path] (Optional): The target directory to initialize. If omitted, this defaults to the value of the --slug flag (creating a directory named after your slug).

Dynamic Blueprint Defaults

Depending on the content type specified, the tool clones a dedicated template repository if no custom --repo is defined: - atp: Clones git@github.com:astrona-io/training-path-blueprint.git - ats: Clones git@github.com:astrona-io/training-sandbox-blueprint.git

Custom Scaffolding Arguments

You can supply the following customization arguments via command-line flags to template the newly initialized content:

Flag Variable Default Description
-i, --path-id path_id ATPxxx Unique identifier of the training path/lab (e.g., ATP010)
-s, --slug slug path-slug URL-safe and directory-friendly slug name of the path (becomes default folder name)
-t, --title title Path Title Human-readable title of the path
-d, --description description (engaging summary) A short summary of what the learner will achieve
-v, --version version 0.1.0 Initial version of the training content
-g, --language language en Learning language (e.g., en, fr, de)
-a, --author-name author_name Author Name Name of the author/teacher creating the content
-l, --license license Apache-2.0 Content license (default is Apache 2.0 license)
-r, --repo repo "" Custom template repository to clone from (overrides default blueprints)

Example Scaffolding Commands

  1. Initialize an ATP (Training Path) into a custom directory:

    astrona content init atp labs/my-custom-path \
      --path-id "ATP040" \
      --slug "linux-networking-basics" \
      --title "Linux Networking Basics" \
      --author-name "Alice Smith"
    

  2. Initialize an ATS (Lab Sandbox) defaulting to a folder named after your slug:

    astrona content init ats \
      --path-id "ATS012" \
      --slug "virtio-disk-discovery" \
      --title "Virtio Disk Discovery Lab" \
      --author-name "Bob Jones"
    
    This will automatically clone the sandbox template and create a new directory ./virtio-disk-discovery/.

Validating Content (For Teachers & Authors)

Once a Training Path (atp) has been scaffolded and filled in, validate its path.yaml before publishing:

astrona content validate <type> <source> [flags]
  • <type> (Required): ats (a training repository, its astrona.yaml) or atp (a training path, its path.yaml).
  • <source> (Required): Either a local folder path (the raw content files, containing path.yaml) or a git repository URL (https://, git@host:, ssh://, or a .git suffix). Use --git-ref to pin a git source to a branch or tag.

Validation:

  1. Confirms path.yaml exists at <source> and parses as YAML.
  2. Checks apiVersion is content.astrona.io/v1alpha1 and kind is TrainingPath.
  3. Walks every spec.stages[].content[] entry and resolves its repository at version (a git branch or tag) via the same git cache used by --git elsewhere in this CLI — this both verifies you have access to the referenced content repo and warms the local cache for later steps.
astrona content validate atp ./paths/my-custom-path
astrona content validate atp https://github.com/astrona-io/kubernetes-networking-atp.git --git-ref v1.2.0

Validating a training repository (ATS)

astrona content validate ats ./ATS014
astrona content validate ats https://github.com/astrona-io/ATS014.git --git-ref main

Checks astrona.yaml the way astrona.io's content sync reads it, and prints the totals (ATS014: 146 readings, 56 labs, 0 playgrounds, 20 quizzes (174 questions)). It fails (✗) on what would keep the training or an item off the site, and warns (!) about what is worth a look:

  • training.id is set; every item has a title, a known type — reading, lab, playground or quiz — and a path inside the repository that exists: a Markdown file for a reading, a directory with a config.yaml for a lab or playground, a YAML question bank for a quiz.
  • A quiz (knowledge check) bank has kind: question_bank and schema_version: 1, and each question an unique id, a prompt, 2–6 options with unique ids and a text, a correct_option that is one of them, and type: single_choice (or none). Its source_readings must exist; one that is not a reading in astrona.yaml gets no "read it again" link (warning). The site uses at most 30 questions. Fields, an example and the JSON Schema: Writing a Question Bank.
  • The quiz item's pass_score is a percentage above 0 and at most 100 (the site's default is 70); a question_count that differs from the bank is a warning — the site counts the bank.
  • A bank with status: draft, an option or question without an explanation, and a quiz.yaml the manifest does not list are warnings.

A knowledge check looks like this in astrona.yaml, right after the module's summary:

      - type: quiz
        title: "Knowledge Check: How A Request Moves Through The Mesh"
        path: sections/section-000/module-01/quiz.yaml
        pass_score: 70

path.yaml shape (ATP)

apiVersion: content.astrona.io/v1alpha1
kind: TrainingPath
metadata:
  id: ATP001
  slug: linux-foundation-certified-sysadmin-lfcs
  title: "Linux Foundation Certified System Administrator (LFCS)"
  version: "1.0.0"
  authors:
    - name: "Alice Smith"
      email: "alice@example.com"
spec:
  stages:
    - id: "ATP001-STG001"
      title: "Operations Deployment"
      weight: 25
      content:
        - ref: ATS002
          repository: "git@github.com:astrona-io/ATS002.git"
          path: "."
          version: "1.0.0"

Each stages[].content[] entry references an external content repository (typically an ATS lab): repository is the git URL, version the branch/tag to check out, and path the subdirectory within it.

Because a built bundle lays content out as <output>/<stage-id>/<ref>, and path.yaml often comes from a cloned repo, validate and build reject, before fetching anything:

  • a stage id or content ref that isn't a single name of letters, digits, ., _ and - (no /, no .., not empty);
  • an absolute path, or one that escapes the content repository (../…).

A repository or version starting with - is also rejected before git runs.

Building Content (For Teachers & Authors)

Once a Training Path validates cleanly, materialize it into a self-contained bundle — every referenced ATS repo resolved and copied locally, ready to ship or serve:

astrona content build <type> <source> [flags]
  • <type> (Required): Currently only atp is implemented, same as validate.
  • <source> (Required): Same as validate — a local folder path or a git repository URL, optionally pinned with --git-ref.
  • -o, --output: Output directory for the bundle (default ./dist).
  • --clean: Remove the output directory first if it already exists and is not empty. Without it, build refuses to write into a non-empty output directory.

Build:

  1. Runs the same path.yaml checks as validate.
  2. Resolves every spec.stages[].content[] entry's repository at version via the git cache (cloning/pulling as needed).
  3. Copies the resolved content (or its path subdirectory, if set) into <output>/<stage-id>/<ref>, skipping .git.
  4. Copies path.yaml itself to the bundle root.
astrona content build atp ./paths/my-custom-path --output ./dist
astrona content build atp https://github.com/astrona-io/kubernetes-networking-atp.git --git-ref v1.2.0 --output ./dist --clean

Starting from a working lab

astrona init lab ./k8s-web-01

Creates a complete kind lab you can run immediately and reshape into yours: config.yaml (editor schema, preloaded image, readiness gates, jsonpath / count / resourceExists checks with hints and points, commented addon and exam options), the four student docs, a bootstrap manifest, a reference solution in solution/ for astrona test, and .github/workflows/lab.yml running astrona validate + astrona test on every pull request. As generated it passes validate and test, and its step-by-step guide solves it. The sections below explain each part.

1. Metadata

metadata:
  name: "k8s-basics-01"
  docs:
    prerequisites: "docs/prerequisites.md"    # knowledge/tooling needed before attempting
    question: "docs/exam-question.md"     # formal, self-contained task statement
    caseStudy: "docs/case-study.md"           # softer, hint-driven version of the same task
    solution: "docs/step-by-step-guide.md"       # full walkthrough with the answer

metadata.name becomes the cluster/VM name (prefixed astro- by astrona). metadata.docs are Markdown files, relative to config.yaml, that students read with astrona docs. The block is optional: a doc it doesn't list is found by its usual file name next to config.yaml — question.md, solution.md, prerequisites.md, case-study.md — so this is enough:

labs/lab-01/
  config.yaml      # metadata: { name: "ats-015-lab-010-01" } — no docs block needed
  question.md
  solution.md
astrona docs                 # lists what the lab provides
astrona docs question        # docs.question (question.md) — the task
astrona docs case-study      # docs.caseStudy (case-study.md)
astrona docs prerequisites   # docs.prerequisites (prerequisites.md)
astrona docs solution        # docs.solution (solution.md) — shown with a "full solution" warning

They're rendered for the terminal (headings, code blocks, lists, inline code, bold, links) and paged; astrona run ends with "Your task: astrona docs question" when the lab has a question. Write them as plain Markdown — tables and HTML are shown as-is. A YAML front-matter block at the very top of a doc (a --- line, YAML such as estimated_duration: 20m, a --- line) is hidden; if it has a title, that's shown as the doc's heading. A --- further down is still a horizontal rule. Doc paths can't point outside the lab directory, and control characters (terminal escape sequences) are stripped before anything is printed. Name the cluster context as kind-astro-<metadata.name> in your docs — or point students at astrona shell <lab>.

Set an expected working time in the question document's front matter to show the UI's lab progress bar:

---
title: Configure a deployment
estimated_duration: 20m
---

Create a deployment with three replicas.

Use a positive duration with units, such as 45m or 1h30m. The UI compares this estimate with active time spent and pauses it during idle periods. Exceeding it does not stop the lab or change its grade. Missing or invalid estimates show as unavailable. This display applies to labs, not playgrounds; exam deadlines and playground expiry remain separate.

2. Pick a runtime

Omit runtime: entirely for a kind cluster (the default), or see Runtimes for a qemu VM-backed lab.

3. Bootstrap — what the student starts with

bootstrap:
  init:
    - name: "echo"
      type: "file"
      source: "hello.sh"
  manifests: []

Keep this to whatever scaffolding the exercise needs before the student's own work — the actual task (creating the namespace, deploying the app, whatever it is) should not be pre-built here.

4. Testing — your reference solution

testing:
  manifests:
    - name: "solution"
      type: "folder"
      source: "solution"

This only runs under astrona test, never for a student. Put the finished, correct solution here — the thing a student would produce if they solved the lab perfectly — so astrona test can prove your validation block actually passes against it.

5. Validation — how it's graded

validation:
  checks:
    - name: "lab-ns namespace exists"
      type: "resourceExists"
      resource: "namespace/lab-ns"
    - name: "hello-config configmap exists"
      type: "resourceExists"
      resource: "configmap/hello-config -n lab-ns"
  script:
    name: "verify-configmap-content"
    description: "Custom script check: confirms the configmap's actual content, not just that it exists"
    type: "file"
    source: "validate.sh"

Start with resourceExists/podReady checks for "does the thing exist," and reach for a script when you need to verify actual content or behavior (see Grading for the full check-type reference). A validation script exits 0 for pass, non-zero for fail — write it the same way you'd write a test assertion.

6. Teardown

teardown:
  init:
    - name: "dump-logs"
      type: "file"
      source: "teardown/dump-logs.sh"
  keepCluster: false

Usually just keepCluster: false (or omitted — that's the default). Add init scripts only if you need to capture state before the cluster disappears.

Files the student needs: resources/

Instead of asking students to paste a script or a YAML file from question.md, put it in a resources/ folder next to config.yaml. When the lab starts, astrona copies it to ~/.astrona/resources/<lab>/ and lists what's there — nothing runs until the student asks for it:

sections/section-010/module-01/labs/lab-01/
  config.yaml  question.md  solution.md
  bootstrap/  solution/  validation/
  resources/
    setup-db.sh               # runs with bash
    broken-deployment.yaml    # applied to the lab with kubectl

No config is needed; a resources: list adds descriptions and run commands — see resources.

7. Prove it works

astrona validate -c path/to/your-lab     # seconds: typos, rules, missing files
astrona test -c path/to/your-lab --junit-xml=report.xml

astrona validate catches config mistakes — a misspelled field, a missing script — without creating anything; see editor support and validation for autocompletion in your editor.

Before publishing, check the lab isn't flaky:

astrona test -c path/to/your-lab --repeat 5
Repeat summary — 5 runs:

  ✓  Deployment web runs 2 replicas  5/5
  ⚠  2 web pods exist                3/5  flaky

--repeat N (up to 20) runs the whole lifecycle N times on fresh environments. A check that passes only sometimes almost always means grading raced the cluster — add a waitFor (in testing for the reference solution) for whatever the check depends on. Any failed or flaky run fails the command; with --junit-xml, each check is reported once with "passed k/N runs".

This is the whole point of the testing stage: it bootstraps your lab, applies your reference solution, submits it to the Proctor, and tears down — proving a student who does everything right will actually pass. Run this locally before publishing, and wire it into CI (see CI Integration) so a later edit to validation can't silently break the lab's own solution.

Full schema reference

See Lab Config Schema for every field, type, and default.