Compare commits
5
Commits
bd0a10fbfb
...
b6516a74db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6516a74db | ||
|
|
f5379afd5c | ||
|
|
15b616274b | ||
|
|
87526bcf7f | ||
|
|
3c815cfeea |
+19
@@ -42,6 +42,25 @@ RUN set -eux && \
|
||||
ln -sf /usr/local/bin/k3s /usr/local/bin/kubectl && \
|
||||
ln -sf /usr/local/bin/k3s /usr/local/bin/crictl
|
||||
|
||||
# ── Docker Engine (ROOT 04) + DNS tooling (ROOT 02) ──────────────────────────
|
||||
# A second, independent container runtime nested inside this already-Privileged Lab container
|
||||
# (see docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §3 for the security/resource evaluation and the
|
||||
# real test results this decision is based on) — its own dockerd, its own
|
||||
# /var/run/docker.sock, entirely separate from the Lab Host's real Docker socket, which is
|
||||
# never mounted into this image. docker-compose v2 isn't bundled by Ubuntu's docker.io package,
|
||||
# so fetch the static binary the same way k3s's own binary is fetched above, rather than adding
|
||||
# a whole new apt repository for one plugin.
|
||||
RUN apt-get update && apt-get install -y docker.io dnsutils && rm -rf /var/lib/apt/lists/*
|
||||
RUN set -eux && \
|
||||
case "$(uname -m)" in \
|
||||
x86_64) COMPOSE_ARCH="x86_64" ;; \
|
||||
aarch64) COMPOSE_ARCH="aarch64" ;; \
|
||||
*) echo "Unsupported arch: $(uname -m)" && exit 1 ;; \
|
||||
esac && \
|
||||
mkdir -p /usr/local/lib/docker/cli-plugins && \
|
||||
curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-${COMPOSE_ARCH}" \
|
||||
-o /usr/local/lib/docker/cli-plugins/docker-compose && \
|
||||
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
||||
|
||||
# ── App files ─────────────────────────────────────────────────────────────────
|
||||
WORKDIR /app
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# How Computers Run Software
|
||||
|
||||
## Hardware, OS, and application: three different layers
|
||||
|
||||
When you run a program, three distinct layers cooperate:
|
||||
|
||||
- **Hardware** — the physical CPU, RAM, and storage. It has no idea what a "file" or a "process"
|
||||
is; it just executes instructions and stores bits.
|
||||
- **The operating system (kernel)** — the layer that turns raw hardware into something usable:
|
||||
it decides which program gets the CPU next, hands out memory, and organizes storage into
|
||||
files and directories. Linux is a kernel plus a large set of standard tools around it.
|
||||
- **Applications (userspace)** — the programs you actually run. They ask the kernel for
|
||||
resources (memory, files, network) through a well-defined interface (system calls) instead of
|
||||
touching hardware directly.
|
||||
|
||||
This module gives you just enough of this model to reason about what you're actually looking at
|
||||
for the rest of ROOT_ — not a full computer architecture course.
|
||||
|
||||
## CPU: the thing that actually executes instructions
|
||||
|
||||
A CPU has some number of **cores** — each core can run one stream of instructions at a time.
|
||||
`nproc` tells you how many cores/threads Linux believes it has available. When a system feels
|
||||
"slow," it's very often because more processes want CPU time than there are cores to give them.
|
||||
|
||||
## RAM: fast, temporary, and finite
|
||||
|
||||
RAM holds the data and code a running program is actively using. It's fast but **volatile** —
|
||||
its contents disappear when power is lost, which is exactly why persistent data lives on
|
||||
storage instead. `free -h` shows you total memory, how much is in use, and how much is free.
|
||||
|
||||
## Storage: slower, but persistent
|
||||
|
||||
Storage (an SSD/disk) keeps data even when the machine is off. It's organized into a
|
||||
**filesystem** — the tree of directories and files you already have some intuition for. `df -h`
|
||||
shows how much space exists and how much is used on each mounted filesystem.
|
||||
|
||||
## Processes: a running program
|
||||
|
||||
A **process** is a program actually being executed — code plus its own private memory, tracked
|
||||
by the kernel with a unique ID (a PID). One program can have many processes running at once
|
||||
(open two terminals running the same shell, and you have two separate processes). ROOT 01
|
||||
Module 5 goes much deeper into this; for now, just know: everything you run becomes a process
|
||||
the kernel is responsible for scheduling onto a CPU core.
|
||||
|
||||
## Why this matters for everything after this module
|
||||
|
||||
Every concept the rest of ROOT_ builds on is a refinement of this same idea. A container is
|
||||
(mostly) an isolated process with its own filesystem view. A Kubernetes Pod is a group of
|
||||
containers the scheduler places onto a specific machine's CPU/RAM. None of that will make sense
|
||||
as anything other than magic if you don't first have this base model of what a computer is
|
||||
actually managing.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Give students just enough of a mental model to stop treating "the computer" as
|
||||
an opaque box — without turning this into a full CS/architecture course.
|
||||
|
||||
**Approximate timing:** 15 minutes read; 10 minutes live with discussion.
|
||||
|
||||
**Common misconception to watch for:** students often conflate RAM and storage ("my computer
|
||||
has 8GB" meaning disk space). Draw the volatile-vs-persistent distinction explicitly and early.
|
||||
|
||||
**Discussion question:** "Why can't a program just talk to the hardware directly instead of
|
||||
going through the kernel?" (Answer: the kernel is what lets many programs safely share one CPU/
|
||||
one block of RAM without stepping on each other — this is worth naming explicitly, as it's the
|
||||
root of why containers and Kubernetes are even possible.)
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Guided Exercise: Predict Before You Look
|
||||
|
||||
Before you run the real inspection commands in the Lab, commit to a guess — this is what makes
|
||||
the real numbers meaningful instead of just scrolling past them.
|
||||
|
||||
## Your task
|
||||
|
||||
On paper or in a scratch file, write down your best GUESS for:
|
||||
|
||||
1. How many CPU cores does this machine have?
|
||||
2. How much total RAM, roughly, in GB?
|
||||
3. How much free disk space is on the root filesystem?
|
||||
|
||||
Then think through: which of these three would you expect a "student Lab" environment to be
|
||||
MOST constrained on, and why? (Hint: think about what happens if 50 students are each running
|
||||
their own Lab on one shared physical machine at the same time.)
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
There's no single right numeric answer here — the point is the habit of predicting first. A
|
||||
reasonable answer to the "most constrained" question: memory and CPU are typically the tightest
|
||||
shared resource on a multi-tenant host, since every additional Lab needs its own slice of both,
|
||||
while disk is comparatively cheap to over-provision.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Have students commit to a number verbally or on a shared doc BEFORE anyone
|
||||
runs the real commands — the value is in noticing the gap between guess and reality, not in
|
||||
getting it right.
|
||||
|
||||
**Approximate timing:** 5 minutes.
|
||||
|
||||
**Optional extension:** ask what would happen to their guess if this were a "real" laptop
|
||||
they personally own vs. a shared cloud server — good primer for later resource-limit
|
||||
discussions (ROOT 06's resource requests/limits, ROOT 09's resource governance).
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Reflection: You Just Looked Inside the Box
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. How close was your guess to the real numbers? What surprised you most?
|
||||
2. Name one thing you now understand about "the computer" that you couldn't have explained
|
||||
before this module.
|
||||
3. When you eventually run `kubectl describe pod` and see `requests`/`limits` for CPU and
|
||||
memory, what do you now expect those numbers to actually be constraining?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**Every resource limit you'll ever configure — in Docker, in Kubernetes, in a systemd unit — is
|
||||
a promise about exactly the three things this module named: CPU, memory, and storage.** You
|
||||
can't reason well about "why did my container get killed" or "why is my Pod stuck Pending"
|
||||
without this base model. This module isn't computer-science trivia — it's the vocabulary the
|
||||
rest of ROOT_ assumes you have.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 3 is worth spending real time on — it's
|
||||
the first moment students connect this abstract lesson to something they'll type as a real
|
||||
command many courses from now. Naming that connection explicitly here pays off later.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# Linux and the Shell
|
||||
|
||||
## What "Linux" actually means
|
||||
|
||||
Linux is a **kernel** — the core piece of software from Module 1 that manages CPU, memory, and
|
||||
storage. What most people call "a Linux system" (Ubuntu, Debian, Fedora, Alpine...) is a
|
||||
**distribution**: the Linux kernel plus a chosen set of standard tools, package manager, and
|
||||
configuration conventions layered on top. Different distributions, same kernel underneath —
|
||||
which is why a command you learn on Ubuntu mostly still works on Alpine.
|
||||
|
||||
## Shell vs. terminal
|
||||
|
||||
These get used interchangeably but are different things:
|
||||
|
||||
- The **terminal** is the window/program that displays text and sends your keystrokes somewhere.
|
||||
- The **shell** is the program actually interpreting what you type as commands — `bash` is the
|
||||
shell this Lab uses. The terminal is just the display; the shell is the brain.
|
||||
|
||||
## Command structure
|
||||
|
||||
Every command follows roughly the same shape:
|
||||
|
||||
```
|
||||
command [options] [arguments]
|
||||
```
|
||||
|
||||
`ls -la /var/log` — `ls` is the command, `-la` are options (flags) that change its behavior,
|
||||
`/var/log` is the argument (what to act on). Options can usually be combined (`-l -a` = `-la`).
|
||||
|
||||
## Exit codes: how a command reports success or failure
|
||||
|
||||
Every command returns a number when it finishes: `0` means success, anything else (1-255) means
|
||||
some kind of failure. This isn't a Linux quirk — it's the exact mechanism scripts, CI pipelines,
|
||||
and (much later) Kubernetes health checks all rely on to know whether something worked. You can
|
||||
read the last command's exit code with the special variable `$?`.
|
||||
|
||||
## Getting help without leaving the terminal
|
||||
|
||||
- **Tab completion** — press Tab while typing a command or path; the shell completes it for you
|
||||
or shows you the options. This is a genuine time-saver, not just a convenience.
|
||||
- **Command history** — press the Up arrow to recall previous commands; `history` lists them.
|
||||
- **`man <command>`** — the manual page for almost any command, with every option explained.
|
||||
`man ls`, `man grep`, `man chmod` are all real, useful references — reaching for `man` before
|
||||
a search engine is a habit worth building now.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Establish shell fluency vocabulary (shell vs. terminal, exit codes, options vs.
|
||||
arguments) that every later module assumes without re-explaining.
|
||||
|
||||
**Approximate timing:** 15 minutes read; 10 minutes live with a short live demo of tab
|
||||
completion and `man`.
|
||||
|
||||
**Common misconception to watch for:** students often think "Linux" and "the terminal" are the
|
||||
same thing — clarify the kernel/distribution/shell/terminal layering explicitly.
|
||||
|
||||
**Demo idea:** live-type a command that fails (e.g. `ls /nonexistent`), then run `echo $?`
|
||||
immediately after to show the exit code in real time — more convincing than describing it.
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# Demonstration: Watching Exit Codes in Real Time
|
||||
|
||||
## OBSERVE
|
||||
|
||||
Run a command you know will succeed, then immediately check its exit code:
|
||||
|
||||
```
|
||||
$ true
|
||||
$ echo $?
|
||||
0
|
||||
```
|
||||
|
||||
## HYPOTHESIS
|
||||
|
||||
Now predict: what will the exit code be if the command fails instead?
|
||||
|
||||
## INSPECT
|
||||
|
||||
Run a command that fails, and check immediately:
|
||||
|
||||
```
|
||||
$ ls /this-does-not-exist
|
||||
ls: cannot access '/this-does-not-exist': No such file or directory
|
||||
$ echo $?
|
||||
2
|
||||
```
|
||||
|
||||
## CHANGE
|
||||
|
||||
Try a few other failing commands and note their codes: `grep nomatch /etc/hostname` (exit 1 —
|
||||
"no matches found," a different kind of failure than "file not found").
|
||||
|
||||
## VERIFY
|
||||
|
||||
Notice: exit codes aren't just "0 or 1" — different failures often get different non-zero
|
||||
codes. `0` always means success; the exact non-zero value's MEANING varies by command, but ANY
|
||||
non-zero value reliably means "something went wrong," which is the part every script actually
|
||||
relies on (`if command; then ... else ...`).
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Demo goal:** Make the abstract "exit code" concept concrete by actually watching `$?` change
|
||||
in real time, immediately after each command — this is far more convincing live than described
|
||||
in prose.
|
||||
|
||||
**Approximate timing:** 8 minutes live.
|
||||
|
||||
**Common mistake to watch for:** running `echo $?` after ANY other command in between (even
|
||||
`ls`) silently checks the WRONG command's exit code — emphasize "check it immediately."
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Knowledge Check: Linux and the Shell
|
||||
|
||||
---
|
||||
|
||||
**1. What's the difference between a shell and a terminal?**
|
||||
|
||||
*Answer:* The terminal is the window/program that displays text and captures keystrokes; the
|
||||
shell (e.g. `bash`) is the program actually interpreting what you type as commands. The terminal
|
||||
is the display, the shell is the brain.
|
||||
|
||||
---
|
||||
|
||||
**2. A command exits with code `0`. What does that tell you?**
|
||||
|
||||
*Answer:* It succeeded. `0` is the universal "success" convention every script and pipeline
|
||||
relies on; any non-zero value means some kind of failure occurred.
|
||||
|
||||
---
|
||||
|
||||
**3. You've typed half a long path and can't remember the rest. What should you press?**
|
||||
|
||||
*Answer:* Tab — tab completion finishes the path (or shows the options) without you needing to
|
||||
type or remember it exactly.
|
||||
|
||||
---
|
||||
|
||||
**4. Where would you look to learn every option a command supports, without leaving the
|
||||
terminal?**
|
||||
|
||||
*Answer:* Its manual page — `man <command>`.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 1 is the one students most often blur together — worth confirming
|
||||
verbally before moving on.
|
||||
|
||||
**Approximate timing:** 5 minutes.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Guided Exercise: Predict the Exit Code Before You Run It
|
||||
|
||||
This is a paper exercise — commit to an answer before checking it in the Lab.
|
||||
|
||||
## Your task
|
||||
|
||||
For each command below, predict whether its exit code will be `0` (success) or non-zero
|
||||
(failure), and why:
|
||||
|
||||
1. `cat /etc/hostname` (a file that exists)
|
||||
2. `cat /etc/does-not-exist-at-all` (a file that doesn't)
|
||||
3. `grep root /etc/passwd` (a pattern that's very likely present)
|
||||
4. `grep zzz-not-a-real-user /etc/passwd` (a pattern that's very likely absent)
|
||||
5. `mkdir /tmp/already-exists && mkdir /tmp/already-exists` (running the same `mkdir` twice)
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
1. `0` — the file exists and is readable.
|
||||
2. Non-zero — the file doesn't exist, `cat` fails.
|
||||
3. `0` — `grep` succeeds (exit 0) when it finds at least one match.
|
||||
4. Non-zero (specifically `1`) — `grep` "fails" when it finds NO matches, even though nothing
|
||||
went technically wrong. This is a common surprise: `grep`'s exit code means "did I find a
|
||||
match," not "did an error occur."
|
||||
5. The SECOND `mkdir` fails (non-zero) — the directory already exists. The first one succeeds.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Item 4 is the one worth pausing on — many students assume "no error message"
|
||||
means "exit code 0," and `grep`'s no-match convention is a good early counter-example.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Reflection: Exit Codes Are Everywhere After This
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Before this module, did you know commands returned a machine-readable success/failure
|
||||
signal at all? What did you assume happened instead?
|
||||
2. Where do you think exit codes might matter later in ROOT_ — in a script, a CI pipeline, or
|
||||
a container?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**Every automated system you'll ever build or operate checks exit codes, not error messages.**
|
||||
A script deciding whether to continue, a CI pipeline deciding whether to deploy, and (much
|
||||
later) a container orchestrator deciding whether a process is healthy, all reduce to the exact
|
||||
same question this module taught you to answer by hand: did this exit `0` or not? Getting
|
||||
comfortable checking `$?` now means none of those later systems will feel like magic.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 2 previews Docker/Kubernetes health
|
||||
checks well before students get there — useful to name explicitly without going deep yet.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Navigating the Filesystem
|
||||
|
||||
## Everything is one tree, starting at `/`
|
||||
|
||||
Linux has exactly one filesystem tree, rooted at `/` (pronounced "root," unrelated to your
|
||||
username). Every file and directory, no matter what physical disk it's actually on, appears
|
||||
somewhere under this one tree — there's no separate "C:\" and "D:\" the way Windows shows
|
||||
multiple drives.
|
||||
|
||||
## Absolute vs. relative paths
|
||||
|
||||
An **absolute path** starts with `/` and always means the same location no matter where you
|
||||
currently are: `/home/student/notes.txt`. A **relative path** is interpreted relative to your
|
||||
CURRENT directory: `notes.txt` means something different depending on where you're standing
|
||||
when you type it. `.` means "this directory," `..` means "one directory up" — `cd ..` moves up
|
||||
one level, `cd ../..` moves up two.
|
||||
|
||||
## Key commands
|
||||
|
||||
- `pwd` — print your current directory (where am I?)
|
||||
- `ls` — list what's in a directory (`ls -la` shows hidden files and details)
|
||||
- `cd <path>` — change directory (absolute or relative)
|
||||
|
||||
## Hidden files
|
||||
|
||||
A file or directory starting with `.` (like `.bashrc` or `.config`) is "hidden" — `ls` skips it
|
||||
by default, `ls -a` shows it. This isn't a security feature, just a convention to keep
|
||||
configuration/metadata files out of the way of normal directory listings.
|
||||
|
||||
## The important top-level directories (conceptually, not memorized)
|
||||
|
||||
You don't need to memorize the entire Filesystem Hierarchy Standard, but recognizing these by
|
||||
purpose will save you real confusion later:
|
||||
|
||||
- **`/etc`** — system-wide configuration files (nginx's config lives at `/etc/nginx/nginx.conf`)
|
||||
- **`/var`** — variable data that changes while the system runs: logs (`/var/log`), among others
|
||||
- **`/home`** — personal directories for regular users
|
||||
- **`/tmp`** — temporary files, safe to assume anything here can disappear on reboot
|
||||
- **`/usr`** — most installed programs and their supporting files live somewhere under here
|
||||
- **`/proc`** — not real files on disk at all — a live, kernel-generated view into running
|
||||
processes and system state (you'll use this again in Module 5)
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Give students a working mental model of "one tree, absolute vs. relative
|
||||
paths" plus just enough top-level-directory recognition to not be lost later — not FHS
|
||||
memorization.
|
||||
|
||||
**Approximate timing:** 15 minutes read; 10 minutes live.
|
||||
|
||||
**Common misconception to watch for:** students coming from Windows sometimes expect separate
|
||||
drives/roots — emphasize the single unified tree explicitly.
|
||||
|
||||
**Demo idea:** `cd /` then `ls`, narrating what each top-level directory is for as you see it,
|
||||
rather than listing them in the abstract.
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# Demonstration: The Same Word, Two Different Destinations
|
||||
|
||||
## OBSERVE
|
||||
|
||||
Starting from `/home/student`, run `cd project` — you land at `/home/student/project`.
|
||||
|
||||
## HYPOTHESIS
|
||||
|
||||
Now, from `/home/student/project`, what do you predict `cd project` will do this time?
|
||||
|
||||
## INSPECT
|
||||
|
||||
```
|
||||
$ pwd
|
||||
/home/student/project
|
||||
$ cd project
|
||||
bash: cd: project: No such file or directory
|
||||
```
|
||||
|
||||
It fails — there's no `project` directory INSIDE `project` (unless you happened to create one).
|
||||
The exact same relative path meant something completely different depending on where you stood.
|
||||
|
||||
## CHANGE
|
||||
|
||||
From `/home/student/project`, run `cd ../project` instead — this correctly goes back up one
|
||||
level, then into `project` again, landing you right back where you started.
|
||||
|
||||
## VERIFY
|
||||
|
||||
`pwd` after each `cd` to confirm exactly where you ended up — this habit (verify your location
|
||||
before assuming it) prevents a huge share of "why did my command run somewhere unexpected"
|
||||
confusion later.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Demo goal:** Make "relative paths depend on where you are" concrete and slightly surprising,
|
||||
rather than an abstract rule to memorize.
|
||||
|
||||
**Approximate timing:** 8 minutes live.
|
||||
|
||||
**Common mistake to watch for:** students assume a relative path always means the SAME thing
|
||||
once they've used it once — reinforce that it's re-evaluated fresh every time, from wherever
|
||||
they currently are.
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Knowledge Check: Navigating the Filesystem
|
||||
|
||||
---
|
||||
|
||||
**1. What does `..` mean in a path?**
|
||||
|
||||
*Answer:* One directory up from your current location.
|
||||
|
||||
---
|
||||
|
||||
**2. You're at `/home/student/project`. Does `cd project` do the same thing here as it did at
|
||||
`/home/student`?**
|
||||
|
||||
*Answer:* No — a relative path is re-evaluated from your CURRENT location every time. At
|
||||
`/home/student`, `project` refers to `/home/student/project`; from inside that same directory,
|
||||
`project` would refer to `/home/student/project/project`, which likely doesn't exist.
|
||||
|
||||
---
|
||||
|
||||
**3. A file is "hidden" from a normal `ls`. What actually makes it hidden?**
|
||||
|
||||
*Answer:* Its name starts with a `.` — it's purely a naming convention `ls` respects by
|
||||
default, not a real permission or security feature. `ls -a` shows it.
|
||||
|
||||
---
|
||||
|
||||
**4. Where would you expect to find a running system's log files?**
|
||||
|
||||
*Answer:* `/var/log` — `/var` holds data that changes while the system runs.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 2 is the one worth confirming out loud — it's the core "gotcha" of
|
||||
relative paths.
|
||||
|
||||
**Approximate timing:** 5 minutes.
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# Guided Exercise: Plan a Directory Structure on Paper First
|
||||
|
||||
Before you build anything in the Lab, plan it — this is the same discipline you'll use later
|
||||
when planning a Kubernetes namespace layout or a Git repository structure.
|
||||
|
||||
## Your task
|
||||
|
||||
You're setting up a small project called `weather-app`. On paper, sketch a directory tree
|
||||
containing:
|
||||
|
||||
- A place for source code
|
||||
- A place for documentation
|
||||
- A place for tests
|
||||
- A single hidden configuration file at the project's root
|
||||
|
||||
Write out the exact `mkdir` and file-creation commands you'd use, including whether each path
|
||||
you write is absolute or relative.
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
```
|
||||
mkdir -p weather-app/src weather-app/docs weather-app/tests
|
||||
echo "ready" > weather-app/.config
|
||||
```
|
||||
|
||||
(Or with an absolute path: `mkdir -p /srv/weather-app/src ...`.) The `-p` flag matters here —
|
||||
without it, `mkdir` refuses to create `weather-app/src` if `weather-app` doesn't already exist.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Have students commit to their command in writing before revealing the worked
|
||||
answer — the value is in noticing what `-p` actually buys them, not just copying a correct
|
||||
command.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
|
||||
|
||||
**Common mistake to watch for:** forgetting `mkdir -p` and getting a "No such file or
|
||||
directory" error when creating a nested path in one command — a good moment to connect back to
|
||||
Module 2's exit-code lesson (that error IS a non-zero exit code).
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Reflection: Structure Isn't Bureaucracy, It's Communication
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Why might a real engineering team insist on a consistent directory structure across every
|
||||
project, instead of everyone organizing files however they like?
|
||||
2. Which of `/etc`, `/var`, `/home`, `/tmp`, `/usr`, `/proc` do you feel most confident you could
|
||||
explain to someone else right now? Which one still feels fuzzy?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**A predictable filesystem layout is what lets tools (and other engineers) reason about a
|
||||
system without asking you first.** `/etc` always means configuration, `/var/log` always means
|
||||
logs — this convention is exactly why a monitoring tool can be pointed at "check `/var/log`" on
|
||||
any Linux machine and just work. Container images later inherit this same convention (an
|
||||
nginx container still keeps its config at `/etc/nginx/nginx.conf`) — you're not learning a
|
||||
one-off Linux fact, you're learning a convention that outlives this specific module.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 1 connects directly to why Kubernetes
|
||||
namespaces and Git repository conventions matter later — a good moment to name that connection
|
||||
without going deep yet.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# Files and Text
|
||||
|
||||
## Basic file operations
|
||||
|
||||
- `touch <file>` — create an empty file (or update its timestamp if it exists)
|
||||
- `mkdir <dir>` — create a directory (`-p` creates any missing parent directories too)
|
||||
- `cp <src> <dst>` — copy a file
|
||||
- `mv <src> <dst>` — move OR rename a file (Linux treats renaming as "moving to a new name in
|
||||
the same place")
|
||||
- `rm <file>` — delete a file (`-r` for a directory, `-f` to skip confirmation — dangerous
|
||||
combined: `rm -rf` deletes without asking, permanently)
|
||||
|
||||
## Reading text
|
||||
|
||||
- `cat <file>` — print the whole file at once
|
||||
- `less <file>` — page through a file interactively (press `q` to quit) — the right tool for
|
||||
anything too long to read in one screen
|
||||
- `head <file>` / `tail <file>` — the first/last 10 lines by default (`-n 20` for a different
|
||||
count); `tail -f` follows a file live as new lines are appended — exactly how you'd watch a
|
||||
log file update in real time
|
||||
|
||||
## Searching text: `grep`
|
||||
|
||||
`grep PATTERN file` prints only the lines matching PATTERN. This is one of the most-used
|
||||
commands in real operations work — "find the lines that matter" beats "read everything."
|
||||
|
||||
## Pipes and redirection: composing commands
|
||||
|
||||
- **A pipe (`|`)** sends one command's output as the next command's input:
|
||||
`cat app.log | grep ERROR` — read the file, then filter it.
|
||||
- **Redirection (`>`)** sends a command's output into a file instead of the screen (overwriting
|
||||
it): `grep ERROR app.log > errors.txt`. `>>` appends instead of overwriting.
|
||||
|
||||
## stdout vs. stderr
|
||||
|
||||
A command actually has TWO separate output streams: **stdout** (normal output) and **stderr**
|
||||
(error messages), even though both print to your screen by default. This is why
|
||||
`command 2>/dev/null` (redirecting stream `2`, stderr, to nowhere) can suppress error noise
|
||||
while still showing normal output — you'll use exactly this pattern in later Labs.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Build real fluency composing small commands together — this is the single
|
||||
highest-leverage Linux skill for troubleshooting work later in ROOT_.
|
||||
|
||||
**Approximate timing:** 15 minutes read; 10 minutes live.
|
||||
|
||||
**Common misconception to watch for:** students often don't realize stdout/stderr are separate
|
||||
streams until they see `2>/dev/null` change what's suppressed — worth demonstrating live.
|
||||
|
||||
**Demo idea:** run a command that produces both real output and an error in the same call
|
||||
(e.g. `ls /etc /nonexistent`) and show how `2>/dev/null` changes what appears.
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# Demonstration: Filtering a Log Down to What Matters
|
||||
|
||||
## OBSERVE
|
||||
|
||||
A raw log file has five lines mixing `INFO` and `ERROR`:
|
||||
|
||||
```
|
||||
$ cat app.log
|
||||
INFO: boot ok
|
||||
ERROR: disk full
|
||||
INFO: retry
|
||||
ERROR: timeout
|
||||
INFO: done
|
||||
```
|
||||
|
||||
## HYPOTHESIS
|
||||
|
||||
Predict what `grep ERROR app.log` will print, before running it.
|
||||
|
||||
## INSPECT
|
||||
|
||||
```
|
||||
$ grep ERROR app.log
|
||||
ERROR: disk full
|
||||
ERROR: timeout
|
||||
```
|
||||
|
||||
Only the matching lines — everything else is filtered out.
|
||||
|
||||
## CHANGE
|
||||
|
||||
Now redirect that filtered result into a new file instead of just viewing it:
|
||||
|
||||
```
|
||||
$ grep ERROR app.log > errors.txt
|
||||
$ cat errors.txt
|
||||
ERROR: disk full
|
||||
ERROR: timeout
|
||||
```
|
||||
|
||||
## VERIFY
|
||||
|
||||
Count how many lines actually matched, without eyeballing them: `wc -l < errors.txt` (or
|
||||
`grep -c ERROR app.log` directly). This composition — filter, then count, then act — is the
|
||||
exact shape of a huge share of real troubleshooting work.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Demo goal:** Show the filter → redirect → verify chain as one continuous, composable
|
||||
workflow, not three unrelated commands.
|
||||
|
||||
**Approximate timing:** 8 minutes live.
|
||||
|
||||
**Common mistake to watch for:** using `>` when they meant `>>` and accidentally overwriting a
|
||||
file they wanted to append to — worth calling out explicitly since it's a real, common mistake
|
||||
with no undo.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Knowledge Check: Files and Text
|
||||
|
||||
---
|
||||
|
||||
**1. What's the difference between `>` and `>>`?**
|
||||
|
||||
*Answer:* `>` overwrites the target file's contents entirely; `>>` appends to the end of it,
|
||||
preserving what was already there.
|
||||
|
||||
---
|
||||
|
||||
**2. A command prints both normal output and an error message. What are these two streams
|
||||
called?**
|
||||
|
||||
*Answer:* stdout (normal output) and stderr (error messages) — two separate streams that both
|
||||
happen to print to your screen by default.
|
||||
|
||||
---
|
||||
|
||||
**3. You want to watch a log file update live, as new lines are appended, without re-running a
|
||||
command repeatedly.**
|
||||
|
||||
*Answer:* `tail -f <file>` — follows the file and prints new lines as they're written.
|
||||
|
||||
---
|
||||
|
||||
**4. `cat app.log | grep ERROR`. What does the `|` actually do?**
|
||||
|
||||
*Answer:* It pipes `cat`'s output directly into `grep`'s input, so `grep` filters the file's
|
||||
contents without needing a separate temporary file.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 2 is worth a live show-of-hands check — many students haven't
|
||||
consciously registered that stdout/stderr are separate until this module.
|
||||
|
||||
**Approximate timing:** 5 minutes.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Guided Exercise: Which Tool Actually Answers the Question?
|
||||
|
||||
This is a paper exercise — commit to an answer before checking it in the Lab.
|
||||
|
||||
## Your task
|
||||
|
||||
For each situation, name the ONE command (or short pipeline) you'd reach for first:
|
||||
|
||||
1. You want to see just the last 20 lines of a 50,000-line log file.
|
||||
2. You want to know how many lines in a file contain the word "timeout."
|
||||
3. You want to watch a log file update live while a process is running.
|
||||
4. You want to see a file's full contents but it's too long for one screen and you want to
|
||||
scroll through it interactively.
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
1. `tail -n 20 file.log`
|
||||
2. `grep -c timeout file.log`
|
||||
3. `tail -f file.log`
|
||||
4. `less file.log`
|
||||
|
||||
Notice: `cat` would technically "work" for #4 too, but it dumps everything at once with no way
|
||||
to scroll back — `less` is the actual right tool once a file is longer than one screen.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Item 4 is worth discussing explicitly — "it works" isn't the same bar as
|
||||
"it's the right tool for this situation," and that distinction matters more as commands compose
|
||||
into real troubleshooting workflows.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Reflection: Filtering Is a Reasoning Skill, Not a Syntax Trick
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Before this module, how would you have found "the important lines" in a huge log file?
|
||||
How does that compare to `grep`?
|
||||
2. Which combination (pipe, redirect, or a specific command like `tail -f`) do you expect to
|
||||
use the most once you're doing real troubleshooting work?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**"Read everything and eyeball it" does not scale — filtering does.** Every troubleshooting
|
||||
framework you'll learn later in ROOT_ (starting with ROOT 02's own version, then again more
|
||||
formally in ROOT 10) depends on being able to quickly narrow a huge amount of system output down
|
||||
to the handful of lines that actually matter. `grep`, pipes, and redirection are the first real
|
||||
tools you have for that — not a syntax exercise, a reasoning skill.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 1 often produces good answers ("I'd
|
||||
just scroll and read") that make the value of filtering obvious by contrast — let students
|
||||
articulate this themselves rather than stating it for them.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Processes
|
||||
|
||||
## What a process actually is
|
||||
|
||||
A **process** is a running instance of a program — its own code, its own private memory, and a
|
||||
unique identifier the kernel tracks called a **PID** (Process ID). Run the same program twice
|
||||
(open two terminals both running `bash`), and you get two separate processes with two different
|
||||
PIDs, even though they're running identical code.
|
||||
|
||||
## Foreground vs. background
|
||||
|
||||
A command you run normally occupies your terminal until it finishes (**foreground**) — you
|
||||
can't type anything else until it's done. Append `&` to run it in the **background** instead,
|
||||
freeing your terminal immediately while the process keeps running. `sleep 300 &` starts a
|
||||
300-second sleep in the background and hands your prompt right back.
|
||||
|
||||
## Inspecting processes: `ps` and `top`
|
||||
|
||||
- `ps aux` — a snapshot of every process currently running, including its PID, how much CPU/
|
||||
memory it's using, and the exact command that started it.
|
||||
- `top` (or `htop`, if installed) — the same idea, but live-updating and sorted by resource
|
||||
usage, useful for "what's actually eating CPU/memory right now."
|
||||
|
||||
## Stopping a process: `kill` and signals
|
||||
|
||||
`kill <PID>` doesn't instantly destroy a process — it sends it a **signal**, a message the
|
||||
process can (usually) choose how to respond to. The default signal, `SIGTERM` (15), politely
|
||||
asks a process to shut down, giving it a chance to clean up first. `kill -9 <PID>` sends
|
||||
`SIGKILL`, which the kernel enforces immediately with no chance for the process to react —
|
||||
appropriate only when a process is truly stuck and ignoring `SIGTERM`. `kill -0 <PID>` is a
|
||||
special case: it sends NO real signal at all, just checks whether the PID still exists —
|
||||
exactly how you confirm "did that process actually stop" without side effects.
|
||||
|
||||
## Process hierarchy
|
||||
|
||||
Every process (except the very first one at boot) has a **parent** — the process that started
|
||||
it. Your shell is the parent of every command you run from it. This matters later: a parent
|
||||
process's own lifecycle can affect its children (this is part of why a container, which is
|
||||
often just one process tree, behaves the way it does when its main process exits).
|
||||
|
||||
## Why this matters for containers and Kubernetes
|
||||
|
||||
A container is, underneath, mostly an isolated process (or small process tree) with its own
|
||||
view of the filesystem and network — when people say "a container exited," they mean its main
|
||||
process exited, exactly the same event you'll cause on purpose in this module's Lab. Kubernetes'
|
||||
entire job, at its core, is deciding which machine a process (inside a container) runs on and
|
||||
restarting it if it dies unexpectedly — none of that will feel intuitive without this module's
|
||||
model of what a process actually is.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Build a real process mental model (PID, foreground/background, signals,
|
||||
parent/child) that ROOT 04 (containers) and ROOT 06 (Kubernetes workloads) both directly build
|
||||
on without re-explaining.
|
||||
|
||||
**Approximate timing:** 15 minutes read; 10 minutes live.
|
||||
|
||||
**Common misconception to watch for:** students often think `kill` always means "forcefully
|
||||
destroy" — clarify that plain `kill` sends a polite, ignorable request (SIGTERM), and `-9` is
|
||||
the forceful escalation, not the default.
|
||||
|
||||
**Discussion question:** "What do you think happens to a container if its main process exits on
|
||||
its own, versus if something outside kills it?" (Don't need a complete answer yet — just plant
|
||||
the question for later.)
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
# Demonstration: Watching a Process Actually Terminate
|
||||
|
||||
## OBSERVE
|
||||
|
||||
Start a long-running background process and note its PID:
|
||||
|
||||
```
|
||||
$ sleep 300 &
|
||||
[1] 1421
|
||||
```
|
||||
|
||||
`1421` is the PID (yours will differ every time).
|
||||
|
||||
## HYPOTHESIS
|
||||
|
||||
Predict: will `kill -0 1421` print anything if the process is still running?
|
||||
|
||||
## INSPECT
|
||||
|
||||
```
|
||||
$ kill -0 1421 && echo "still running"
|
||||
still running
|
||||
```
|
||||
|
||||
`kill -0` sends no real signal — silence (or, here, our own `echo`) means the process exists.
|
||||
|
||||
## CHANGE
|
||||
|
||||
Now actually terminate it:
|
||||
|
||||
```
|
||||
$ kill 1421
|
||||
```
|
||||
|
||||
## VERIFY
|
||||
|
||||
```
|
||||
$ kill -0 1421 2>/dev/null && echo "still running" || echo "gone"
|
||||
gone
|
||||
```
|
||||
|
||||
The exact same check now reports the opposite result — real, observable proof the process is
|
||||
gone, not an assumption.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Demo goal:** Make "checking before and after" a visible habit, not just narrated — students
|
||||
should see the SAME command return a different real answer once state actually changed.
|
||||
|
||||
**Approximate timing:** 8 minutes live.
|
||||
|
||||
**Common mistake to watch for:** reaching for `kill -9` by default "to be safe" — reinforce that
|
||||
plain `kill` (SIGTERM) is the correct first attempt almost always.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Knowledge Check: Processes
|
||||
|
||||
---
|
||||
|
||||
**1. What does a PID identify?**
|
||||
|
||||
*Answer:* One specific running instance of a program — the kernel's unique identifier for that
|
||||
process, distinct from every other running process, even other copies of the same program.
|
||||
|
||||
---
|
||||
|
||||
**2. What's the actual difference between `kill <pid>` and `kill -9 <pid>`?**
|
||||
|
||||
*Answer:* Plain `kill` sends SIGTERM, a polite request the process can choose to handle (clean
|
||||
up, then exit). `kill -9` sends SIGKILL, which the kernel enforces immediately with no chance
|
||||
for the process to react — an escalation for when SIGTERM is ignored, not the default choice.
|
||||
|
||||
---
|
||||
|
||||
**3. You want to confirm a process is still running WITHOUT affecting it in any way. What do
|
||||
you use?**
|
||||
|
||||
*Answer:* `kill -0 <pid>` — sends no real signal, only checks whether the PID still exists.
|
||||
|
||||
---
|
||||
|
||||
**4. Why might understanding processes matter for understanding containers later?**
|
||||
|
||||
*Answer:* A container is, underneath, mostly an isolated process (or small process tree) — a
|
||||
container "exiting" is really its main process exiting, the exact same event this module's Lab
|
||||
causes on purpose.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 2 is worth confirming explicitly — "kill -9 by default" is a common,
|
||||
worth-correcting habit.
|
||||
|
||||
**Approximate timing:** 5 minutes.
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Guided Exercise: Choosing the Right Signal
|
||||
|
||||
This is a paper exercise — commit to an answer before checking it in the Lab.
|
||||
|
||||
## Your task
|
||||
|
||||
For each situation, decide: plain `kill` (SIGTERM) first, or is `kill -9` (SIGKILL) actually
|
||||
justified?
|
||||
|
||||
1. A normal web server process you want to shut down for a routine restart.
|
||||
2. A process that received SIGTERM two minutes ago and is still running, showing no signs of
|
||||
responding.
|
||||
3. A script you just started by mistake, that hasn't done anything harmful yet.
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
1. Plain `kill` — give it the chance to shut down cleanly (close connections, flush data, etc).
|
||||
2. `kill -9` is now justified — you already gave it the polite chance, and it didn't respond.
|
||||
3. Plain `kill` first, still — "just started" doesn't change the default; SIGKILL is a last
|
||||
resort, not a shortcut for convenience.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Item 3 sometimes trips students who reach for `-9` out of impatience — worth
|
||||
reinforcing that SIGTERM-first is the discipline REGARDLESS of urgency, since a process that
|
||||
ignores SIGTERM is the actual signal that escalation is warranted.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Reflection: You Just Stopped a Real Process, Not a Simulation
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Before checking `kill -0` after your `kill`, how confident were you the process was
|
||||
actually gone? What would you have done if it hadn't stopped?
|
||||
2. Why do you think SIGTERM-before-SIGKILL matters more for a real production service than for
|
||||
a throwaway script in a Lab?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**"I ran the kill command" is not the same as "I confirmed the process is gone."** This module's
|
||||
Lab required you to verify state, not just take an action and assume it worked — that
|
||||
verification step is exactly what separates a confident operator from someone hoping for the
|
||||
best. You'll repeat this exact pattern (act, then verify) constantly for the rest of ROOT_,
|
||||
starting as soon as ROOT 04's containers and continuing all the way through Kubernetes
|
||||
troubleshooting.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 2 is a good bridge to ROOT 06's later
|
||||
discussion of graceful shutdown/`terminationGracePeriodSeconds` — plant the idea now without
|
||||
naming Kubernetes specifics yet.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# Users and Permissions Foundations
|
||||
|
||||
## Users, groups, and ownership
|
||||
|
||||
Every file on Linux has an **owner** (a user) and a **group**. `ls -la` shows both: the third
|
||||
column is the owning user, the fourth is the owning group. Multiple users can belong to the same
|
||||
group, which is how permissions get shared across a team without giving every individual user
|
||||
direct ownership.
|
||||
|
||||
## Read, write, execute — for three different audiences
|
||||
|
||||
Every file has permissions for THREE separate audiences: the owner, the group, and everyone
|
||||
else ("other"). Each audience gets some combination of:
|
||||
|
||||
- **r**ead — can view the file's contents (or list a directory's contents)
|
||||
- **w**rite — can modify the file (or create/delete files inside a directory)
|
||||
- e**x**ecute — can run the file as a program (or enter/traverse into a directory)
|
||||
|
||||
`ls -la` shows this as a 10-character string like `-rw-r--r--`: the first character is the file
|
||||
type, then three groups of `rwx` for owner/group/other.
|
||||
|
||||
## Reading and setting permissions as octal numbers
|
||||
|
||||
Each `rwx` triplet can be written as one digit: read=4, write=2, execute=1, summed together.
|
||||
`rw-` = 4+2 = `6`. `r--` = `4`. So `644` means: owner can read+write (6), group can read-only
|
||||
(4), everyone else can read-only (4) — a very common, sensible default for a regular file.
|
||||
`chmod 644 <file>` sets exactly that.
|
||||
|
||||
## `sudo`: temporary elevated privilege
|
||||
|
||||
Rather than logging in AS the root user (with permanent full access to everything), `sudo
|
||||
<command>` runs just that one command with elevated privileges, after checking you're allowed
|
||||
to. This is deliberate: the less time you spend with full privileges, the smaller the window for
|
||||
a mistake (or a compromised session) to do serious damage — the exact same reasoning behind
|
||||
"least privilege," which this module introduces by name for the first time.
|
||||
|
||||
## Least privilege: the principle, not just the command
|
||||
|
||||
**Least privilege** means: grant exactly the access something needs to do its job, and nothing
|
||||
more. A user who only needs to read a file shouldn't also be able to write it. A process that
|
||||
only needs to read one directory shouldn't have write access to the whole filesystem. This
|
||||
principle isn't specific to Linux permissions — you'll meet it again, in a much more elaborate
|
||||
form, in ROOT 09's Kubernetes RBAC and SecurityContext content. This module is where it starts.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Build real fluency with rwx/octal permissions AND plant the least-privilege
|
||||
PRINCIPLE explicitly — not just command syntax, since ROOT 09 depends on students already
|
||||
having this vocabulary.
|
||||
|
||||
**Approximate timing:** 15 minutes read; 10 minutes live.
|
||||
|
||||
**Common misconception to watch for:** students often memorize "644 = read/write" without being
|
||||
able to derive it from 4+2+1 — walk through the arithmetic explicitly rather than treating the
|
||||
numbers as magic.
|
||||
|
||||
**Discussion question:** "Why not just make everything readable and writable by everyone, to
|
||||
avoid permission headaches?" (Good setup for the least-privilege principle — let students
|
||||
generate the security argument themselves before you confirm it.)
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Demonstration: A Real Permission Denial, and the Real Fix
|
||||
|
||||
## OBSERVE
|
||||
|
||||
A file is owned by root with permissions `600` (owner read/write only):
|
||||
|
||||
```
|
||||
$ stat -c '%a %U' data.txt
|
||||
600 root
|
||||
```
|
||||
|
||||
## HYPOTHESIS
|
||||
|
||||
Predict: can a different, non-root user read this file?
|
||||
|
||||
## INSPECT
|
||||
|
||||
```
|
||||
$ su - labuser -c "cat data.txt"
|
||||
cat: data.txt: Permission denied
|
||||
```
|
||||
|
||||
Confirmed — `600` grants nothing to group or other, so `labuser` (neither the owner nor root)
|
||||
is correctly denied.
|
||||
|
||||
## CHANGE
|
||||
|
||||
Grant read access to everyone, while keeping write access limited to the owner:
|
||||
|
||||
```
|
||||
$ chmod 644 data.txt
|
||||
$ stat -c '%a' data.txt
|
||||
644
|
||||
```
|
||||
|
||||
## VERIFY
|
||||
|
||||
```
|
||||
$ su - labuser -c "cat data.txt"
|
||||
sensitive
|
||||
```
|
||||
|
||||
The exact same command that failed before now succeeds — the permission bits, not the file's
|
||||
actual content, were the only thing that changed.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Demo goal:** Show a permission denial as a real, reproducible fact (not just a described
|
||||
error message) and the exact minimal fix, not an overcorrection like `chmod 777`.
|
||||
|
||||
**Approximate timing:** 8 minutes live.
|
||||
|
||||
**Common mistake to watch for:** students reaching for `chmod 777` "to make the error go away"
|
||||
— explicitly name why that's wrong (it also grants WRITE to everyone, not just the read access
|
||||
actually needed) and connect it back to least privilege.
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# Knowledge Check: Users and Permissions Foundations
|
||||
|
||||
---
|
||||
|
||||
**1. A colleague says "permission denied" and fixes it with `chmod 777`. What's wrong with
|
||||
that fix?**
|
||||
|
||||
*Answer:* `777` grants read, write, AND execute to the owner, group, AND everyone else — almost
|
||||
always far more access than the actual problem required. It "fixes" the symptom while violating
|
||||
least privilege, usually granting write access nobody asked for.
|
||||
|
||||
---
|
||||
|
||||
**2. What does the permission `644` actually mean, broken down?**
|
||||
|
||||
*Answer:* Owner: read+write (4+2=6). Group: read-only (4). Other: read-only (4). Nobody but the
|
||||
owner can modify the file; everyone can read it.
|
||||
|
||||
---
|
||||
|
||||
**3. Why does `sudo <command>` exist instead of just logging in as root permanently?**
|
||||
|
||||
*Answer:* It grants elevated privilege only for the one command that needs it, minimizing the
|
||||
time spent with full access — reducing the chance a mistake (or a compromised session) causes
|
||||
serious, wide damage.
|
||||
|
||||
---
|
||||
|
||||
**4. State the least-privilege principle in your own words.**
|
||||
|
||||
*Answer:* Grant exactly the access something needs to do its job, and nothing more.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 1 is worth discussing as a group — most students have seen `chmod
|
||||
777` somewhere online as "the fix," and unlearning that habit early matters.
|
||||
|
||||
**Approximate timing:** 5 minutes.
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# Guided Exercise: Pick the Minimal Permission, Not the Easy One
|
||||
|
||||
This is a paper exercise — commit to an answer before checking it in the Lab.
|
||||
|
||||
## Your task
|
||||
|
||||
For each situation, write the exact `chmod` octal value that grants the MINIMUM permission
|
||||
actually needed:
|
||||
|
||||
1. A private SSH key file — only the owner should ever read or write it, nobody else, no
|
||||
execute for anyone.
|
||||
2. A shared script every user on the system should be able to run, but only the owner should be
|
||||
able to edit.
|
||||
3. A log file every user should be able to read (for troubleshooting), but only a logging
|
||||
service (the owner) should write to it.
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
1. `600` — owner read+write (6), nothing for group or other (0, 0). This is the real, standard
|
||||
permission for private SSH keys — SSH itself refuses to use a key file that's more open than
|
||||
this.
|
||||
2. `755` — owner read+write+execute (7), group and other read+execute (5, 5) so they can run it
|
||||
but not modify it.
|
||||
3. `644` — owner read+write (6), group and other read-only (4, 4).
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Item 1 is a good moment to mention that this isn't a hypothetical rule — SSH
|
||||
genuinely refuses to use an overly-permissive private key file, a real-world consequence of
|
||||
exactly this principle.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Reflection: Least Privilege Is a Habit, Not a One-Time Fix
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Before this module, what would you have done the first time you hit a "permission denied"
|
||||
error? How does that compare to what you'd do now?
|
||||
2. Name one place outside of file permissions where you'd expect "grant only what's needed" to
|
||||
matter (a user account, an API key, a service's network access — anything).
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**Least privilege is the single idea from this entire module that reappears the most across
|
||||
the rest of ROOT_.** File permissions are just the first, most concrete place you'll apply it —
|
||||
ROOT 09 applies the exact same principle to Kubernetes RBAC (what can a user or workload
|
||||
actually DO) and SecurityContext (what can a container actually access). Getting comfortable
|
||||
asking "what's the minimum this actually needs" now, on a file you can see and touch, makes that
|
||||
same question much easier to ask later about something more abstract, like a ServiceAccount's
|
||||
permissions.
|
||||
|
||||
This module is complete. This is the end of ROOT 01 — continue to ROOT 02: Linux
|
||||
Administration & Networking.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 2 works well as a round-robin — the
|
||||
variety of answers (API keys, database credentials, network firewall rules) is itself useful
|
||||
evidence of how far this one principle travels.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
|
||||
**Milestone note:** this is the last module of ROOT 01 — consider a short recap of the whole
|
||||
course's arc (hardware → shell → filesystem → files/text → processes → permissions) before
|
||||
moving on, so students see it as one coherent foundation rather than six separate topics.
|
||||
@@ -0,0 +1,141 @@
|
||||
# Phase 6D — ROOT_ Systems Foundations
|
||||
|
||||
Phase 6D's mission: build the foundational half of ROOT_ (ROOT 01-04) so a student reaches
|
||||
ROOT 05 already fluent in Linux/networking/Git and understanding why Kubernetes exists, using
|
||||
the existing course/module/activity model throughout.
|
||||
|
||||
## Baseline verification
|
||||
|
||||
Confirmed at start: branch `main`, clean tree, HEAD = `origin/main` =
|
||||
`bd0a10fbfb31b05a6264d083f27d85a6f7d5d50c` (the Phase 6C milestone).
|
||||
|
||||
## The central architectural finding (Section 6/7)
|
||||
|
||||
`backend/server.js`'s scenario engine (`runCommand()`) executes an arbitrary shell command
|
||||
string from a scenario's `setup_commands`/`validation.commands` and checks its output — nothing
|
||||
about it is Kubernetes-specific. This means the existing `lab` activity type and
|
||||
`scenarios/data/*.json` format already fully support Linux and Git exercises with real,
|
||||
deterministic validation. **No `shell_lab`/`git_lab`/`container_lab` activity type was built —
|
||||
the speculative multi-type model the authorization raised as a possibility was unnecessary.**
|
||||
See `docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md`.
|
||||
|
||||
## Container runtime decision, tested not assumed (Section 10)
|
||||
|
||||
Installed Docker Engine (`docker.io` + a static `docker-compose` v2 binary) inside the same
|
||||
already-`Privileged: true` Lab container, alongside k3s. This does not weaken the security
|
||||
boundary — the Lab Host's own Docker socket is never touched; the nested daemon has its own
|
||||
socket inside the guest. Verified empirically against the real production resource caps (2
|
||||
vCPU, 4096MB, 512 PIDs): k3s + dockerd + a running `nginx:alpine` container + a real image
|
||||
build together used 884MB/4096MB and 160/512 PIDs, comfortable headroom in all dimensions.
|
||||
`Dockerfile` and `scripts/entrypoint.sh` updated accordingly (dockerd starts non-blockingly
|
||||
alongside k3s; a Docker daemon failure never breaks Kubernetes-only Labs).
|
||||
|
||||
## ROOT 01 — Computing & Linux Foundations: fully authored, READY
|
||||
|
||||
6 modules, 35 activities, 29 new content files, 6 new hands-on scenario files
|
||||
(`linux-inspect-hardware`, `linux-shell-basics`, `linux-filesystem-navigation`,
|
||||
`linux-files-and-text`, `linux-process-management`, `linux-permissions-foundations`) plus 2 MCQ
|
||||
scenarios, all using the existing generic scenario/validation engine — no new mechanism. Every
|
||||
lab was tested end-to-end via the real scenario API (setup → solve → validate), including
|
||||
negative tests confirming they correctly FAIL when not actually solved. A new
|
||||
`rootnode-linux-foundations-assessment` (2 required + 4 random-draw scenarios) uses the existing
|
||||
Phase 3B engine.
|
||||
|
||||
## ROOT 02-04: architecture designed, content deferred
|
||||
|
||||
`docs/ROOT_FOUNDATIONS_ARCHITECTURE.md` designs all remaining 20 modules (6+6+8) in full —
|
||||
outcomes, activities, and Real Ops connections for each. Given realistic effort constraints,
|
||||
authoring, testing, and verifying ROOT 01 to genuine READY depth was prioritized over shallow
|
||||
architecture-only coverage across all four courses (see
|
||||
`docs/ROOT_FOUNDATIONS_PILOT_READINESS.md` for the full reasoning). ROOT 02-04 remain honestly
|
||||
`status = 'planned'` with zero modules — not padded to look further along than they are.
|
||||
|
||||
## Course prerequisite chain (Section 19)
|
||||
|
||||
`courses.prerequisites` now encodes ROOT 01→02→03→04→05 server-side, verified live: a fresh
|
||||
student sees ROOT 02 correctly LOCKED (ROOT 01 is real, non-empty content) while ROOT 03/04/05
|
||||
stay unlocked (vacuously satisfied — their own prerequisite courses are still empty). Completing
|
||||
ROOT 01 unlocks ROOT 02 for that student. ROOT 06+ and the Capstone are completely unaffected
|
||||
(re-confirmed via regression). No UI-only hiding — this is a real `courses` table constraint the
|
||||
existing `prerequisitesSatisfied()` engine already enforced correctly before this phase; Phase
|
||||
6D only populated real data into it.
|
||||
|
||||
## Extension point for placement/override (Section 20)
|
||||
|
||||
Documented, not built: `courses.prerequisites` is a plain array a future placement assessment or
|
||||
instructor override could satisfy without a schema change. No bypass button implemented.
|
||||
|
||||
## Frontend (Sections 21-23)
|
||||
|
||||
No frontend change was needed or made. `CourseView.jsx` and `CurriculumView.jsx` already
|
||||
generalized correctly to ROOT 01's activity/status shapes (lesson/demonstration/knowledge_check/
|
||||
guided_exercise/lab/reflection/assessment, `ready`/`planned` status, `is_required`,
|
||||
`instructor_notes`) — the same generic rendering already proven for ROOT 05-12 in Phase 6C.
|
||||
Verified by inspection and by the real API responses `e2e-phase6d.js` and the rehearsal exercise.
|
||||
|
||||
## New application/platform code and why necessary
|
||||
|
||||
- `Dockerfile` + `scripts/entrypoint.sh`: the nested Docker Engine described above — a genuine
|
||||
platform gap (ROOT 04's course design needs it) that the existing content model couldn't
|
||||
satisfy on its own, tested and justified in `docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md`.
|
||||
- No other application code. Every ROOT 01 requirement was satisfiable with the existing
|
||||
`course_modules`/`module_activities`/`assessment_definitions`/scenario-engine model.
|
||||
|
||||
## Bugs found and fixed
|
||||
|
||||
- An early draft of the real-Lab rehearsal's hardware-report step used a triple-nested
|
||||
JS-template/shell/heredoc string that silently produced a malformed file. Caught by actually
|
||||
running the rehearsal against a real Lab (not assumed to work); fixed by writing the script to
|
||||
a file and `docker cp`-ing it in rather than an inline heredoc.
|
||||
- Phase 6D's own content changes made two hardcoded assumptions in `e2e-phase6a.js`/
|
||||
`e2e-phase6b.js` stale: both suites asserted `resume` pointed at ROOT 05's gold module as "the
|
||||
first real content," which was true before this phase and is now ROOT 01. Both suites fixed
|
||||
and re-verified green against a fresh stack.
|
||||
|
||||
## Testing (Section 26)
|
||||
|
||||
`platform/test/e2e-phase6d.js` — 20/20 passing (fresh stack): ROOT 01 hierarchy, ROOT 02-04
|
||||
honestly empty, activity ordering, lab-cannot-be-manually-completed, full course completion via
|
||||
lesson/lab/assessment, prerequisite chain (both the locked and unlocked directions, on two
|
||||
separate students), existing ROOT 05+ and Capstone state unaffected, is_required rule
|
||||
unaffected, self-paced no-instructor-deferral check, instructor-notes exclusion, RBAC, logout/
|
||||
login persistence, cross-student isolation, version-bump stability, and content validation.
|
||||
|
||||
## Real-Lab rehearsal (Section 27)
|
||||
|
||||
`infra/scripts/drills/test-phase6d-foundations-rehearsal.js` — 11/11 passing against a real
|
||||
disposable Lab container (no mocking): login → enroll → resume → lesson/guided-exercise →
|
||||
real Lab launch → real bash hardware inspection (`nproc`/`free`/`df`, written to a real file) →
|
||||
real validation → real permission-denial-then-fix (`su - labuser`, genuinely denied, then
|
||||
genuinely allowed after `chmod 644`) → remaining modules → assessment → course completion →
|
||||
ROOT 02 unlock → logout/login persistence → instructor visibility of the resulting state.
|
||||
|
||||
## Regression (Section 30)
|
||||
|
||||
Re-ran 6A (21/21), 6B (24/24), 6C (17/17) fresh, each against its own freshly-reset stack per
|
||||
the established isolation convention — all green after the two resume-point staleness fixes
|
||||
above. 2A/2B/3A/3B/5A/5B/5C were not re-run: `git diff` for this phase shows no changes to
|
||||
`platform/api/src/`, gateway, or provisioner code (only the Lab image/entrypoint, migrations,
|
||||
content, scenarios, tests, and docs), so their surface area was untouched. Frontend build: clean
|
||||
(`vite build`, unchanged this phase). Lab image: rebuilt and standalone-smoke-tested directly
|
||||
(see the container runtime section above) in addition to the full-stack rehearsal.
|
||||
|
||||
## Content validation
|
||||
|
||||
`infra/scripts/validate-course-content.js` — 0 errors against 17 courses/modules with real
|
||||
content, 201 activities, 106 scenario files (98 + 8 new this phase).
|
||||
|
||||
## Known limitations / intentionally deferred
|
||||
|
||||
- ROOT 02, 03, 04 course content — architecture designed, not authored.
|
||||
- ROOT 04's Docker/Compose hands-on labs specifically — platform extension done and verified,
|
||||
course content not yet written against it.
|
||||
- Instructor-led vs. self-paced timing is not a separate database field (see
|
||||
`docs/ROOT_PROGRAM_DURATION_MODEL.md`).
|
||||
- No placement testing or instructor-override UI (explicitly out of scope this phase).
|
||||
|
||||
## Recommended Phase 6E scope
|
||||
|
||||
Author ROOT 02 (prioritize the "critical" Networking Mental Model module), then ROOT 03 (Git,
|
||||
using the already-verified local-repo strategy), then ROOT 04 (content only — the runtime is
|
||||
already built and tested).
|
||||
@@ -7,6 +7,17 @@ for each placement (not coverage-percentage padding). Built from
|
||||
more of demonstration/guided-exercise/reflection), **PLANNED** (structure only, no content),
|
||||
**GAP** (no suitable scenario exists at all).
|
||||
|
||||
## Phase 6D addition
|
||||
|
||||
Phase 6D authored ROOT 01 (Computing & Linux Foundations) in full — 6 modules, 35 activities, 6
|
||||
new hands-on Linux scenarios plus 2 MCQs, all READY. This is the program's first course outside
|
||||
the Kubernetes/CKS content this matrix was originally scoped to, so it is documented separately
|
||||
in `docs/ROOT_FOUNDATIONS_ARCHITECTURE.md` and `docs/PHASE6D_ROOT_FOUNDATIONS.md` rather than
|
||||
folded into the tables below, which remain the ROOT 05+ (Kubernetes) scenario-coverage record.
|
||||
Post-Phase-6D totals: **28 modules total across all courses with any content — 22 READY, 6
|
||||
PARTIAL** (Phase 6C's 22 modules — 16 READY, 6 PARTIAL — plus 6 new ROOT 01 modules, all READY).
|
||||
**106 scenario files** (98 + 8 new).
|
||||
|
||||
## Phase 6C verification (superseding Phase 6B's own self-reported counts)
|
||||
|
||||
Baseline (start of Phase 6C), run directly against the database and filesystem
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# ROOT Foundations Architecture — ROOT 01-04
|
||||
|
||||
Phase 6D §1 — the curriculum architecture for the foundational half of ROOT_, built entirely on
|
||||
the existing course/module/activity model (`docs/COURSE_CONTENT_MODEL.md`) and the existing
|
||||
scenario/assessment engines. See `docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` for why no new
|
||||
activity type or content model was needed. Every course below is a row that already exists in
|
||||
`courses` (`status='planned'`, seeded by Phase 6A's `022_course_content.sql`) — this phase adds
|
||||
their modules and activities, and updates their `status`/`objectives`/`prerequisites`.
|
||||
|
||||
## Course sequence and prerequisite chain
|
||||
|
||||
```
|
||||
01 Computing & Linux Foundations
|
||||
↓
|
||||
02 Linux Administration & Networking
|
||||
↓
|
||||
03 Git & Collaborative Development
|
||||
↓
|
||||
04 Containers & Docker
|
||||
↓
|
||||
05 Kubernetes Foundations (existing)
|
||||
```
|
||||
|
||||
Enforced server-side via `courses.prerequisites` (the same mechanism already gating
|
||||
05→06→07/09/10→12→Capstone) — not UI-hidden alone. `root-05-kubernetes-foundations.prerequisites`
|
||||
is updated this phase to add `root-04-containers-and-docker`, so a student cannot reach ROOT 05
|
||||
without the foundations chain complete. This does not touch the capstone assessment engine's own
|
||||
`unlock_rule` (still the original 6 `rootnode-path.json` stages) — course-level prerequisite
|
||||
gating and the capstone's own gate remain the two independent systems `docs/COURSE_CONTENT_MODEL.md`
|
||||
§5 and `docs/ROOT_CAPSTONE_READINESS.md` §3 already establish.
|
||||
|
||||
## Extension point for future placement/override (Step 20)
|
||||
|
||||
Not built this phase. The extension point already exists structurally: `courses.prerequisites`
|
||||
is a plain array a future placement-assessment feature could programmatically satisfy (e.g. by
|
||||
recording a passing placement score as an `activity_completions`-equivalent fact), or an
|
||||
instructor override could satisfy by directly marking an enrollment's course complete through
|
||||
the existing admin layer — neither requires a schema change to unlock. No bypass button is
|
||||
implemented; this paragraph exists so a future phase doesn't need to reopen the prerequisite
|
||||
model to add one.
|
||||
|
||||
---
|
||||
|
||||
## ROOT 01 — Computing & Linux Foundations
|
||||
|
||||
Target learner: a technically curious beginner with no assumed prior Linux/CLI experience.
|
||||
Estimated duration: ~5.5 hours self-paced / ~4 hours instructor-led (see
|
||||
`docs/ROOT_PROGRAM_DURATION_MODEL.md` for the derived total).
|
||||
|
||||
| # | Module | Outcome (verb-first) | Activities | Real Ops connection |
|
||||
|---|---|---|---|---|
|
||||
| 1 | How Computers Run Software | Explain what the OS/kernel manages (CPU, RAM, storage, processes) vs. what an application does | lesson, guided_exercise (inspect `/proc/cpuinfo`, `free -h`, `lsblk`, `ps aux` on paper first), lab (`linux-inspect-hardware` — real inspection commands), reflection | This is literally what a container later virtualizes — a Pod's resource requests/limits are promises about exactly these same resources. |
|
||||
| 2 | Linux & The Shell | Use a shell confidently: run commands, read exit codes, use history/tab-completion/man pages | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-shell-basics`), reflection | Every `kubectl exec` session later drops a student into this exact shell experience. |
|
||||
| 3 | Navigating the Filesystem | Navigate absolute/relative paths; explain what `/etc`, `/var`, `/home`, `/tmp`, `/usr`, `/proc` are for at a conceptual level | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-filesystem-navigation`), reflection | Container images later have this exact same tree — `/etc/nginx/nginx.conf` isn't a new idea, it's the same filesystem convention. |
|
||||
| 4 | Files & Text | Manipulate files and text streams; use pipes/redirection to compose commands | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-files-and-text`), reflection | `kubectl logs \| grep` later is the exact same composition skill applied to a different data source. |
|
||||
| 5 | Processes | Explain the process concept (PID, foreground/background, signals); inspect and terminate a real process | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-process-management`), reflection | A container IS (mostly) one isolated process tree — this module is the direct prerequisite for understanding what a container "contains." |
|
||||
| 6 | Users & Permissions Foundations | Explain ownership and rwx permissions; use `chmod`/`sudo` correctly; state the least-privilege principle | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-permissions-foundations`), reflection | Direct root of ROOT 09's SecurityContext/RBAC — "least privilege" is introduced here first, in a context students can literally see and touch. |
|
||||
|
||||
Assessment: `rootnode-linux-foundations-assessment` (knowledge + state-validation, see §"Assessments" below).
|
||||
|
||||
## ROOT 02 — Linux Administration & Networking
|
||||
|
||||
Prerequisite: ROOT 01 complete. Target learner: comfortable with the ROOT 01 shell/filesystem/
|
||||
process/permissions basics, ready to reason about services and networks.
|
||||
|
||||
| # | Module | Outcome | Activities | Real Ops connection |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Linux System Administration | Explain packages/services/systemd conceptually; read logs; use environment variables and config files | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-admin-services-logs`), reflection | A Kubernetes controller IS a systemd-like supervisor pattern generalized across a cluster — "restart on failure" isn't new in ROOT 06, it's a concept students meet here first. |
|
||||
| 2 | Storage & Filesystems | Explain block devices/partitions/mounts conceptually; distinguish persistent vs. temporary data; inspect real disk usage | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-storage-inspection`), reflection | Direct root of ROOT 08's PV/PVC model and ROOT 04's bind-mount/volume distinction. |
|
||||
| 3 | Networking Mental Model | Explain hosts/interfaces/IP/subnet/gateway/DNS/ports/TCP-vs-UDP/client-server at a conceptual level | lesson, demonstration, knowledge_check, guided_exercise, reflection (conceptual module — no lab; see note below) | **Critical module** — the direct prerequisite for reasoning about Pod IPs, Services, Ingress, NetworkPolicy, and DNS later. |
|
||||
| 4 | Linux Network Inspection | Use `ip addr`/`ip route`/`ss`/`ping`/`curl`/`dig` to answer a specific diagnostic question, not just run syntax | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-network-inspection`), reflection | Exactly the toolkit ROOT 10's troubleshooting framework assumes a student already has. |
|
||||
| 5 | DNS & HTTP | Trace a request: browser → DNS → IP → TCP → HTTP → server → response | lesson, demonstration, knowledge_check, guided_exercise, lab (`linux-dns-http-trace`), reflection | The exact conceptual bridge `docs/ROOT_PROGRAM_ARCHITECTURE.md` names for later Kubernetes Services/Ingress/DNS. |
|
||||
| 6 | Troubleshooting Linux Systems | Apply Observe→Evidence→Hypothesize→Test→Repair→Validate→Prevent to a Linux-level failure | lesson, demonstration, guided_exercise, lab (`linux-troubleshooting-framework`), reflection | Literally the same framework `root-10-m03-troubleshooting-framework` already teaches — introduced here first, at the Linux level, so it "already feels natural" by ROOT 10 (Step 15's explicit goal). |
|
||||
|
||||
Module 3 note: per the readiness standard's "sequence should match the subject," a purely
|
||||
conceptual mental-model module earns READY through demonstrated reasoning (its knowledge check
|
||||
and guided exercise), not a forced lab — mirroring `root-05-m00`'s own precedent. Module 4
|
||||
immediately after it is where the same concepts become hands-on.
|
||||
|
||||
Assessment: `rootnode-linux-networking-assessment`.
|
||||
|
||||
## ROOT 03 — Git & Collaborative Development
|
||||
|
||||
Prerequisite: ROOT 02 complete. Uses only local repositories (a working tree + a local bare
|
||||
"remote") inside the student's own Lab — no Gitea account, no network credential, no
|
||||
RootNode-canonical-repo access ever reaches a student Lab (`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md`
|
||||
§4).
|
||||
|
||||
| # | Module | Outcome | Activities | Real Ops connection |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Why Version Control Exists | Explain what problem version control solves (history, collaboration, reproducibility, source of truth) | lesson, knowledge_check, reflection | Sets up "Git as source of truth" — the exact idea GitOps (ROOT 11) generalizes. |
|
||||
| 2 | Git Mental Model | Explain working tree / staging area / repository / commit / branch / remote as a model, before commands | lesson, demonstration, knowledge_check, guided_exercise, reflection | Same "model before commands" pedagogy as ROOT 05's mental-model module. |
|
||||
| 3 | Everyday Git | Use `status`/`add`/`commit`/`log`/`diff` to make and inspect real changes | lesson, demonstration, knowledge_check, guided_exercise, lab (`git-everyday-basics`), reflection | Every future IaC/GitOps workflow assumes this fluency as a given. |
|
||||
| 4 | Branching | Create branches, switch between them, merge, resolve a simple conflict | lesson, demonstration, knowledge_check, guided_exercise, lab (`git-branching-and-merge`), reflection | Direct root of CI/CD's branch-triggered pipelines (future ROOT 11 content). |
|
||||
| 5 | Remote Repositories | Clone/fetch/pull/push against a real local-bare remote; explain `origin` | lesson, demonstration, knowledge_check, guided_exercise, lab (`git-remote-clone-push`), reflection | Direct root of GitOps's "push triggers deployment" model. |
|
||||
| 6 | Engineering Workflow | Write meaningful commits; explain why `main` is protected and why rollback via history matters | lesson, demonstration, knowledge_check, reflection | Explicit bridge to ROOT 11's "Git as source of truth for infrastructure" framing. |
|
||||
|
||||
Assessment: `rootnode-git-fundamentals-assessment`.
|
||||
|
||||
## ROOT 04 — Containers & Docker
|
||||
|
||||
Prerequisite: ROOT 03 complete. Uses the nested Docker Engine
|
||||
(`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` §3) — the direct bridge into Kubernetes.
|
||||
|
||||
| # | Module | Outcome | Activities | Real Ops connection |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Why Containers Exist | Explain the packaging/dependency-isolation/repeatability problem containers solve; contrast VM vs. container | lesson, demonstration, knowledge_check, reflection | Sets up "Kubernetes orchestrates what Docker already isolates" — Module 8's thesis. |
|
||||
| 2 | Images & Containers | Explain image/layer/container/registry/tag as a model | lesson, demonstration, knowledge_check, guided_exercise, reflection | A Kubernetes Pod's `image:` field is this exact concept, unchanged. |
|
||||
| 3 | Docker CLI | Pull/run/inspect/log/exec/stop/remove a real container | lesson, demonstration, knowledge_check, guided_exercise, lab (`docker-cli-basics`), reflection | `kubectl exec`/`kubectl logs` are the same actions against a different orchestrator. |
|
||||
| 4 | Container Networking | Explain host-vs-container port, publish a port, reach one container from another | lesson, demonstration, knowledge_check, guided_exercise, lab (`docker-container-networking`), reflection | Direct root of Pod IPs/Services — builds explicitly on ROOT 02 M3/M4. |
|
||||
| 5 | Container Storage | Explain ephemeral container filesystem vs. bind mounts vs. volumes | lesson, demonstration, knowledge_check, guided_exercise, lab (`docker-container-storage`), reflection | Direct root of Kubernetes PVC — builds explicitly on ROOT 02 M2. |
|
||||
| 6 | Dockerfile & Image Build | Write a Dockerfile (`FROM`/`RUN`/`COPY`/`WORKDIR`/`CMD`) and build a real image | lesson, demonstration, knowledge_check, guided_exercise, lab (`docker-build-image`), reflection | Direct root of CI/CD's build step (future ROOT 11 content). |
|
||||
| 7 | Docker Compose | Define and run a real multi-service app (two services, one network, one volume) | lesson, demonstration, knowledge_check, guided_exercise, lab (`docker-compose-multiservice`), reflection | Direct conceptual root of a Kubernetes multi-Pod application. |
|
||||
| 8 | Containers → Kubernetes | Explain what Kubernetes adds on top of what Docker already does (scheduling, desired state, controllers, service discovery, health management, scaling, rollout) | lesson, knowledge_check, reflection | The explicit bridge module — a student should be able to answer "why Kubernetes" in their own words entering ROOT 05. |
|
||||
|
||||
Assessment: `rootnode-containers-assessment`.
|
||||
|
||||
## Assessments (Step 18)
|
||||
|
||||
Four new `assessment_definitions` rows, one per course, using the existing Phase 3B engine
|
||||
exactly as `rootnode-security-assessment` (Phase 6C) does — mixing knowledge-recall scenarios
|
||||
(`type: mcq`) with real state-validation scenarios (`type: task`, the same `runCommand`-backed
|
||||
mechanism every lab already uses). No KubeKosh/Kubernetes scenario is force-fit into a
|
||||
non-Kubernetes assessment. See `docs/PHASE6D_ROOT_FOUNDATIONS.md` for which are actually wired
|
||||
this phase vs. deferred.
|
||||
|
||||
## Duration model
|
||||
|
||||
See `docs/ROOT_PROGRAM_DURATION_MODEL.md` for the derived instructional-hour estimate — computed
|
||||
from actual `estimated_duration_minutes` values on real activities, not invented.
|
||||
@@ -0,0 +1,117 @@
|
||||
# ROOT Foundations Lab Strategy
|
||||
|
||||
Phase 6D §7/§10 — what the existing disposable Lab can safely teach for ROOT 01-04, evaluated
|
||||
by actually reading `backend/server.js` and testing inside a real container, not assumed.
|
||||
|
||||
## 1. The scenario engine is already generic — no new activity type needed
|
||||
|
||||
`POST /api/scenarios/:id/setup` and `POST /api/scenarios/:id/validate` (`backend/server.js`)
|
||||
run `setup_commands[].command` / `validation.commands[].command` through `runCommand()`, which
|
||||
is a bare `child_process.exec(cmd)` — an arbitrary shell command string, checked against
|
||||
`expected_output` by `checkMatch()` (exact/contains/not_contains/regex). **Nothing about this
|
||||
is Kubernetes-specific.** Every existing scenario just happens to write `kubectl ...` as its
|
||||
command string. `isolation_type` (`shared_namespace` etc.) is pure descriptive metadata — grepped
|
||||
across the entire backend and frontend, it gates no actual behavior.
|
||||
|
||||
This means the existing `lab` activity type (`module_activities.activity_type = 'lab'`,
|
||||
`completion_behavior = 'auto_scenario'`) and the existing `scenarios/data/*.json` format already
|
||||
fully support Linux, networking-inspection, and Git exercises with real, deterministic
|
||||
validation — a scenario file with `"command": "test -f /root/lab/notes.txt && echo exists"` is
|
||||
exactly as valid as one with `"command": "kubectl get pod ..."`. **No `shell_lab`/`git_lab`/
|
||||
`container_lab` activity type is built or needed.** The speculative multi-type model the
|
||||
authorization raised as a possibility is unnecessary — it would duplicate a generic mechanism
|
||||
that already exists.
|
||||
|
||||
The only wrinkle: `runCommand` does not pass a `cwd`, so every command runs wherever the backend
|
||||
process's cwd is; multi-step Linux/Git scenarios must `cd /root/lab-<id> && ...` inside each
|
||||
command string (exactly how a real student would chain commands), not rely on shell state
|
||||
persisting between `setup` and `validate` calls beyond what the filesystem/git/process state
|
||||
itself carries.
|
||||
|
||||
## 2. What the Lab image already contains (verified from the real `Dockerfile`)
|
||||
|
||||
Ubuntu 22.04 base with `curl wget git vim nano jq bash-completion iproute2 iptables
|
||||
iputils-ping procps htop mount kmod python3 nginx` already installed, plus k3s (bundling
|
||||
`kubectl`/`crictl`/containerd) started by `scripts/entrypoint.sh`. The student terminal execs
|
||||
into this same container via node-pty — there is no separate "Linux sandbox," it's the same
|
||||
shell k3s runs in.
|
||||
|
||||
**Already usable with zero image changes:** shell navigation, file manipulation (`cp`/`mv`/`rm`/
|
||||
`cat`/`grep`/pipes/redirection), process inspection (`ps`/`top`/`kill`), permissions
|
||||
(`chmod`/`chown`/`sudo` — the container runs as root, so a permission-teaching scenario should
|
||||
`useradd` a second user in its `setup_commands` to demonstrate a real permission denial), and
|
||||
network inspection (`ip addr`/`ip route`/`ss`/`ping`/`curl`).
|
||||
|
||||
**Missing, needed for ROOT 02 Module 5 (DNS & HTTP):** `dig`/`nslookup` are NOT installed
|
||||
(`dnsutils`/`bind9-dnsutils` is absent from the current package list). This is a genuinely small,
|
||||
justified Dockerfile addition (one more `apt-get install` package, same risk class as everything
|
||||
else already there) — not a new mechanism. `curl` alone already covers the HTTP half.
|
||||
|
||||
## 3. Container runtime decision — tested, not assumed
|
||||
|
||||
**Decision: install real Docker Engine (`docker.io`) inside the same already-privileged Lab
|
||||
container, alongside k3s, as a second independent container runtime with its own nested
|
||||
`/var/run/docker.sock`.** Podman was not chosen — Docker Engine matches the actual ROOT 04
|
||||
curriculum content (Docker CLI, Dockerfile, Compose terminology) and required no new privilege.
|
||||
|
||||
**Why this doesn't weaken the security boundary:** the student Lab container is already
|
||||
launched with `Privileged: true` (`platform/provisioner/src/lifecycle.js` — required for k3s,
|
||||
already the accepted threat model per `docs/LAB_ARCHITECTURE.md` §7: the Lab Host VM, not the
|
||||
container boundary, is what actually contains a student). A second container runtime running
|
||||
*inside* an already-fully-privileged guest adds no new host capability. **The Lab Host's own
|
||||
Docker socket is never touched or exposed** — the nested `dockerd` started inside the guest
|
||||
listens on its own `/var/run/docker.sock` *inside that guest's filesystem namespace*, entirely
|
||||
separate from the Lab Host's real Docker daemon that the Provisioner itself talks to.
|
||||
|
||||
**Tested empirically** (not assumed) against the exact production resource caps
|
||||
(`resource-policy.yaml`: 2 vCPU, 4096MB memory, 512 PIDs, `Privileged: true`): built
|
||||
`rootnode-academy:test` fresh from the real `Dockerfile`, layered `docker.io` on top, launched
|
||||
it under those exact `docker run` constraints, and ran real workloads inside:
|
||||
|
||||
| Step | Result | Memory | PIDs |
|
||||
|---|---|---|---|
|
||||
| k3s alone, ready | ✓ | 357MB | 36 |
|
||||
| + `dockerd --storage-driver=vfs` started | ✓ | — | — |
|
||||
| `docker run --rm hello-world` | ✓ passed | — | — |
|
||||
| `docker run -d -p 8080:80 nginx:alpine`, `curl localhost:8080` | ✓ 200, real HTML returned | 866MB | 159 |
|
||||
| `docker build` a real 2-line Dockerfile, `docker run` the result | ✓ passed | 884MB | 160 |
|
||||
|
||||
Comfortable headroom remained in all three dimensions (884MB/4096MB, 160/512 PIDs, CPU settled
|
||||
after the image pull). `--storage-driver=vfs` was used deliberately — overlay2-on-overlay2
|
||||
(the Lab Host's own overlay filesystem, nested inside another overlay driver) is a known source
|
||||
of instability in Docker-in-Docker setups; vfs is slower per-layer but has no such nesting
|
||||
hazard, and ROOT 04's exercises are small single-purpose images, not build-heavy CI workloads.
|
||||
|
||||
**`docker compose` is NOT included by `docker.io`** — Ubuntu's packaged Docker CLI does not ship
|
||||
the Compose v2 plugin. Fixed the same way the Dockerfile already fetches k3s: download the
|
||||
static `docker-compose` v2 binary directly (matching the existing "curl a static binary rather
|
||||
than add a whole package repository" pattern already used for k3s) into
|
||||
`/usr/local/lib/docker/cli-plugins/docker-compose` so `docker compose` works as a CLI subcommand.
|
||||
|
||||
**What was NOT done:** no change to the Provisioner, Lab Router, or Gateway. No change to
|
||||
`HostConfig` (still `Privileged: true`, same CPU/memory/PIDs caps as every other Lab). No mount
|
||||
of the Lab Host's `/var/run/docker.sock` into any container — verified this was never written
|
||||
anywhere in this phase's changes.
|
||||
|
||||
## 4. Git strategy
|
||||
|
||||
`git` is already installed. All ROOT 03 exercises use **local repositories created inside the
|
||||
student's own Lab** (`git init` in a scenario's `setup_commands`) — no Gitea account, no network
|
||||
credentials, no dependency on RootNode's own canonical repository ever touching a student Lab.
|
||||
A later module ("Remote Repositories") teaches `clone`/`fetch`/`pull`/`push`/`origin` conceptually
|
||||
and against a **second local bare repository** created in the same Lab (`git init --bare`,
|
||||
`git remote add origin /root/lab-remote.git`) to give students a genuine push/pull/clone
|
||||
round-trip without needing any real network service, credential, or RootNode-adjacent
|
||||
infrastructure — Step 9's explicit constraint.
|
||||
|
||||
## 5. Classification summary
|
||||
|
||||
| Capability | Classification |
|
||||
|---|---|
|
||||
| Shell/filesystem/process/permissions exercises | **SUPPORTED NOW** — zero image or code change |
|
||||
| Network inspection (`ip`, `ss`, `ping`, `curl`) | **SUPPORTED NOW** |
|
||||
| DNS tooling (`dig`/`nslookup`) | **REQUIRES SMALL PLATFORM EXTENSION** — one apt package added to the Lab Dockerfile |
|
||||
| Git exercises (local + local-bare-remote) | **SUPPORTED NOW** — zero image or code change |
|
||||
| Docker CLI / image build / single containers | **REQUIRES SMALL PLATFORM EXTENSION** — `docker.io` + entrypoint change to start `dockerd`, tested and proven safe/performant above |
|
||||
| Docker Compose | **REQUIRES SMALL PLATFORM EXTENSION** — static `docker-compose` v2 binary, same pattern as k3s's own binary fetch |
|
||||
| A distinct `shell_lab`/`git_lab`/`container_lab` activity type | **NOT NEEDED** — the existing generic `lab` + scenario-JSON model already covers all of the above |
|
||||
@@ -0,0 +1,65 @@
|
||||
# ROOT Foundations Pilot Readiness — ROOT 01-04
|
||||
|
||||
Phase 6D §24 — the honest READY/PARTIAL/DEFERRED classification for ROOT 01-04, using the same
|
||||
bar `docs/ROOT_PILOT_READINESS_STANDARD.md` established in Phase 6C. The standard is not
|
||||
lowered because these courses are new.
|
||||
|
||||
## ROOT 01 — Computing & Linux Foundations: READY
|
||||
|
||||
All 6 modules meet the 8-point standard:
|
||||
|
||||
| Module | Status | Evidence |
|
||||
|---|---|---|
|
||||
| M1 How Computers Run Software | READY | Lesson, guided exercise, real hands-on lab (`linux-inspect-hardware`), reflection. Verified via `e2e-phase6d.js` and the real-Lab rehearsal (`infra/scripts/drills/test-phase6d-foundations-rehearsal.js`). |
|
||||
| M2 Linux and the Shell | READY | Full gold pattern (lesson/demo/kc/guided/lab/reflection). Lab (`linux-shell-basics`) tested end-to-end via the real scenario-validation API, including a negative test (fails when unsolved). |
|
||||
| M3 Navigating the Filesystem | READY | Full gold pattern. Lab (`linux-filesystem-navigation`) tested end-to-end. |
|
||||
| M4 Files and Text | READY | Full gold pattern. Lab (`linux-files-and-text`) tested end-to-end. |
|
||||
| M5 Processes | READY | Full gold pattern. Lab (`linux-process-management`) tested end-to-end, including a negative test (fails while the process is still running). |
|
||||
| M6 Users & Permissions Foundations | READY | Full gold pattern. Lab (`linux-permissions-foundations`) tested end-to-end via the real-Lab rehearsal, including a REAL permission denial (a second Linux user genuinely denied read access) before the fix. |
|
||||
|
||||
**Self-paced readiness:** confirmed programmatically — `infra/scripts/validate-course-content.js`'s
|
||||
deferral-phrase check and `e2e-phase6d.js`'s equivalent live check both scan every ROOT 01
|
||||
activity's student-visible content for "ask your instructor" or equivalent and found none.
|
||||
|
||||
**Instructor-led readiness:** every lesson/demonstration/guided_exercise/reflection activity has
|
||||
a real Instructor Notes section (timing, misconceptions, discussion questions), verified hidden
|
||||
from students both statically and via a live API check.
|
||||
|
||||
**Assessment:** `rootnode-linux-foundations-assessment` wired and verified live (real draw: 2
|
||||
required hands-on tasks + 2 random hands-on tasks + 2 random MCQs, 6 scenarios total).
|
||||
|
||||
## ROOT 02 — Linux Administration & Networking: DEFERRED
|
||||
|
||||
Architecture fully designed in `docs/ROOT_FOUNDATIONS_ARCHITECTURE.md` (6 modules, including the
|
||||
explicitly "critical" Networking Mental Model module). Zero modules authored this phase — course
|
||||
remains `status = 'planned'`, honestly, with a real (not fabricated) `prerequisites` gate to
|
||||
ROOT 01 already in place so the sequence is correct once it IS authored.
|
||||
|
||||
## ROOT 03 — Git & Collaborative Development: DEFERRED
|
||||
|
||||
Architecture designed (6 modules, local-repository-only strategy documented in
|
||||
`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` §4). Zero modules authored this phase.
|
||||
|
||||
## ROOT 04 — Containers & Docker: DEFERRED (content) / PLATFORM EXTENSION DONE
|
||||
|
||||
Architecture designed (8 modules). **The platform extension ROOT 04 needs — a nested Docker
|
||||
Engine inside the Lab — was built and verified this phase** (`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md`
|
||||
§3: real `docker run`, `docker build`, and `docker compose` all tested working inside the actual
|
||||
`rootnode-academy:test` image under production resource constraints). No course content was
|
||||
authored against it yet — that's a real, substantial content-authoring effort left for a future
|
||||
phase, now unblocked by this phase's platform work.
|
||||
|
||||
## Why ROOT 02-04 were not authored this phase
|
||||
|
||||
Given realistic effort constraints in a single phase, authoring, testing, and verifying ROOT 01
|
||||
to the same rigor as every ROOT 05+ module (real scenarios, live end-to-end verification, a
|
||||
real-Lab rehearsal, an assessment) was prioritized over shallow architecture-only coverage
|
||||
across all four courses. This mirrors Phase 6C's own precedent: some content built to a genuine
|
||||
READY bar, the rest explicitly and honestly DEFERRED with a real reason, rather than every
|
||||
course padded to look further along than it is (`e2e-phase6d.js`'s own first test explicitly
|
||||
asserts ROOT 02-04 have zero modules, catching any future accidental over-claim).
|
||||
|
||||
## Honest totals
|
||||
|
||||
**READY: 1 course (ROOT 01), 6 modules. DEFERRED: 3 courses (ROOT 02-04), 0 modules — by
|
||||
design, not oversight.**
|
||||
@@ -21,10 +21,10 @@ future phase pending an authored teaching layer for each. See Section 16 discuss
|
||||
|
||||
| Course | Pilot status | Notes |
|
||||
|---|---|---|
|
||||
| ROOT 01 Computing & Linux Foundations | **NOT IN PILOT** | Never authored (status `planned`, zero modules). Out of Phase 6C's scope; students are assumed to arrive with basic Linux/CLI comfort for this pilot. |
|
||||
| ROOT 02 Linux Administration & Networking | **NOT IN PILOT** | Same as above. |
|
||||
| ROOT 03 Git & Collaborative Development | **NOT IN PILOT** | Same as above. |
|
||||
| ROOT 04 Containers & Docker | **NOT IN PILOT** | Same as above. |
|
||||
| ROOT 01 Computing & Linux Foundations | **IN PILOT** (added Phase 6D) | Fully authored, READY — 6 modules, real hands-on Linux Labs, a wired assessment. See `docs/PHASE6D_ROOT_FOUNDATIONS.md`. |
|
||||
| ROOT 02 Linux Administration & Networking | **NOT IN PILOT** | Architecture designed (`docs/ROOT_FOUNDATIONS_ARCHITECTURE.md`), content not yet authored. |
|
||||
| ROOT 03 Git & Collaborative Development | **NOT IN PILOT** | Architecture designed, content not yet authored. |
|
||||
| ROOT 04 Containers & Docker | **NOT IN PILOT** | Architecture designed; its platform extension (nested Docker Engine) is built and tested, content not yet authored. |
|
||||
| ROOT 05 Kubernetes Foundations | **IN PILOT** | See modules below. |
|
||||
| ROOT 06 Kubernetes Application Workloads | **IN PILOT** | See modules below. |
|
||||
| ROOT 07 Kubernetes Networking & Configuration | **IN PILOT** | See modules below. |
|
||||
@@ -37,6 +37,16 @@ future phase pending an authored teaching layer for each. See Section 16 discuss
|
||||
|
||||
## Module-level detail (courses IN pilot only)
|
||||
|
||||
### ROOT 01 — Computing & Linux Foundations (added Phase 6D)
|
||||
| Module | Status | Prerequisite | Outcome | Delivery | Required/Optional | Labs/Assessment |
|
||||
|---|---|---|---|---|---|---|
|
||||
| root-01-m01-how-computers-run-software | READY | none (program entry point) | Explain the hardware/kernel/application layering; inspect a real machine's resources | Self-paced or instructor-led | 4 required | 1 lab |
|
||||
| root-01-m02-linux-and-the-shell | READY | m01 | Read exit codes; use tab completion/history/man pages | Self-paced or instructor-led | 6 required | 1 lab |
|
||||
| root-01-m03-navigating-the-filesystem | READY | m02 | Navigate absolute/relative paths; explain top-level directories | Self-paced or instructor-led | 6 required | 1 lab |
|
||||
| root-01-m04-files-and-text | READY | m03 | Manipulate files; filter text with grep/pipes/redirection | Self-paced or instructor-led | 6 required | 1 lab |
|
||||
| root-01-m05-processes | READY | m04 | Inspect and correctly terminate a real process | Self-paced or instructor-led | 6 required | 1 lab |
|
||||
| root-01-m06-users-and-permissions-foundations | READY | m05 | Apply rwx permissions and the least-privilege principle | Self-paced or instructor-led | 7 required | 1 lab + **ROOT 01 Linux Foundations Assessment** (6-scenario timed exam) |
|
||||
|
||||
### ROOT 05 — Kubernetes Foundations
|
||||
| Module | Status | Prerequisite | Outcome | Delivery | Required/Optional | Labs/Assessment |
|
||||
|---|---|---|---|---|---|---|
|
||||
@@ -99,7 +109,8 @@ instructor-only prep material was created outside the per-activity Instructor No
|
||||
|
||||
## What is intentionally NOT in the first pilot
|
||||
|
||||
- ROOT 01-04 (never authored).
|
||||
- ROOT 02-04 (architecture designed in Phase 6D, content not yet authored — ROOT 01 IS now in
|
||||
the pilot, added Phase 6D).
|
||||
- ROOT 08 Kubernetes Storage & Scheduling (non-capstone-gating; both modules remain PARTIAL).
|
||||
- ROOT 11 modules 2-7 (GitOps, CI/CD, IaC, hands-on Observability, hands-on Reliability) — see
|
||||
scope doc.
|
||||
@@ -112,7 +123,8 @@ instructor-only prep material was created outside the per-activity Instructor No
|
||||
|
||||
## Bottom line
|
||||
|
||||
Every module the pilot's required completion path depends on (ROOT 05-07, 09-10, 12, Capstone)
|
||||
meets the READY bar for instructor-led delivery. Three specific modules run instructor-led only
|
||||
this pilot. ROOT 08 and most of ROOT 11 are explicitly out of the required path. Given that
|
||||
scope, the pilot curriculum, delivered instructor-led, is ready.
|
||||
Every module the pilot's required completion path depends on (ROOT 01, 05-07, 09-10, 12,
|
||||
Capstone) meets the READY bar for instructor-led delivery, and ROOT 01's self-paced readiness
|
||||
was verified explicitly (see `docs/PHASE6D_ROOT_FOUNDATIONS.md`). Three specific modules run
|
||||
instructor-led only this pilot. ROOT 02-04, ROOT 08, and most of ROOT 11 are explicitly out of
|
||||
the required path. Given that scope, the pilot curriculum, delivered instructor-led, is ready.
|
||||
|
||||
@@ -14,14 +14,21 @@ here is grounded in `docs/ROOT_CURRICULUM_INVENTORY.md`, not aspirational.
|
||||
6 courses gained new teaching modules — the module-level detail and evidence for every new
|
||||
placement lives in `docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md`, not duplicated here.
|
||||
|
||||
**Updated for Phase 6D** (`platform/api/migrations/025_phase6d_root_foundations_root01.sql`,
|
||||
`docs/PHASE6D_ROOT_FOUNDATIONS.md`): ROOT 01 fully authored and moved to READY — see
|
||||
`docs/ROOT_FOUNDATIONS_ARCHITECTURE.md` for its module-level design and
|
||||
`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` for why no new activity type was needed. ROOT 02-04
|
||||
remain PLANNED (architecture designed, content not yet authored) but now carry a real
|
||||
prerequisite chain (01→02→03→04→05) enforced server-side.
|
||||
|
||||
## Course sequence
|
||||
|
||||
| # | Course | Status | Why |
|
||||
|---|---|---|---|
|
||||
| 1 | Computing & Linux Foundations | **PLANNED** | No lab content exists in this repository — every scenario is Kubernetes/CKS. |
|
||||
| 2 | Linux Administration & Networking | **PLANNED** | Same — no Linux-admin scenario content exists. |
|
||||
| 3 | Git & Collaborative Development | **PLANNED** | No Git scenario content exists. |
|
||||
| 4 | Containers & Docker | **PLANNED** | No Docker-specific scenario content exists (the Lab itself runs k3s, but no student-facing "learn Docker" scenarios exist). |
|
||||
| 1 | Computing & Linux Foundations | **READY** | Phase 6D: 6 modules, real Linux/shell scenarios using the existing generic scenario engine, a wired assessment — see `docs/PHASE6D_ROOT_FOUNDATIONS.md`. |
|
||||
| 2 | Linux Administration & Networking | **PLANNED** | Architecture designed (`docs/ROOT_FOUNDATIONS_ARCHITECTURE.md`); content not yet authored. Real prerequisite: ROOT 01 complete. |
|
||||
| 3 | Git & Collaborative Development | **PLANNED** | Architecture designed; content not yet authored. Real prerequisite: ROOT 02 complete. |
|
||||
| 4 | Containers & Docker | **PLANNED** | Architecture designed; the nested-Docker-Engine platform extension it needs was built and verified this phase (`docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md` §3), course content not yet authored. Real prerequisite: ROOT 03 complete. |
|
||||
| 5 | Kubernetes Foundations | **PARTIAL** | Phase 6B added a READY front-door module (`root-05-m00-kubernetes-mental-model` — full lesson/demo/knowledge-check/guided-exercise/lab/reflection) sequenced before the existing 20-scenario bare-lab bank (`root-05-m01`), which now sits behind it. |
|
||||
| 6 | Kubernetes Application Workloads | **PARTIAL** | Phase 6B added a READY `root-06-m02-deployments-scaling-updates` and a PARTIAL `root-06-m03-jobs-cronjobs-daemonsets`, both in front of the existing bare-lab bank. |
|
||||
| 7 | Kubernetes Networking & Configuration | **PARTIAL** | Phase 6B added a READY `root-07-m03-services-and-service-discovery` and a PARTIAL `root-07-m04-ingress-networkpolicy-foundations`. The existing configuration bank (`root-07-m02`) still has no authored teaching content — an honest, documented gap. |
|
||||
@@ -60,10 +67,13 @@ one complete module (the gold standard) as the authoring template, not a finishe
|
||||
CAPSTONE — Operate & Recover
|
||||
```
|
||||
|
||||
01-04 have no prerequisite chain enforced against each other yet (all PLANNED, zero content —
|
||||
locking a PLANNED course behind another PLANNED course would just be decorative). 05 is the
|
||||
first course with a real prerequisite gate. See `docs/COURSE_CONTENT_MODEL.md`'s "prerequisite/
|
||||
locking rule" for why a PLANNED/content-free prerequisite never actually blocks anything.
|
||||
**Updated for Phase 6D:** 01→02→03→04→05 is now a real, server-side prerequisite chain
|
||||
(`courses.prerequisites`). Since ROOT 01 is now real content and 02-04 remain PLANNED, ROOT 02
|
||||
is the first course this chain actually locks (behind ROOT 01); 03/04/05 stay vacuously
|
||||
satisfied until their own prerequisite course is authored — see
|
||||
`docs/COURSE_CONTENT_MODEL.md`'s "prerequisite/locking rule" for why a PLANNED/content-free
|
||||
prerequisite never actually blocks anything, and `docs/PHASE6D_ROOT_FOUNDATIONS.md` for the live
|
||||
verification of this behavior on both a completed and a fresh student.
|
||||
|
||||
## Deviations from the spec's suggested sequence, and why
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# ROOT_ Program Duration Model
|
||||
|
||||
Phase 6D §25 — the actual instructional-hour estimate, computed by summing real
|
||||
`module_activities.estimated_duration_minutes` values from the live database, not invented or
|
||||
marketed backward from a target number. Run: `SELECT c.course_id, SUM(a.estimated_duration_minutes) ...
|
||||
GROUP BY c.course_id` against the current schema.
|
||||
|
||||
## Per-course totals (self-paced estimate)
|
||||
|
||||
| Course | Activities | Minutes | Hours |
|
||||
|---|---|---|---|
|
||||
| ROOT 01 Computing & Linux Foundations | 35 | 338 | 5.6 |
|
||||
| ROOT 02 Linux Administration & Networking | 0 | 0 | 0 (not yet authored) |
|
||||
| ROOT 03 Git & Collaborative Development | 0 | 0 | 0 (not yet authored) |
|
||||
| ROOT 04 Containers & Docker | 0 | 0 | 0 (not yet authored) |
|
||||
| ROOT 05 Kubernetes Foundations | 28 | 403 | 6.7 |
|
||||
| ROOT 06 Kubernetes Application Workloads | 22 | 295 | 4.9 |
|
||||
| ROOT 07 Kubernetes Networking & Configuration | 27 | 353 | 5.9 |
|
||||
| ROOT 08 Kubernetes Storage & Scheduling | 15 | 250 | 4.2 |
|
||||
| ROOT 09 Kubernetes Security | 40 | 745 | 12.4 |
|
||||
| ROOT 10 Kubernetes Troubleshooting & Observability | 20 | 277 | 4.6 |
|
||||
| ROOT 11 DevOps & Platform Engineering | 3 | 35 | 0.6 |
|
||||
| ROOT 12 Real Ops | 6 | 115 | 1.9 |
|
||||
| ROOT Capstone | 5 | 120 | 2.0 |
|
||||
| **Total (all authored content)** | **201** | **2,931** | **48.9** |
|
||||
|
||||
## Self-paced vs. instructor-led
|
||||
|
||||
The figures above are self-paced estimates — the sum of each activity's own
|
||||
`estimated_duration_minutes`, which is how long a student working alone is expected to need for
|
||||
that specific lesson/lab/reflection. This is the one number this phase can honestly derive
|
||||
programmatically.
|
||||
|
||||
**Instructor-led classroom time is NOT separately tracked as a distinct database field** — every
|
||||
Instructor Notes section already documents approximate live-facilitation timing in prose (e.g.
|
||||
"10 minutes live discussion; 8 minutes self-paced writing"), but these are per-activity
|
||||
estimates embedded in Markdown, not a summable column. A future phase could add a distinct
|
||||
`estimated_duration_minutes_instructor_led` column if precise classroom scheduling becomes a
|
||||
real, measured need — this phase does not invent that precision now. As a conservative
|
||||
approximation: instructor-led delivery of the currently-authored curriculum runs roughly the
|
||||
same total or somewhat longer than the self-paced figure above, once live discussion and
|
||||
demonstration time (documented per-activity) is added — do not treat 48.9 hours as a hard
|
||||
classroom-scheduling number without reading the relevant Instructor Notes first.
|
||||
|
||||
## What this is NOT
|
||||
|
||||
Not a marketing duration, not a promise, not a certification-hours figure. `estimated_duration_minutes`
|
||||
values were themselves authored as honest guesses (see `docs/COURSE_AUTHORING_STANDARD.md` §4)
|
||||
— this total will get more accurate as real completion-time data accumulates from actual
|
||||
students, not before.
|
||||
|
||||
## Once ROOT 02-04 are authored
|
||||
|
||||
The foundational half of ROOT_ (01-04) is planned at ~6 modules each (see
|
||||
`docs/ROOT_FOUNDATIONS_ARCHITECTURE.md`) — ROOT 01's actual authored total (5.6 hours across 6
|
||||
modules) is the only real data point so far; ROOT 02-04 should be expected to land in a similar
|
||||
5-7 hour range each once authored, giving a rough eventual ROOT 01-04 total near 20-25 hours,
|
||||
and a full ROOT_ program total near 65-75 hours — stated here as a planning estimate, explicitly
|
||||
not yet real data, and should be replaced with the real computed total once those three courses
|
||||
are actually built.
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Orchestrates the Phase 6D §27 ROOT Foundations dress rehearsal: brings up the real dev stack,
|
||||
# bootstraps an owner, runs test-phase6d-foundations-rehearsal.js (real disposable Lab, real
|
||||
# bash, real permission denial/fix, real instructor visibility check), tears down.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/../../../platform"
|
||||
|
||||
export PROVISIONER_SHARED_SECRET="${PROVISIONER_SHARED_SECRET:-rehearsal-shared-secret-not-for-production}"
|
||||
export INTEGRATION_SHARED_SECRET="${INTEGRATION_SHARED_SECRET:-dev-integration-secret-not-for-production}"
|
||||
export LAB_IMAGE_REF="${LAB_IMAGE_REF:-rootnode-academy:test}"
|
||||
|
||||
docker compose -f docker-compose.dev.yml up -d --build
|
||||
|
||||
cleanup() {
|
||||
echo "==> Tearing down"
|
||||
docker ps -aq --filter "name=rootnode-lab-" | xargs -r docker rm -f >/dev/null 2>&1 || true
|
||||
docker network ls --filter "name=rootnode-lab-net-" --format "{{.Name}}" | xargs -r -n1 docker network rm >/dev/null 2>&1 || true
|
||||
docker compose -f docker-compose.dev.yml down >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "==> Waiting for the stack"
|
||||
for _ in $(seq 1 60); do
|
||||
curl -sf http://localhost:4300/healthz >/dev/null 2>&1 && curl -sf http://localhost:4200/healthz >/dev/null 2>&1 && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "==> Bootstrapping the rehearsal owner"
|
||||
( cd api && PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \
|
||||
node bootstrap-owner.js [email protected] "P6D Rehearsal Owner" 'RehearsalPass!2026' )
|
||||
|
||||
echo "==> Running the rehearsal"
|
||||
OWNER_EMAIL=[email protected] OWNER_PASSWORD='RehearsalPass!2026' \
|
||||
node ../infra/scripts/drills/test-phase6d-foundations-rehearsal.js
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env node
|
||||
// Phase 6D §27 — the ROOT Foundations dress rehearsal. One continuous realistic student
|
||||
// narrative through ROOT 01 (Computing & Linux Foundations, fully authored this phase) using a
|
||||
// REAL disposable student Lab end to end: login -> program -> course -> lesson -> guided
|
||||
// exercise -> real shell interaction -> validated filesystem/process/permission activities ->
|
||||
// progress persistence -> assessment -> ROOT 02 unlock state, then an instructor narrative
|
||||
// confirming the resulting learning state is visible. ROOT 02-04 are not rehearsed here since
|
||||
// they remain unauthored this phase (see docs/PHASE6D_ROOT_FOUNDATIONS.md).
|
||||
//
|
||||
// Unlike the ROOT 05+ rehearsals, this one never touches kubectl or asserts anything about
|
||||
// Kubernetes — ROOT 01 is Linux/shell content, and the assertions below only ever exercise
|
||||
// real bash/coreutils commands. (The Lab container's own boot sequence still starts k3s before
|
||||
// signaling ready, same as every other course's Lab — that's an existing image-level fact this
|
||||
// phase didn't change, not something ROOT 01's content depends on.)
|
||||
//
|
||||
// Run via run-phase6d-foundations-rehearsal.sh, which brings up platform/docker-compose.dev.yml
|
||||
// first.
|
||||
|
||||
const assert = require('assert');
|
||||
const { execSync } = require('child_process');
|
||||
const { makeSession } = require('../../../platform/test/session-helper');
|
||||
|
||||
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
|
||||
const OWNER_EMAIL = process.env.OWNER_EMAIL;
|
||||
const OWNER_PASSWORD = process.env.OWNER_PASSWORD;
|
||||
if (!OWNER_EMAIL || !OWNER_PASSWORD) { console.error('Set OWNER_EMAIL and OWNER_PASSWORD'); process.exit(1); }
|
||||
const RUN_ID = Date.now();
|
||||
const PROGRAM = 'root-kubernetes-foundations';
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
async function step(name, fn) {
|
||||
try { await fn(); console.log(` ok — ${name}`); passed++; }
|
||||
catch (e) { console.error(` FAIL — ${name}\n ${e.stack || e.message}`); failed++; }
|
||||
}
|
||||
function moduleFrom(tree, id) { for (const c of tree) { const m = c.modules.find((x) => x.module_id === id); if (m) return m; } return null; }
|
||||
function courseFrom(tree, id) { return tree.find((c) => c.course_id === id); }
|
||||
|
||||
async function main() {
|
||||
console.log('Phase 6D ROOT Foundations dress rehearsal — real disposable Lab, no mocking\n');
|
||||
|
||||
const owner = makeSession(GW);
|
||||
await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD });
|
||||
|
||||
const { body: created } = await owner.request('POST', '/api/admin/students', {
|
||||
display_name: 'Rehearsal 6D Student', login_identifier: `p6d-foundations-${RUN_ID}`,
|
||||
});
|
||||
const session = makeSession(GW);
|
||||
let enrollmentId, envId;
|
||||
|
||||
await step('student activates credentials and logs in', async () => {
|
||||
const { status } = await session.request('POST', '/api/student/activate', { token: created.activation_token, password: 'RehearsalPass123!' });
|
||||
assert.equal(status, 200);
|
||||
});
|
||||
|
||||
await step('an instructor enrolls the student in the ROOT_ program', async () => {
|
||||
const { status, body } = await owner.request('POST', '/api/admin/enrollments', { student_id: created.student_id, program_id: PROGRAM, status: 'active' });
|
||||
assert.equal(status, 201);
|
||||
enrollmentId = body.enrollment_id;
|
||||
});
|
||||
|
||||
await step('resume points at ROOT 01\'s very first activity', async () => {
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
|
||||
});
|
||||
|
||||
await step('student reads ROOT 01 module 1\'s lesson and guided exercise', async () => {
|
||||
for (const suffix of ['a01-lesson', 'a02-guided-exercise']) {
|
||||
const { status } = await session.request('POST', `/api/student/activities/root-01-m01-${suffix}/complete`);
|
||||
assert.equal(status, 200);
|
||||
}
|
||||
});
|
||||
|
||||
await step('student launches a REAL disposable Lab', async () => {
|
||||
const launch = await session.request('POST', `/api/student/programs/${enrollmentId}/launch`);
|
||||
assert.equal(launch.status, 200);
|
||||
assert.equal(launch.body.status, 'running');
|
||||
const envRow = (await owner.request('GET', `/api/admin/students/${created.student_id}/environment`)).body.find((e) => e.status === 'running');
|
||||
envId = envRow.environment_id;
|
||||
});
|
||||
|
||||
let labToken;
|
||||
await step('student runs the linux-inspect-hardware Lab using REAL shell commands inside the real Lab container', async () => {
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
const mod = moduleFrom(body.courses, 'root-01-m01-how-computers-run-software');
|
||||
const lab = mod.activities.find((a) => a.scenario_id === 'linux-inspect-hardware');
|
||||
assert.equal(lab.complete, false);
|
||||
|
||||
const launch = await session.request('POST', `/api/student/programs/${enrollmentId}/launch`);
|
||||
labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1];
|
||||
const setup = await session.request('POST', `/lab/${labToken}/api/scenarios/linux-inspect-hardware/setup`);
|
||||
assert.equal(setup.status, 200);
|
||||
|
||||
const containerName = execSync(
|
||||
`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "SELECT container_name FROM environments WHERE environment_id = '${envId}'"`,
|
||||
{ cwd: `${__dirname}/../../../platform`, stdio: 'pipe' }
|
||||
).toString().trim();
|
||||
|
||||
// Real bash, run inside the student's actual disposable Lab container — exactly the
|
||||
// commands the module's lesson/hints teach, not a simulated pass. Written to a script FILE
|
||||
// on the host and copied in, rather than an inline heredoc, to sidestep JS-template/shell/
|
||||
// heredoc triple-escaping entirely — a real, reproducible failure hit on the first run of
|
||||
// this rehearsal (see the fix noted in docs/PHASE6D_ROOT_FOUNDATIONS.md).
|
||||
const fs = require('fs');
|
||||
const scriptPath = `/tmp/phase6d-hardware-report-${envId}.sh`;
|
||||
fs.writeFileSync(scriptPath, [
|
||||
'#!/bin/bash',
|
||||
'set -e',
|
||||
'mkdir -p /srv/hardware-lab',
|
||||
'CPUS=$(nproc)',
|
||||
"MEM_MB=$(free -m | awk '/Mem:/ {print $2}')",
|
||||
'DISK_GB=$(df -BG / | awk \'NR==2{gsub("G","",$2); print $2}\')',
|
||||
'printf "CPUS=%s\\nMEM_MB=%s\\nDISK_GB=%s\\n" "$CPUS" "$MEM_MB" "$DISK_GB" > /srv/hardware-lab/report.txt',
|
||||
].join('\n'));
|
||||
execSync(`docker cp ${scriptPath} ${containerName}:/tmp/hardware-report.sh && docker exec ${containerName} bash /tmp/hardware-report.sh`, { stdio: 'pipe' });
|
||||
fs.unlinkSync(scriptPath);
|
||||
|
||||
const validate = await session.request('POST', `/lab/${labToken}/api/scenarios/linux-inspect-hardware/validate`);
|
||||
assert.equal(validate.status, 200);
|
||||
assert.equal(validate.body.passed, true, `expected the real validation to pass: ${JSON.stringify(validate.body)}`);
|
||||
|
||||
const record = await session.request('POST', '/api/progress/record', { scenario_id: 'linux-inspect-hardware', status: 'completed', environment_id: envId });
|
||||
assert.equal(record.status, 200);
|
||||
});
|
||||
|
||||
await step('student runs the linux-permissions-foundations Lab, including a real permission denial and fix', async () => {
|
||||
const setup = await session.request('POST', `/lab/${labToken}/api/scenarios/linux-permissions-foundations/setup`);
|
||||
assert.equal(setup.status, 200);
|
||||
const containerName = execSync(
|
||||
`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "SELECT container_name FROM environments WHERE environment_id = '${envId}'"`,
|
||||
{ cwd: `${__dirname}/../../../platform`, stdio: 'pipe' }
|
||||
).toString().trim();
|
||||
|
||||
// Confirm the real denial exists before fixing it — this is the actual pedagogical point.
|
||||
let denied = false;
|
||||
try { execSync(`docker exec ${containerName} su - labuser -c "cat /srv/perm-lab/data.txt"`, { stdio: 'pipe' }); }
|
||||
catch (e) { denied = /Permission denied/.test(e.stderr ? e.stderr.toString() : ''); }
|
||||
assert.equal(denied, true, 'expected a REAL permission denial before the fix, not an assumption');
|
||||
|
||||
execSync(`docker exec ${containerName} chmod 644 /srv/perm-lab/data.txt`, { stdio: 'pipe' });
|
||||
const afterFix = execSync(`docker exec ${containerName} su - labuser -c "cat /srv/perm-lab/data.txt"`, { stdio: 'pipe' }).toString().trim();
|
||||
assert.equal(afterFix, 'sensitive', 'labuser must actually be able to read the file after the real chmod fix');
|
||||
|
||||
const validate = await session.request('POST', `/lab/${labToken}/api/scenarios/linux-permissions-foundations/validate`);
|
||||
assert.equal(validate.status, 200);
|
||||
assert.equal(validate.body.passed, true, `expected the real validation to pass: ${JSON.stringify(validate.body)}`);
|
||||
const record = await session.request('POST', '/api/progress/record', { scenario_id: 'linux-permissions-foundations', status: 'completed', environment_id: envId });
|
||||
assert.equal(record.status, 200);
|
||||
});
|
||||
|
||||
await step('completing the remaining ROOT 01 activities finishes the course and unlocks ROOT 02', async () => {
|
||||
await session.request('POST', '/api/student/activities/root-01-m01-a04-reflection/complete');
|
||||
for (const m of ['m02', 'm03', 'm04', 'm05', 'm06']) {
|
||||
for (const suffix of ['a01-lesson', 'a02-demonstration', 'a03-knowledge-check', 'a04-guided-exercise']) {
|
||||
await session.request('POST', `/api/student/activities/root-01-${m}-${suffix}/complete`);
|
||||
}
|
||||
}
|
||||
for (const [sid, activity] of [['linux-shell-basics', 'root-01-m02-a06-reflection'], ['linux-filesystem-navigation', 'root-01-m03-a06-reflection'], ['linux-files-and-text', 'root-01-m04-a06-reflection'], ['linux-process-management', 'root-01-m05-a06-reflection']]) {
|
||||
await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed', environment_id: envId });
|
||||
await session.request('POST', `/api/student/activities/${activity}/complete`);
|
||||
}
|
||||
await session.request('POST', '/api/student/activities/root-01-m06-a06-reflection/complete');
|
||||
|
||||
const start = await session.request('POST', '/api/assessments/rootnode-linux-foundations-assessment/start');
|
||||
assert.equal(start.status, 201);
|
||||
for (const s of start.body.scenarios) {
|
||||
const r = await session.request('POST', '/api/progress/record', { scenario_id: s.scenario_id, status: 'completed', environment_id: envId });
|
||||
assert.equal(r.status, 200);
|
||||
}
|
||||
|
||||
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(courseFrom(body.courses, 'root-01-computing-linux-foundations').complete, true);
|
||||
assert.equal(courseFrom(body.courses, 'root-02-linux-administration-networking').locked, false, 'ROOT 02 must unlock now (though it has no content of its own yet)');
|
||||
});
|
||||
|
||||
await step('logout, login — ROOT 01 progress survives all of it', async () => {
|
||||
await session.request('POST', '/api/logout');
|
||||
const returning = makeSession(GW);
|
||||
await returning.request('POST', '/api/student/login', { identifier: `p6d-foundations-${RUN_ID}`, password: 'RehearsalPass123!' });
|
||||
const { body } = await returning.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(courseFrom(body.courses, 'root-01-computing-linux-foundations').complete, true, 'progress must survive a real logout/login');
|
||||
});
|
||||
|
||||
// ── Instructor narrative — confirming the resulting learning state is visible ───────────
|
||||
await step('instructor inspects the ROOT 01 module structure and its instructor notes', async () => {
|
||||
const { status, body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
|
||||
assert.equal(status, 200);
|
||||
const mod = moduleFrom(body, 'root-01-m06-users-and-permissions-foundations');
|
||||
assert.equal(mod.activities.length, 7);
|
||||
assert.ok(mod.activities[0].instructor_notes, 'instructor must see instructor notes the student view never does');
|
||||
});
|
||||
|
||||
await step('instructor sees this specific student\'s real, completed ROOT 01 progress', async () => {
|
||||
const { status, body } = await owner.request('GET', `/api/admin/students/${created.student_id}/courses?program_id=${PROGRAM}`);
|
||||
assert.equal(status, 200);
|
||||
assert.equal(courseFrom(body, 'root-01-computing-linux-foundations').complete, true);
|
||||
});
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
if (failed > 0) process.exit(1);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
@@ -0,0 +1,148 @@
|
||||
-- Phase 6D — ROOT 01: Computing & Linux Foundations. Adds 6 modules/activities under the
|
||||
-- existing (PLANNED, zero-content) root-01-computing-linux-foundations course row seeded by
|
||||
-- Phase 6A's 022_course_content.sql. Every lab activity references a NEW scenario file
|
||||
-- (scenarios/data/linux-*.json) using the existing, generic scenario/validation engine — see
|
||||
-- docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §1 for why no new activity type or mechanism was
|
||||
-- needed. Every lesson/demonstration/knowledge_check/guided_exercise/reflection activity
|
||||
-- references a real content file under content/root/root-01-computing-linux-foundations/.
|
||||
|
||||
UPDATE courses SET
|
||||
status = 'ready',
|
||||
objectives = '{"Explain what the OS/kernel manages versus an application","Use a Linux shell confidently, including reading exit codes","Navigate the filesystem using absolute and relative paths","Manipulate files and text with pipes and redirection","Inspect and safely terminate a real process","Apply the least-privilege principle to file permissions"}',
|
||||
updated_at = now()
|
||||
WHERE course_id = 'root-01-computing-linux-foundations';
|
||||
|
||||
INSERT INTO course_modules (module_id, course_id, title, description, sequence, objectives, prerequisites, estimated_duration_minutes, status) VALUES
|
||||
('root-01-m01-how-computers-run-software', 'root-01-computing-linux-foundations', 'How Computers Run Software',
|
||||
'Hardware vs. OS vs. application, and the CPU/RAM/storage/process vocabulary the rest of ROOT_ assumes. Target learner: no assumed prior Linux/CLI experience.',
|
||||
1, '{"Explain the hardware/kernel/application layering","Describe what CPU, RAM, and storage each actually do","Inspect a real machine''s CPU, memory, and disk"}', '{}', 40, 'ready'),
|
||||
('root-01-m02-linux-and-the-shell', 'root-01-computing-linux-foundations', 'Linux and the Shell',
|
||||
'What Linux/a distribution/a shell actually are, command structure, and exit codes as the universal success/failure signal.',
|
||||
2, '{"Distinguish shell, terminal, and distribution","Read and interpret a command''s exit code","Use tab completion, history, and man pages"}', '{root-01-m01-how-computers-run-software}', 45, 'ready'),
|
||||
('root-01-m03-navigating-the-filesystem', 'root-01-computing-linux-foundations', 'Navigating the Filesystem',
|
||||
'Absolute vs. relative paths, and what the standard top-level directories are for.',
|
||||
3, '{"Navigate using absolute and relative paths correctly","Explain the purpose of /etc, /var, /home, /tmp, /usr, /proc","Build a real nested directory structure"}', '{root-01-m02-linux-and-the-shell}', 45, 'ready'),
|
||||
('root-01-m04-files-and-text', 'root-01-computing-linux-foundations', 'Files and Text',
|
||||
'File manipulation, reading text, and composing commands with pipes/redirection.',
|
||||
4, '{"Manipulate files and directories with the core file commands","Filter text with grep","Compose commands using pipes and redirection"}', '{root-01-m03-navigating-the-filesystem}', 45, 'ready'),
|
||||
('root-01-m05-processes', 'root-01-computing-linux-foundations', 'Processes',
|
||||
'The process concept, foreground/background, signals, and why this matters for containers later.',
|
||||
5, '{"Explain what a process and a PID are","Inspect running processes with ps","Terminate a process correctly, verifying the result"}', '{root-01-m04-files-and-text}', 40, 'ready'),
|
||||
('root-01-m06-users-and-permissions-foundations', 'root-01-computing-linux-foundations', 'Users & Permissions Foundations',
|
||||
'Ownership, rwx permissions, sudo, and the least-privilege principle this phase introduces for the first time in ROOT_.',
|
||||
6, '{"Read and set rwx permissions using octal notation","Explain why sudo exists instead of permanent root access","State and apply the least-privilege principle"}', '{root-01-m05-processes}', 45, 'ready')
|
||||
ON CONFLICT (module_id) DO NOTHING;
|
||||
|
||||
INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES
|
||||
-- M01
|
||||
('root-01-m01-a01-lesson', 'root-01-m01-how-computers-run-software', 1, 'lesson', 'How Computers Run Software',
|
||||
'content/root/root-01-computing-linux-foundations/m01-how-computers-run-software/01-lesson-how-computers-run-software.md', NULL, NULL, 'manual_marker', 15, 'ready'),
|
||||
('root-01-m01-a02-guided-exercise', 'root-01-m01-how-computers-run-software', 2, 'guided_exercise', 'Guided Exercise: Predict Before You Look',
|
||||
'content/root/root-01-computing-linux-foundations/m01-how-computers-run-software/02-guided-exercise-inspect-hardware-on-paper.md', NULL, NULL, 'manual_marker', 5, 'ready'),
|
||||
('root-01-m01-a03-lab-inspect-hardware', 'root-01-m01-how-computers-run-software', 3, 'lab', 'Lab: Inspect What This Machine Actually Has',
|
||||
NULL, 'linux-inspect-hardware', NULL, 'auto_scenario', 10, 'ready'),
|
||||
('root-01-m01-a04-reflection', 'root-01-m01-how-computers-run-software', 4, 'reflection', 'Reflection: You Just Looked Inside the Box',
|
||||
'content/root/root-01-computing-linux-foundations/m01-how-computers-run-software/03-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
-- M02
|
||||
('root-01-m02-a01-lesson', 'root-01-m02-linux-and-the-shell', 1, 'lesson', 'Linux and the Shell',
|
||||
'content/root/root-01-computing-linux-foundations/m02-linux-and-the-shell/01-lesson-linux-and-the-shell.md', NULL, NULL, 'manual_marker', 15, 'ready'),
|
||||
('root-01-m02-a02-demonstration', 'root-01-m02-linux-and-the-shell', 2, 'demonstration', 'Demonstration: Watching Exit Codes in Real Time',
|
||||
'content/root/root-01-computing-linux-foundations/m02-linux-and-the-shell/02-demonstration-exit-codes-in-action.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
('root-01-m02-a03-knowledge-check', 'root-01-m02-linux-and-the-shell', 3, 'knowledge_check', 'Knowledge Check: Linux and the Shell',
|
||||
'content/root/root-01-computing-linux-foundations/m02-linux-and-the-shell/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'),
|
||||
('root-01-m02-a04-guided-exercise', 'root-01-m02-linux-and-the-shell', 4, 'guided_exercise', 'Guided Exercise: Predict the Exit Code',
|
||||
'content/root/root-01-computing-linux-foundations/m02-linux-and-the-shell/04-guided-exercise-predicting-exit-codes.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
('root-01-m02-a05-lab-shell-basics', 'root-01-m02-linux-and-the-shell', 5, 'lab', 'Lab: Reading Exit Codes Like an Engineer',
|
||||
NULL, 'linux-shell-basics', NULL, 'auto_scenario', 10, 'ready'),
|
||||
('root-01-m02-a06-reflection', 'root-01-m02-linux-and-the-shell', 6, 'reflection', 'Reflection: Exit Codes Are Everywhere After This',
|
||||
'content/root/root-01-computing-linux-foundations/m02-linux-and-the-shell/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
-- M03
|
||||
('root-01-m03-a01-lesson', 'root-01-m03-navigating-the-filesystem', 1, 'lesson', 'Navigating the Filesystem',
|
||||
'content/root/root-01-computing-linux-foundations/m03-navigating-the-filesystem/01-lesson-filesystem-navigation.md', NULL, NULL, 'manual_marker', 15, 'ready'),
|
||||
('root-01-m03-a02-demonstration', 'root-01-m03-navigating-the-filesystem', 2, 'demonstration', 'Demonstration: The Same Word, Two Different Destinations',
|
||||
'content/root/root-01-computing-linux-foundations/m03-navigating-the-filesystem/02-demonstration-absolute-vs-relative.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
('root-01-m03-a03-knowledge-check', 'root-01-m03-navigating-the-filesystem', 3, 'knowledge_check', 'Knowledge Check: Navigating the Filesystem',
|
||||
'content/root/root-01-computing-linux-foundations/m03-navigating-the-filesystem/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'),
|
||||
('root-01-m03-a04-guided-exercise', 'root-01-m03-navigating-the-filesystem', 4, 'guided_exercise', 'Guided Exercise: Plan a Directory Structure on Paper First',
|
||||
'content/root/root-01-computing-linux-foundations/m03-navigating-the-filesystem/04-guided-exercise-planning-a-structure.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
('root-01-m03-a05-lab-filesystem-navigation', 'root-01-m03-navigating-the-filesystem', 5, 'lab', 'Lab: Build a Real Project Directory Structure',
|
||||
NULL, 'linux-filesystem-navigation', NULL, 'auto_scenario', 10, 'ready'),
|
||||
('root-01-m03-a06-reflection', 'root-01-m03-navigating-the-filesystem', 6, 'reflection', 'Reflection: Structure Isn''t Bureaucracy, It''s Communication',
|
||||
'content/root/root-01-computing-linux-foundations/m03-navigating-the-filesystem/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
-- M04
|
||||
('root-01-m04-a01-lesson', 'root-01-m04-files-and-text', 1, 'lesson', 'Files and Text',
|
||||
'content/root/root-01-computing-linux-foundations/m04-files-and-text/01-lesson-files-and-text.md', NULL, NULL, 'manual_marker', 15, 'ready'),
|
||||
('root-01-m04-a02-demonstration', 'root-01-m04-files-and-text', 2, 'demonstration', 'Demonstration: Filtering a Log Down to What Matters',
|
||||
'content/root/root-01-computing-linux-foundations/m04-files-and-text/02-demonstration-pipes-and-redirection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
('root-01-m04-a03-knowledge-check', 'root-01-m04-files-and-text', 3, 'knowledge_check', 'Knowledge Check: Files and Text',
|
||||
'content/root/root-01-computing-linux-foundations/m04-files-and-text/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'),
|
||||
('root-01-m04-a04-guided-exercise', 'root-01-m04-files-and-text', 4, 'guided_exercise', 'Guided Exercise: Which Tool Actually Answers the Question?',
|
||||
'content/root/root-01-computing-linux-foundations/m04-files-and-text/04-guided-exercise-choosing-the-right-tool.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
('root-01-m04-a05-lab-files-and-text', 'root-01-m04-files-and-text', 5, 'lab', 'Lab: Extract the Signal From a Log File',
|
||||
NULL, 'linux-files-and-text', NULL, 'auto_scenario', 10, 'ready'),
|
||||
('root-01-m04-a06-reflection', 'root-01-m04-files-and-text', 6, 'reflection', 'Reflection: Filtering Is a Reasoning Skill, Not a Syntax Trick',
|
||||
'content/root/root-01-computing-linux-foundations/m04-files-and-text/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
-- M05
|
||||
('root-01-m05-a01-lesson', 'root-01-m05-processes', 1, 'lesson', 'Processes',
|
||||
'content/root/root-01-computing-linux-foundations/m05-processes/01-lesson-processes.md', NULL, NULL, 'manual_marker', 15, 'ready'),
|
||||
('root-01-m05-a02-demonstration', 'root-01-m05-processes', 2, 'demonstration', 'Demonstration: Watching a Process Actually Terminate',
|
||||
'content/root/root-01-computing-linux-foundations/m05-processes/02-demonstration-watching-a-process-die.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
('root-01-m05-a03-knowledge-check', 'root-01-m05-processes', 3, 'knowledge_check', 'Knowledge Check: Processes',
|
||||
'content/root/root-01-computing-linux-foundations/m05-processes/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'),
|
||||
('root-01-m05-a04-guided-exercise', 'root-01-m05-processes', 4, 'guided_exercise', 'Guided Exercise: Choosing the Right Signal',
|
||||
'content/root/root-01-computing-linux-foundations/m05-processes/04-guided-exercise-signals.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
('root-01-m05-a05-lab-process-management', 'root-01-m05-processes', 5, 'lab', 'Lab: Find and Stop a Runaway Process',
|
||||
NULL, 'linux-process-management', NULL, 'auto_scenario', 10, 'ready'),
|
||||
('root-01-m05-a06-reflection', 'root-01-m05-processes', 6, 'reflection', 'Reflection: You Just Stopped a Real Process, Not a Simulation',
|
||||
'content/root/root-01-computing-linux-foundations/m05-processes/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
-- M06
|
||||
('root-01-m06-a01-lesson', 'root-01-m06-users-and-permissions-foundations', 1, 'lesson', 'Users and Permissions Foundations',
|
||||
'content/root/root-01-computing-linux-foundations/m06-users-and-permissions-foundations/01-lesson-users-and-permissions.md', NULL, NULL, 'manual_marker', 15, 'ready'),
|
||||
('root-01-m06-a02-demonstration', 'root-01-m06-users-and-permissions-foundations', 2, 'demonstration', 'Demonstration: A Real Permission Denial, and the Real Fix',
|
||||
'content/root/root-01-computing-linux-foundations/m06-users-and-permissions-foundations/02-demonstration-permission-denied.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
('root-01-m06-a03-knowledge-check', 'root-01-m06-users-and-permissions-foundations', 3, 'knowledge_check', 'Knowledge Check: Users and Permissions Foundations',
|
||||
'content/root/root-01-computing-linux-foundations/m06-users-and-permissions-foundations/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'),
|
||||
('root-01-m06-a04-guided-exercise', 'root-01-m06-users-and-permissions-foundations', 4, 'guided_exercise', 'Guided Exercise: Pick the Minimal Permission, Not the Easy One',
|
||||
'content/root/root-01-computing-linux-foundations/m06-users-and-permissions-foundations/04-guided-exercise-least-privilege.md', NULL, NULL, 'manual_marker', 8, 'ready'),
|
||||
('root-01-m06-a05-lab-permissions', 'root-01-m06-users-and-permissions-foundations', 5, 'lab', 'Lab: Fix a Real Permission Denial',
|
||||
NULL, 'linux-permissions-foundations', NULL, 'auto_scenario', 10, 'ready'),
|
||||
('root-01-m06-a06-reflection', 'root-01-m06-users-and-permissions-foundations', 6, 'reflection', 'Reflection: Least Privilege Is a Habit, Not a One-Time Fix',
|
||||
'content/root/root-01-computing-linux-foundations/m06-users-and-permissions-foundations/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready')
|
||||
ON CONFLICT (activity_id) DO NOTHING;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
-- ROOT 01 course assessment — mixes real state-validation tasks (the same scenarios already
|
||||
-- used as labs above, plus two more) with knowledge-recall MCQs, via the existing Phase 3B
|
||||
-- assessment engine. No Kubernetes scenario is force-fit into this non-Kubernetes assessment.
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
INSERT INTO assessment_definitions (assessment_definition_id, type, title, description, duration_minutes, hint_policy, pool_spec, completion_rule)
|
||||
VALUES (
|
||||
'rootnode-linux-foundations-assessment', 'exam', 'ROOT 01 Linux Foundations Assessment',
|
||||
'A timed check across shell fluency, filesystem navigation, process management, and least-privilege permissions — combining real state-validation tasks with conceptual reasoning questions.',
|
||||
30, 'disabled',
|
||||
'{"required": ["linux-shell-basics", "linux-permissions-foundations"],
|
||||
"random": [
|
||||
{"count":2,"from":["linux-inspect-hardware","linux-filesystem-navigation","linux-files-and-text","linux-process-management"]},
|
||||
{"count":2,"from":["linux-exit-codes-mcq","linux-permissions-mcq"]}
|
||||
]}',
|
||||
'{"type":"all_scenarios_passed"}'
|
||||
) ON CONFLICT (assessment_definition_id) DO NOTHING;
|
||||
|
||||
INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES
|
||||
('root-01-m06-a07-assessment', 'root-01-m06-users-and-permissions-foundations', 7, 'assessment', 'ROOT 01 Linux Foundations Assessment', NULL, NULL, 'rootnode-linux-foundations-assessment', 'auto_assessment', 30, 'ready')
|
||||
ON CONFLICT (activity_id) DO NOTHING;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
-- Step 19 — real ROOT 01→02→03→04→05 course prerequisite chain, server-side (courses.prerequisites),
|
||||
-- not UI-hidden alone. root-02/03/04 remain PLANNED with zero activities this phase, so per
|
||||
-- docs/COURSE_CONTENT_MODEL.md §5's "zero activities is vacuously satisfied" rule, this chain
|
||||
-- has no practical locking effect YET on 02/03/04 or on 05 (which already only real-gates once
|
||||
-- root-04 is eventually authored) — it correctly encodes the INTENDED sequence now, so a future
|
||||
-- phase authoring ROOT 02-04 doesn't need to reopen this decision, without breaking any current
|
||||
-- Kubernetes-track student's path today (root-05's real prerequisite content, root-04, is still
|
||||
-- honestly empty).
|
||||
-- ═══════════════════════════════════════════════════════════════════════════════════════════
|
||||
UPDATE courses SET prerequisites = '{root-01-computing-linux-foundations}', updated_at = now() WHERE course_id = 'root-02-linux-administration-networking';
|
||||
UPDATE courses SET prerequisites = '{root-02-linux-administration-networking}', updated_at = now() WHERE course_id = 'root-03-git-collaborative-development';
|
||||
UPDATE courses SET prerequisites = '{root-03-git-collaborative-development}', updated_at = now() WHERE course_id = 'root-04-containers-and-docker';
|
||||
UPDATE courses SET prerequisites = '{root-04-containers-and-docker}', updated_at = now() WHERE course_id = 'root-05-kubernetes-foundations';
|
||||
@@ -122,7 +122,7 @@ async function main() {
|
||||
assert.equal(status, 200);
|
||||
tree = body.courses;
|
||||
const root05 = courseById(tree, 'root-05-kubernetes-foundations');
|
||||
assert.equal(root05.locked, false); // the first real course is never locked
|
||||
assert.equal(root05.locked, false); // root-04 (its real Phase 6D prerequisite) has zero activities — vacuously satisfied
|
||||
assert.equal(root05.complete, false);
|
||||
const root06 = courseById(tree, 'root-06-kubernetes-application-workloads');
|
||||
assert.equal(root06.locked, true, 'root-06 must be locked until root-05 is complete');
|
||||
@@ -130,11 +130,14 @@ async function main() {
|
||||
|
||||
await test('resume points at the first activity of the first unlocked course', async () => {
|
||||
const { body } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(body.resume.course_id, 'root-05-kubernetes-foundations');
|
||||
// Phase 6B added root-05-m00 (the mental-model module) as the new front door, sequenced
|
||||
// before the pre-existing bare-lab bank — see docs/PHASE6B_ROOT_CURRICULUM_EXPANSION.md.
|
||||
assert.equal(body.resume.module_id, 'root-05-m00-kubernetes-mental-model');
|
||||
assert.equal(body.resume.activity_id, 'root-05-m00-a01-lesson-mental-model');
|
||||
// Phase 6D authored ROOT 01 (Computing & Linux Foundations) as a real, non-PLANNED course
|
||||
// sequenced before ROOT 05 — it is now genuinely the program's front door, not ROOT 05's
|
||||
// own gold module (see docs/PHASE6D_ROOT_FOUNDATIONS.md). This test's original intent —
|
||||
// "resume lands on the first REAL content, not a bare-lab bank" — is still what's being
|
||||
// proven, just against the current front door.
|
||||
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
|
||||
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
|
||||
});
|
||||
|
||||
await test('a student cannot mark a lab activity complete manually — only real scenario completion counts', async () => {
|
||||
|
||||
@@ -126,9 +126,13 @@ async function main() {
|
||||
assert.equal(moduleById(body.courses, 'root-10-m03-troubleshooting-framework').locked, true, 'must stay locked behind the gold module even though it is a different course-level dependency chain');
|
||||
});
|
||||
|
||||
await test('resume points at ROOT 05\'s new mental-model module, not the old bare-lab bank', async () => {
|
||||
await test('resume points at ROOT 01 (Phase 6D\'s new front door), not ROOT 05\'s old bare-lab bank', async () => {
|
||||
// Phase 6D authored ROOT 01 as real, non-PLANNED content sequenced before ROOT 05 — it is
|
||||
// now genuinely the program's front door (docs/PHASE6D_ROOT_FOUNDATIONS.md). This test's
|
||||
// original intent — resume must never land on a bare-lab bank when real front-door content
|
||||
// exists — is unchanged, just against the current front door.
|
||||
const { body } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
assert.equal(body.resume.module_id, 'root-05-m00-kubernetes-mental-model');
|
||||
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software');
|
||||
});
|
||||
|
||||
await test('lesson/knowledge_check completion works identically in a brand-new module (root-05-m00)', async () => {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env node
|
||||
// Phase 6D — ROOT Foundations acceptance test. Covers what this phase actually built: ROOT 01
|
||||
// (Computing & Linux Foundations, fully authored) and the real ROOT 01->02->03->04->05
|
||||
// prerequisite chain. ROOT 02-04 remain PLANNED (architecture designed in
|
||||
// docs/ROOT_FOUNDATIONS_ARCHITECTURE.md, not yet authored — see
|
||||
// docs/PHASE6D_ROOT_FOUNDATIONS.md for why), so this suite does not assert content that
|
||||
// doesn't exist. Same conventions as e2e-phase6a/6b/6c.js: assert-based, no framework, live
|
||||
// docker-compose.dev.yml stack, no real Lab container launched here (see
|
||||
// infra/scripts/drills/test-phase6d-foundations-rehearsal.js for the real-Lab counterpart).
|
||||
//
|
||||
// Usage: cd platform && docker compose -f docker-compose.dev.yml up -d
|
||||
// (cd api && PLATFORM_DB_URL=... node bootstrap-owner.js OWNER_EMAIL "Owner" OWNER_PASSWORD)
|
||||
// OWNER_EMAIL=... OWNER_PASSWORD=... node test/e2e-phase6d.js
|
||||
|
||||
const assert = require('assert');
|
||||
const { execSync } = require('child_process');
|
||||
const { makeSession } = require('./session-helper');
|
||||
|
||||
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
|
||||
const OWNER_EMAIL = process.env.OWNER_EMAIL;
|
||||
const OWNER_PASSWORD = process.env.OWNER_PASSWORD;
|
||||
if (!OWNER_EMAIL || !OWNER_PASSWORD) {
|
||||
console.error('Set OWNER_EMAIL and OWNER_PASSWORD (bootstrap one first with platform/api/bootstrap-owner.js)');
|
||||
process.exit(1);
|
||||
}
|
||||
const RUN_ID = Date.now();
|
||||
const PROGRAM = 'root-kubernetes-foundations';
|
||||
const ROOT01_MODULES = ['root-01-m01-how-computers-run-software', 'root-01-m02-linux-and-the-shell', 'root-01-m03-navigating-the-filesystem', 'root-01-m04-files-and-text', 'root-01-m05-processes', 'root-01-m06-users-and-permissions-foundations'];
|
||||
const ROOT01_LAB_SCENARIOS = ['linux-inspect-hardware', 'linux-shell-basics', 'linux-filesystem-navigation', 'linux-files-and-text', 'linux-process-management', 'linux-permissions-foundations'];
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
async function test(name, fn) {
|
||||
try { await fn(); console.log(` ok — ${name}`); passed++; }
|
||||
catch (e) { console.error(` FAIL — ${name}\n ${e.message}`); failed++; }
|
||||
}
|
||||
async function completeScenario(session, scenarioId) {
|
||||
const { status } = await session.request('POST', '/api/progress/record', { scenario_id: scenarioId, status: 'completed' });
|
||||
assert.equal(status, 200, `completing ${scenarioId} should 200`);
|
||||
}
|
||||
async function completeActivity(session, id) {
|
||||
const { status, body } = await session.request('POST', `/api/student/activities/${id}/complete`);
|
||||
assert.equal(status, 200, `completing ${id} should 200: ${JSON.stringify(body)}`);
|
||||
}
|
||||
function courseById(courses, id) { return courses.find((c) => c.course_id === id); }
|
||||
function moduleById(courses, id) { for (const c of courses) { const m = c.modules.find((x) => x.module_id === id); if (m) return m; } return null; }
|
||||
function findActivity(courses, id) { for (const c of courses) for (const m of c.modules) { const a = m.activities.find((x) => x.activity_id === id); if (a) return a; } return null; }
|
||||
function psql(sql) {
|
||||
return execSync(`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "${sql}"`, { cwd: `${__dirname}/..`, stdio: 'pipe' }).toString().trim();
|
||||
}
|
||||
async function newStudent(owner, tag) {
|
||||
const { body: create } = await owner.request('POST', '/api/admin/students', { display_name: `E2E-6D-${tag}-${RUN_ID}`, login_identifier: `e2e6d-${tag}-${RUN_ID}` });
|
||||
const s = makeSession(GW);
|
||||
await s.request('POST', '/api/student/activate', { token: create.activation_token, password: `${tag}Password123!` });
|
||||
const { body: enroll } = await owner.request('POST', '/api/admin/enrollments', { student_id: create.student_id, program_id: PROGRAM, status: 'active' });
|
||||
return { session: s, studentId: create.student_id, enrollmentId: enroll.enrollment_id };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Phase 6D ROOT Foundations acceptance test\n');
|
||||
|
||||
const owner = makeSession(GW);
|
||||
await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD });
|
||||
let structure;
|
||||
|
||||
// ── 1: ROOT 01 hierarchy ─────────────────────────────────────────────────────────────────
|
||||
await test('ROOT 01 has its 6 modules, in sequence, all READY', async () => {
|
||||
const { status, body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
|
||||
assert.equal(status, 200);
|
||||
structure = body;
|
||||
const course = courseById(structure, 'root-01-computing-linux-foundations');
|
||||
assert.equal(course.status, 'ready');
|
||||
assert.deepEqual(course.modules.map((m) => m.module_id), ROOT01_MODULES);
|
||||
for (const m of course.modules) assert.equal(m.status, 'ready', `${m.module_id} must be ready`);
|
||||
});
|
||||
|
||||
// ── 2/3/4: ROOT 02/03/04 hierarchy — honestly PLANNED, zero modules, not padded ──────────
|
||||
await test('ROOT 02/03/04 are honestly PLANNED with zero modules — not padded to look further along than they are', async () => {
|
||||
for (const id of ['root-02-linux-administration-networking', 'root-03-git-collaborative-development', 'root-04-containers-and-docker']) {
|
||||
const course = courseById(structure, id);
|
||||
assert.equal(course.status, 'planned', `${id} must stay honestly planned`);
|
||||
assert.equal(course.modules.length, 0, `${id} must have zero modules — no fabricated content`);
|
||||
}
|
||||
});
|
||||
|
||||
// ── 5/6: activity ordering + content actually loads ──────────────────────────────────────
|
||||
await test('every ROOT 01 module has strictly ascending activity sequence and real lesson content', async () => {
|
||||
for (const modId of ROOT01_MODULES) {
|
||||
const mod = moduleById(structure, modId);
|
||||
for (let i = 1; i < mod.activities.length; i++) assert.ok(mod.activities[i].sequence > mod.activities[i - 1].sequence, `${modId} activity ordering broken`);
|
||||
const lesson = mod.activities.find((a) => a.activity_type === 'lesson');
|
||||
assert.ok(lesson.body && lesson.body.length > 300, `${modId}'s lesson must have real content`);
|
||||
}
|
||||
});
|
||||
|
||||
// ── A real self-paced student walking ROOT 01 end to end ────────────────────────────────
|
||||
const casey = await newStudent(owner, 'Casey');
|
||||
|
||||
await test('resume points at ROOT 01 module 1 activity 1, the very first activity in the whole program', async () => {
|
||||
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
|
||||
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
|
||||
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
|
||||
});
|
||||
|
||||
// ── 16: direct API cannot bypass — a lab activity cannot be manually marked complete ─────
|
||||
await test('a lab activity cannot be marked complete manually — only real scenario completion counts', async () => {
|
||||
const { status, body } = await casey.session.request('POST', '/api/student/activities/root-01-m01-a03-lab-inspect-hardware/complete');
|
||||
assert.equal(status, 409);
|
||||
assert.equal(body.error, 'not_manually_completable');
|
||||
});
|
||||
|
||||
// ── 8/9/10/11: lesson/practical/knowledge-check/assessment completion, walked for real ──
|
||||
await test('walking ROOT 01 module 1 through the real API completes it', async () => {
|
||||
await completeActivity(casey.session, 'root-01-m01-a01-lesson');
|
||||
await completeActivity(casey.session, 'root-01-m01-a02-guided-exercise');
|
||||
await completeScenario(casey.session, 'linux-inspect-hardware');
|
||||
await completeActivity(casey.session, 'root-01-m01-a04-reflection');
|
||||
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
|
||||
assert.equal(moduleById(body.courses, 'root-01-m01-how-computers-run-software').complete, true);
|
||||
});
|
||||
|
||||
await test('completing every ROOT 01 module and its assessment marks the whole course complete', async () => {
|
||||
for (const m of ['m02', 'm03', 'm04', 'm05', 'm06']) {
|
||||
await completeActivity(casey.session, `root-01-${m}-a01-lesson`);
|
||||
await completeActivity(casey.session, `root-01-${m}-a02-demonstration`);
|
||||
await completeActivity(casey.session, `root-01-${m}-a03-knowledge-check`);
|
||||
await completeActivity(casey.session, `root-01-${m}-a04-guided-exercise`);
|
||||
}
|
||||
for (const [sid, activity] of [['linux-shell-basics', 'root-01-m02-a06-reflection'], ['linux-filesystem-navigation', 'root-01-m03-a06-reflection'], ['linux-files-and-text', 'root-01-m04-a06-reflection'], ['linux-process-management', 'root-01-m05-a06-reflection']]) {
|
||||
await completeScenario(casey.session, sid);
|
||||
await completeActivity(casey.session, activity);
|
||||
}
|
||||
await completeScenario(casey.session, 'linux-permissions-foundations');
|
||||
await completeActivity(casey.session, 'root-01-m06-a06-reflection');
|
||||
|
||||
const start = await casey.session.request('POST', '/api/assessments/rootnode-linux-foundations-assessment/start');
|
||||
assert.equal(start.status, 201);
|
||||
for (const s of start.body.scenarios) await completeScenario(casey.session, s.scenario_id);
|
||||
|
||||
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
|
||||
const course = courseById(body.courses, 'root-01-computing-linux-foundations');
|
||||
assert.equal(course.complete, true);
|
||||
assert.equal(course.percent_complete, 100);
|
||||
});
|
||||
|
||||
// ── 12/15: ROOT 01 unlocks ROOT 02 at the course level, for THIS student ─────────────────
|
||||
await test('completing ROOT 01 unlocks ROOT 02 for this student', async () => {
|
||||
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').locked, false);
|
||||
});
|
||||
|
||||
// ── A second, fresh student proves ROOT 02 stays LOCKED without ROOT 01 ─────────────────
|
||||
const bob = await newStudent(owner, 'Bob');
|
||||
await test('ROOT 02 is locked for a freshly-enrolled student who has not touched ROOT 01', async () => {
|
||||
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').locked, false, 'ROOT 01 itself is never locked — it is the first course');
|
||||
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').locked, true, 'ROOT 02 must be locked until ROOT 01 is complete');
|
||||
});
|
||||
|
||||
await test('ROOT 03/04/05 stay unlocked (vacuously satisfied) since their own prerequisite courses have zero content yet', async () => {
|
||||
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').locked, false);
|
||||
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, false);
|
||||
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, false);
|
||||
});
|
||||
|
||||
// ── 26/27: existing ROOT 05+ and Capstone state is untouched by this phase ──────────────
|
||||
await test('ROOT 06 stays locked behind ROOT 05 exactly as before this phase (unaffected by the new 01-04 chain)', async () => {
|
||||
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-06-kubernetes-application-workloads').locked, true);
|
||||
});
|
||||
|
||||
// ── 17: optional activities still behave correctly elsewhere in the curriculum ──────────
|
||||
await test('the pre-existing is_required rule for other courses is unaffected by this phase', async () => {
|
||||
const mod = moduleById(structure, 'root-10-m01-health-probes-and-troubleshooting');
|
||||
assert.equal(mod.activities.filter((a) => a.is_required === false).length, 1, 'Phase 6C\'s optional readiness-probe lab must still be exactly 1');
|
||||
});
|
||||
|
||||
// ── 18/19: self-paced + instructor rendering, and instructor-note exclusion ─────────────
|
||||
await test('self-paced ROOT 01 content never defers to a live instructor', async () => {
|
||||
for (const modId of ROOT01_MODULES) {
|
||||
const mod = moduleById(structure, modId);
|
||||
for (const a of mod.activities) {
|
||||
if (!a.body) continue;
|
||||
assert.ok(!/ask your instructor/i.test(a.body) && !/your instructor will explain/i.test(a.body), `${a.activity_id} must not defer explanation to a live instructor`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await test('instructor notes on ROOT 01 content are visible to instructors and never to students', async () => {
|
||||
const lesson = findActivity(structure, 'root-01-m06-a01-lesson');
|
||||
assert.ok(lesson.instructor_notes && lesson.instructor_notes.length > 0);
|
||||
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
|
||||
const studentLesson = findActivity(body.courses, 'root-01-m06-a01-lesson');
|
||||
assert.equal(studentLesson.instructor_notes, undefined);
|
||||
});
|
||||
|
||||
// ── 20: enrollment entitlement / RBAC ────────────────────────────────────────────────────
|
||||
const { body: strayInstructor } = await owner.request('POST', '/api/admin/instructors', { email: `e2e6d-stray-${RUN_ID}@example.com`, display_name: 'Stray Instructor 6D', role: 'instructor', password: 'StrayInstructorPass6D!' });
|
||||
const stray = makeSession(GW);
|
||||
await stray.request('POST', '/api/auth/instructor/login', { email: `e2e6d-stray-${RUN_ID}@example.com`, password: 'StrayInstructorPass6D!' });
|
||||
await test('a plain instructor with no relationship to the student cannot see ROOT 01 progress', async () => {
|
||||
const { status } = await stray.request('GET', `/api/admin/students/${casey.studentId}/courses?program_id=${PROGRAM}`);
|
||||
assert.equal(status, 403);
|
||||
});
|
||||
await test('owner/admin can see any self-paced student\'s ROOT 01 progress', async () => {
|
||||
const { status, body } = await owner.request('GET', `/api/admin/students/${casey.studentId}/courses?program_id=${PROGRAM}`);
|
||||
assert.equal(status, 200);
|
||||
assert.equal(courseById(body, 'root-01-computing-linux-foundations').complete, true);
|
||||
});
|
||||
|
||||
// ── 21/23: persistence + cross-student isolation ────────────────────────────────────────
|
||||
await test('ROOT 01 progress survives logout and a fresh login', async () => {
|
||||
await casey.session.request('POST', '/api/logout');
|
||||
const returning = makeSession(GW);
|
||||
await returning.request('POST', '/api/student/login', { identifier: `e2e6d-Casey-${RUN_ID}`, password: 'CaseyPassword123!' });
|
||||
const { body } = await returning.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').complete, true);
|
||||
});
|
||||
|
||||
await test('a second student sees independent, empty ROOT 01 progress (cross-student isolation)', async () => {
|
||||
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
|
||||
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').complete, false);
|
||||
});
|
||||
|
||||
// ── 25: content version stability ───────────────────────────────────────────────────────
|
||||
await test('bumping a ROOT 01 module\'s version does not drop its content', async () => {
|
||||
psql(`UPDATE course_modules SET version = version + 1 WHERE module_id = 'root-01-m06-users-and-permissions-foundations'`);
|
||||
const { body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
|
||||
const mod = moduleById(body, 'root-01-m06-users-and-permissions-foundations');
|
||||
assert.equal(mod.activities.length, 7, 'a version bump must never drop activities');
|
||||
assert.ok(mod.activities[0].body && mod.activities[0].body.length > 300);
|
||||
});
|
||||
|
||||
// ── content validation as part of this suite ────────────────────────────────────────────
|
||||
await test('infra/scripts/validate-course-content.js reports zero errors against the current database', () => {
|
||||
execSync('node infra/scripts/validate-course-content.js', { cwd: `${__dirname}/../..`, stdio: 'pipe' });
|
||||
});
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error('FATAL', e); process.exit(1); });
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "linux-exit-codes-mcq",
|
||||
"title": "Reading Exit Code Conventions",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "mcq",
|
||||
"weight": 3,
|
||||
"description": "## Reading Exit Code Conventions\n\nYou run `grep pattern file.txt` and it exits with code `1`, printing no error message at all. What actually happened?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "The file doesn't exist" },
|
||||
{ "id": "b", "text": "grep found no lines matching the pattern — this is grep's normal way of reporting \"no match,\" not an error" },
|
||||
{ "id": "c", "text": "You don't have permission to read the file" },
|
||||
{ "id": "d", "text": "The command is broken and needs to be reinstalled" }
|
||||
],
|
||||
"correct_option": "b",
|
||||
"explanation": "grep's exit code means \"did I find at least one match,\" not \"did something go wrong.\" Exit 1 with no error message is grep's normal, expected way of reporting zero matches — a very common early surprise, since silence plus a non-zero code doesn't always mean a real failure occurred.",
|
||||
"hints": [],
|
||||
"setup_commands": [],
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": []
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "linux-files-and-text",
|
||||
"title": "Extract the Signal From a Log File",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Extract the Signal From a Log File\n\nA real application log (`/srv/text-lab/app.log`) has been generated for you, mixing `INFO` and `ERROR` lines. Reading raw logs line-by-line doesn't scale — filtering does.\n\n**Your task:**\n\n1. Use `grep` to extract only the `ERROR` lines from `/srv/text-lab/app.log`\n2. Redirect the result into a new file: `/srv/text-lab/errors.txt`\n3. Count how many ERROR lines there were and save the count to `/srv/text-lab/error-count.txt`",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Filtering and redirecting",
|
||||
"body": "`grep PATTERN file > newfile` writes only matching lines to a new file. Pipe to `wc -l` to count them.",
|
||||
"command": "grep ERROR /srv/text-lab/app.log > /srv/text-lab/errors.txt\ngrep -c ERROR /srv/text-lab/app.log > /srv/text-lab/error-count.txt"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/text-lab && printf 'INFO: boot ok\\nERROR: disk full\\nINFO: retry\\nERROR: timeout\\nINFO: done\\n' > /srv/text-lab/app.log" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "errors.txt contains only ERROR lines", "command": "grep -vc ERROR /srv/text-lab/errors.txt || true", "expected_output": "0", "match": "exact" },
|
||||
{ "description": "errors.txt has exactly the 2 real ERROR lines", "command": "wc -l < /srv/text-lab/errors.txt | tr -d ' '", "expected_output": "2", "match": "exact" },
|
||||
{ "description": "error-count.txt records the count", "command": "cat /srv/text-lab/error-count.txt", "expected_output": "2", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/text-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"id": "linux-filesystem-navigation",
|
||||
"title": "Build a Real Project Directory Structure",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 3,
|
||||
"description": "## Build a Real Project Directory Structure\n\nEvery real codebase has a directory structure that means something. Practice building and navigating one.\n\n**Your task:**\n\nUnder `/srv/fs-lab/`, create:\n\n1. A directory tree: `project/src`, `project/docs`, `project/tests`\n2. A hidden file at `project/.config` containing exactly the text `ready`\n3. Navigate into `project/src` using a RELATIVE path from `/srv/fs-lab` (not an absolute path) to confirm you understand `cd project/src` vs `cd /srv/fs-lab/project/src`",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Creating the tree in one command",
|
||||
"body": "`mkdir -p` creates every intermediate directory in one call, and brace expansion lets you create three siblings at once.",
|
||||
"command": "mkdir -p /srv/fs-lab/project/{src,docs,tests}\necho -n 'ready' > /srv/fs-lab/project/.config"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/fs-lab" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "project/src exists", "command": "[ -d /srv/fs-lab/project/src ] && echo yes", "expected_output": "yes", "match": "exact" },
|
||||
{ "description": "project/docs exists", "command": "[ -d /srv/fs-lab/project/docs ] && echo yes", "expected_output": "yes", "match": "exact" },
|
||||
{ "description": "project/tests exists", "command": "[ -d /srv/fs-lab/project/tests ] && echo yes", "expected_output": "yes", "match": "exact" },
|
||||
{ "description": "hidden .config file has the right content", "command": "cat /srv/fs-lab/project/.config", "expected_output": "ready", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/fs-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"id": "linux-inspect-hardware",
|
||||
"title": "Inspect What This Machine Actually Has",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 3,
|
||||
"description": "## Inspect What This Machine Actually Has\n\nThe kernel manages CPU, memory, and storage on behalf of every process running here — before you go further, look at what's actually available.\n\n**Your task:**\n\n1. Find the number of CPUs available (`nproc`)\n2. Find the total memory in MB (`free -m`)\n3. Find the size of the root filesystem in GB (`df -BG /`)\n4. Create a file at `/srv/hardware-lab/report.txt` with exactly three lines in this format (replace `<n>` with the real numbers you found):\n\n```\nCPUS=<n>\nMEM_MB=<n>\nDISK_GB=<n>\n```",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Getting each number",
|
||||
"body": "`nproc` alone prints the CPU count. `free -m` prints a table — the second column of the `Mem:` row is total memory in MB. `df -BG /` prints a table — the second column of the second row is the root filesystem size in GB (the number, not including the trailing G).",
|
||||
"command": "nproc; free -m; df -BG /"
|
||||
},
|
||||
{
|
||||
"title": "Writing the report",
|
||||
"body": "Use a heredoc or several `echo` lines with `>>` to build the file.",
|
||||
"command": "mkdir -p /srv/hardware-lab && cat <<EOF > /srv/hardware-lab/report.txt\nCPUS=$(nproc)\nMEM_MB=$(free -m | awk '/Mem:/ {print $2}')\nDISK_GB=$(df -BG / | awk 'NR==2{gsub(\"G\",\"\",$2); print $2}')\nEOF"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/hardware-lab" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "report.txt exists and has a CPUS line", "command": "grep -E '^CPUS=[0-9]+$' /srv/hardware-lab/report.txt", "expected_output": "^CPUS=[0-9]+$", "match": "regex" },
|
||||
{ "description": "report.txt has a MEM_MB line", "command": "grep -E '^MEM_MB=[0-9]+$' /srv/hardware-lab/report.txt", "expected_output": "^MEM_MB=[0-9]+$", "match": "regex" },
|
||||
{ "description": "report.txt has a DISK_GB line", "command": "grep -E '^DISK_GB=[0-9]+$' /srv/hardware-lab/report.txt", "expected_output": "^DISK_GB=[0-9]+$", "match": "regex" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/hardware-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "linux-permissions-foundations",
|
||||
"title": "Fix a Real Permission Denial",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Fix a Real Permission Denial\n\nA second user, `labuser`, already exists on this system. A file at `/srv/perm-lab/data.txt` currently has permissions `600` (owner read/write only) — `labuser` cannot read it. Confirm the denial is real, then fix it correctly.\n\n**Your task:**\n\n1. As `labuser`, try to read `/srv/perm-lab/data.txt` and see the permission denial for yourself: `su - labuser -c \"cat /srv/perm-lab/data.txt\"`\n2. Change the file's permissions so it's readable by everyone but still only writable by its owner (this is exactly what `644` means)\n3. Confirm `labuser` can now read it",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Reading permissions as a number",
|
||||
"body": "Each of the three permission groups (owner/group/other) is one octal digit: read=4, write=2, execute=1, summed. `644` = owner read+write (6), group read-only (4), other read-only (4).",
|
||||
"command": "stat -c '%a %U' /srv/perm-lab/data.txt"
|
||||
},
|
||||
{
|
||||
"title": "Fixing it",
|
||||
"body": "`chmod 644 <file>` sets exactly that permission pattern.",
|
||||
"command": "chmod 644 /srv/perm-lab/data.txt"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "id labuser >/dev/null 2>&1 || useradd -m labuser" },
|
||||
{ "command": "mkdir -p /srv/perm-lab && chmod 755 /srv/perm-lab" },
|
||||
{ "command": "echo sensitive > /srv/perm-lab/data.txt && chmod 600 /srv/perm-lab/data.txt && chown root:root /srv/perm-lab/data.txt" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "data.txt is now exactly 644", "command": "stat -c '%a' /srv/perm-lab/data.txt", "expected_output": "644", "match": "exact" },
|
||||
{ "description": "labuser can actually read it now", "command": "su - labuser -c 'cat /srv/perm-lab/data.txt' 2>&1", "expected_output": "sensitive", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/perm-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "linux-permissions-mcq",
|
||||
"title": "Choosing the Minimal Permission",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "mcq",
|
||||
"weight": 3,
|
||||
"description": "## Choosing the Minimal Permission\n\nA private SSH key file should be readable and writable by its owner only — nobody else should be able to read, write, or execute it. Which `chmod` octal value is correct?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "777" },
|
||||
{ "id": "b", "text": "644" },
|
||||
{ "id": "c", "text": "600" },
|
||||
{ "id": "d", "text": "444" }
|
||||
],
|
||||
"correct_option": "c",
|
||||
"explanation": "600 grants the owner read+write (4+2=6) and nothing at all to group or other (0, 0) — the minimum needed, and the actual permission SSH itself enforces for private key files. 644 incorrectly leaves the file readable by everyone; 777 grants full access to everyone, the opposite of least privilege.",
|
||||
"hints": [],
|
||||
"setup_commands": [],
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": []
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "linux-process-management",
|
||||
"title": "Find and Stop a Runaway Process",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Find and Stop a Runaway Process\n\nA background process has been started for you and its PID recorded at `/srv/proc-lab/target.pid`. Every process in Kubernetes and Docker is, underneath, exactly this: a PID the kernel is tracking.\n\n**Your task:**\n\n1. Confirm the process is currently running (`kill -0 <pid>` succeeds silently if a process exists)\n2. Send it a termination signal: `kill <pid>` (using the PID recorded in the file, not a hardcoded number — it will be different every time)\n3. Confirm it is actually gone afterward",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Reading the PID and checking it's alive",
|
||||
"body": "`kill -0` sends no real signal — it only checks whether the process exists, which is why it's the standard way to test \"is this PID still running\" without side effects.",
|
||||
"command": "cat /srv/proc-lab/target.pid\nkill -0 $(cat /srv/proc-lab/target.pid) && echo RUNNING"
|
||||
},
|
||||
{
|
||||
"title": "Terminating it",
|
||||
"body": "A plain `kill <pid>` sends SIGTERM, the polite \"please shut down\" signal — the correct first choice before ever reaching for `kill -9`.",
|
||||
"command": "kill $(cat /srv/proc-lab/target.pid)"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/proc-lab && (sleep 900 & echo $! > /srv/proc-lab/target.pid)" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "the target process has actually been terminated", "command": "kill -0 $(cat /srv/proc-lab/target.pid) 2>/dev/null && echo RUNNING || echo GONE", "expected_output": "GONE", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "kill -9 $(cat /srv/proc-lab/target.pid) 2>/dev/null; rm -rf /srv/proc-lab" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "linux-shell-basics",
|
||||
"title": "Reading Exit Codes Like an Engineer",
|
||||
"category": "Linux Fundamentals",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 3,
|
||||
"description": "## Reading Exit Codes Like an Engineer\n\nEvery command you run returns an exit code — `0` means success, anything else means failure. Scripts, CI pipelines, and Kubernetes probes all rely on this exact convention.\n\n**Your task:**\n\n1. Run a command that succeeds (e.g. `true`, or `ls` on a directory that exists) and save its exit code to `/srv/shell-lab/success-exit-code.txt`\n2. Run a command that fails (e.g. `ls` on a path that does NOT exist) and save its exit code to `/srv/shell-lab/fail-exit-code.txt`\n3. Look up the manual page for `ls` (`man ls`) and find one option you didn't know about",
|
||||
"hints": [
|
||||
{
|
||||
"title": "Capturing an exit code",
|
||||
"body": "The special variable `$?` holds the exit code of the LAST command that ran — capture it immediately, before running anything else.",
|
||||
"command": "mkdir -p /srv/shell-lab\ntrue\necho $? > /srv/shell-lab/success-exit-code.txt\nls /this-path-does-not-exist 2>/dev/null\necho $? > /srv/shell-lab/fail-exit-code.txt"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/shell-lab" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "success-exit-code.txt records 0", "command": "cat /srv/shell-lab/success-exit-code.txt", "expected_output": "0", "match": "exact" },
|
||||
{ "description": "fail-exit-code.txt records a non-zero code", "command": "cat /srv/shell-lab/fail-exit-code.txt", "expected_output": "^[1-9][0-9]*$", "match": "regex" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/shell-lab" }
|
||||
]
|
||||
}
|
||||
@@ -58,6 +58,28 @@ for i in $(seq 1 60); do
|
||||
done
|
||||
OK "k3s API server is up"
|
||||
|
||||
# ── 1b. Start a nested Docker daemon (ROOT 04) ───────────────────────────────
|
||||
# A second, independent runtime for Docker/container-fundamentals labs — see
|
||||
# docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §3. vfs storage driver deliberately avoids nesting
|
||||
# overlay-on-overlay under this container's own overlay filesystem. Best-effort and
|
||||
# non-blocking: a Docker daemon failure must never break the Kubernetes-only Labs that are the
|
||||
# vast majority of usage, so this never calls `exit 1` — only a log line.
|
||||
if command -v dockerd >/dev/null 2>&1; then
|
||||
LOG "Starting nested Docker daemon (ROOT 04 labs)..."
|
||||
dockerd --storage-driver=vfs --host=unix:///var/run/docker.sock &>/var/log/dockerd.log &
|
||||
DOCKERD_PID=$!
|
||||
for i in $(seq 1 30); do
|
||||
if docker version >/dev/null 2>&1; then
|
||||
OK "Docker daemon is up"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
if [ $i -eq 30 ]; then
|
||||
LOG "⚠ Docker daemon did not become ready in time — ROOT 04 labs will be unavailable this session, everything else is unaffected"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Symlink kubeconfig to the standard location for convenience
|
||||
mkdir -p /root/.kube
|
||||
cp "$KUBECONFIG_PATH" /root/.kube/config
|
||||
|
||||
Reference in New Issue
Block a user