Creating Labs

Scripting System

Learn how to write setup, check, solve, and cleanup scripts for POV Demo labs.

Script Types

Each task has four script types that run at different points in the task lifecycle.

TypeWhen It RunsPurpose
setupBefore the learner starts the taskConfigure the VM, install tools, create initial state
checkWhen the learner clicks "Check"Validate the learner's work; exit 0 = pass, non-zero = fail
solveWhen the learner clicks "Solve"Apply the correct solution idempotently
cleanupAfter the learner completes the taskRemove temporary state created by setup
Lab-level scripts in lab_scripts/ (named setup-{vm} and cleanup-{vm}) run once at provision/teardown time, not per-task.

Naming Convention

Scripts are named {operation}-{vm-name}. The VM name must match the name field in config.yml.

Script filenameVM name in config.yml
setup-ubuntu-1ubuntu-1
check-nomad-server-1nomad-server-1
solve-kubernetes-control-1kubernetes-control-1

Setup Scripts

Setup scripts configure the environment before the learner begins. Use #!/bin/bash -l to load the login shell profile.

#!/bin/bash -l
set -e

# Install required tools
apt-get update -q
apt-get install -y -q curl wget jq

# Create initial directory structure
mkdir -p /home/user/workspace
chown user:user /home/user/workspace

# Pre-populate a config file the learner will modify
cat > /home/user/workspace/config.yaml <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-config
data: {}
EOF

# Add setup commands to bash history so learner can reference them
history -s "kubectl apply -f config.yaml"
  • Use #!/bin/bash -l (login shell) to ensure the PATH includes all installed tools
  • Use set -e so the script fails fast on errors
  • Make scripts idempotent — they may run multiple times
  • Use history -s "command" to pre-populate terminal history (NOT set -o history)

Check Scripts

Check scripts validate the learner's work. They must exit 0 on success and non-zero on failure. Use the provided check_template.sh helper.

#!/bin/bash -l
source /etc/profile.d/check_functions.sh

# Check that kubectl is configured correctly
check_command "kubectl cluster-info" "kubectl cluster-info should succeed"

# Check that the deployment exists
check_command "kubectl get deployment my-app -n default" \
  "Deployment 'my-app' should exist in the default namespace"

# Check specific content in a file
check_file_content "/home/user/workspace/config.yaml" \
  "name: my-config" \
  "ConfigMap name should be 'my-config'"

echo "All checks passed!"

Solve Scripts

Warning: Solve scripts MUST have set -e on line 2. This is the most common lab validation failure.
#!/bin/bash -l
set -e

# Apply the solution
kubectl apply -f /home/user/workspace/solution.yaml

# Verify the solution worked
kubectl wait --for=condition=available deployment/my-app --timeout=60s
  • Must be idempotent — running it twice must not break anything
  • Use history -s "command" to show the solution commands in terminal history (NOT set -o history)

Helper Functions

Source /etc/profile.d/check_functions.sh in check scripts to access these helper functions.

FunctionSignatureDescription
check_commandcheck_command "cmd" "message"Runs cmd; fails with message if exit code is non-zero.
check_file_contentcheck_file_content "file" "pattern" "message"Fails if pattern is not found in file.
failfail "message"Immediately fails the check with message.
ensure_history_flushedensure_history_flushedCall at end of setup scripts to flush history to disk.

AI Gateway

Each lab session automatically has access to an AI gateway for building AI-powered lab scenarios.

VariableValue
AI_GATEWAY_URLInjected at provision time
AI_GATEWAY_TOKENSession-scoped bearer token
AI_MODELDefault model (e.g. claude-3-5-sonnet)

Usage limits

  • $5 per session credit limit
  • 20 requests per minute rate limit
  • Available inside lab VM scripts and task terminals
# Call AI from a lab script
response=$(curl -s -X POST "$AI_GATEWAY_URL/v1/messages" \
  -H "Authorization: Bearer $AI_GATEWAY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "'"$AI_MODEL"'",
    "max_tokens": 500,
    "messages": [{"role": "user", "content": "Explain this error: ..."}]
  }')
echo "$response" | jq -r '.content[0].text'