#!/usr/bin/env bash
# Real demos for P007 "The 7 Levels of Docker". Runs on render01 (Docker).
# The viewer's machine is a clean "laptop": Docker-in-Docker (docker:29.9-dind, hostname laptop)
# with a normal user, ana, in the docker group. It starts with no images, so `docker image ls`
# shows only what the video pulls or builds. Its pulls go through a local pull-through cache of
# Docker Hub (registry:3, kept between runs in a volume), so re-runs don't hit Docker Hub's pull
# limit; the commands and their output are what any pull shows.
# Every command is typed into one real interactive bash (a pty) as ana, in the folder the prompt
# shows. The screen is rebuilt with a terminal emulator (pyte), so live progress output (docker
# pull, docker build, docker compose) is recorded as its final screen, as a viewer would see it.
# Output goes to demo_output.json; build_script.py reads every number from it.
set -euo pipefail
cd "$(dirname "$0")"
NET=ce-p007
LAPTOP=ce-p007-laptop
MIRROR=ce-p007-mirror
W=/tmp/ce-p007
cleanup() { docker rm -f $LAPTOP $MIRROR >/dev/null 2>&1 || true; }
cleanup
trap cleanup EXIT
rm -rf $W && mkdir -p $W/img $W/py

# pyte (a VT100 screen in Python) for the recorder; render01's system Python has no pip.
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -v $W/py:/out python:3.13-slim pip install -q --target /out pyte==0.8.2 >/dev/null 2>&1

# The laptop: official Docker-in-Docker plus bash, curl and the user ana.
cat > $W/img/Dockerfile <<'EOF'
FROM docker:29.9-dind
RUN apk add --no-cache bash curl \
 && (getent group docker >/dev/null || addgroup -S docker) \
 && adduser -D -s /bin/bash -G docker ana
EOF
docker build -q -t ce-p007-dind $W/img >/dev/null
docker network inspect $NET >/dev/null 2>&1 || docker network create $NET >/dev/null
docker run -d --name $MIRROR --network $NET -v ce-p007-mirror:/var/lib/registry \
  -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io registry:3 >/dev/null
docker run -d --privileged --name $LAPTOP --hostname laptop --network $NET ce-p007-dind \
  --registry-mirror=http://$MIRROR:5000 --insecure-registry=$MIRROR:5000 >/dev/null
for i in $(seq 60); do docker exec $LAPTOP docker info >/dev/null 2>&1 && break; sleep 1; done
docker exec $LAPTOP docker info >/dev/null
docker cp lab/. $LAPTOP:/home/ana/
docker exec $LAPTOP chown -R ana /home/ana

PYTHONPATH=$W/py python3 - "$LAPTOP" <<'PY'
import fcntl, json, os, pty, re, select, struct, subprocess, sys, termios, time
import pyte
LAPTOP = sys.argv[1]
COLS = 80      # BuildKit and Compose draw their progress to the terminal width: 80 keeps it compact
WIDE = 200     # everything else: no soft wraps (an 80-column wrap split "server" mid-word in curl's error)
READY = re.compile(rb"__READY__:(.*?):__ ")
steps = {}

def screen_text(raw: bytes, cols: int) -> str:
    """What a terminal `cols` wide shows after this output (progress redraws resolved)."""
    scr = pyte.Screen(cols, 600)
    stream = pyte.ByteStream(scr)
    stream.feed(raw.replace(b"\n", b"\r\n").replace(b"\r\r\n", b"\r\n"))
    lines = [l.rstrip() for l in scr.display]
    while lines and not lines[-1]:
        lines.pop()
    while lines and not lines[0]:
        lines.pop(0)
    return "\n".join(lines)

class Shell:
    def __init__(self):
        pid, fd = pty.fork()
        if pid == 0:
            fcntl.ioctl(0, termios.TIOCSWINSZ, struct.pack("HHHH", 50, COLS, 0, 0))
            os.execvp("docker", ["docker", "exec", "-it", "-u", "ana", "-w", "/home/ana", "-e", "HOME=/home/ana",
                                 "-e", "TERM=xterm", "-e", f"COLUMNS={COLS}", "-e", "LINES=50", LAPTOP, "bash", "-i"])
        self.fd, self.where, self.cols = fd, "~", COLS
        os.write(fd, b"stty -echo; stty cols 80 rows 50; PS1='__REA''DY__:\\w:__ '\n")
        self.read()

    def read(self, timeout=900):
        buf, end = b"", time.time() + timeout
        while time.time() < end:
            if select.select([self.fd], [], [], 0.3)[0]:
                buf += os.read(self.fd, 65536)
                m = None
                for m in READY.finditer(buf):
                    pass
                if m:
                    self.where = m.group(1).decode()
                    return buf[:m.start()]
        raise SystemExit(f"timed out; last output: {buf[-600:]!r}")

    def run(self, key, cmd, cols=WIDE):
        if cols != self.cols:  # off-screen: resize the terminal, like widening the window
            os.write(self.fd, f"stty cols {cols}\n".encode())
            self.read()
            self.cols = cols
        shown = f"ana@laptop:{self.where}$ {cmd}"
        os.write(self.fd, cmd.encode() + b"\n")
        t = time.time()
        out = screen_text(self.read(), self.cols)
        steps[key] = {"cmd": shown, "out": out, "seconds": round(time.time() - t, 1)}
        print(f"--- {key} ({time.time()-t:.1f}s)\n{shown}\n{out}\n", flush=True)
        return out

def quiet(cmd):
    subprocess.run(["docker", "exec", "-u", "ana", LAPTOP, "sh", "-c", cmd], check=True, capture_output=True)

s = Shell()
# Hook and level 3 use nginx; the hook is level 3's run, shown first (build_script.py re-shows it).
# Level 1: containers
s.run("hello", "docker run hello-world")
s.run("os_laptop", "head -2 /etc/os-release")
s.run("os_ubuntu", "docker run --rm ubuntu:24.04 head -2 /etc/os-release")
s.run("kernel_laptop", "uname -r")
s.run("kernel_ubuntu", "docker run --rm ubuntu:24.04 uname -r")
s.run("ps_alpine", "docker run --rm alpine:3.24 ps")
# Level 2: images
s.run("pull_nginx", "docker pull nginx:1.31-alpine")
s.run("images", "docker image ls")
s.run("web1", "docker run -d --name web1 nginx:1.31-alpine")
s.run("web2", "docker run -d --name web2 nginx:1.31-alpine")
s.run("ps_two", "docker ps --format \"table {{.Names}}\\t{{.Image}}\\t{{.Ports}}\"")
# Level 3: ports
s.run("curl_refused", "curl localhost:8080")
s.run("web_port", "docker run -d -p 8080:80 --name web nginx:1.31-alpine")
s.run("curl_web", "curl -s localhost:8080 | grep title")
s.run("ps_ports", "docker ps --format \"table {{.Names}}\\t{{.Image}}\\t{{.Ports}}\"")
s.run("rm_webs", "docker rm -f web1 web2 web")
# Level 4: build your own image
s.run("cd_hello", "cd hello")
s.run("cat_dockerfile", "cat Dockerfile")
s.run("build1", "docker build -t hello-app .", cols=COLS)
s.run("edit_app", "sed -i 's/from a container/from my own image/' app.py")
s.run("build2", "docker build -t hello-app .", cols=COLS)
s.run("run_hello", "docker run -d -p 8000:8000 --name hello hello-app")
time.sleep(2)
s.run("curl_hello", "curl localhost:8000")
s.run("rm_hello", "docker rm -f hello")
s.run("cd_home1", "cd ..")
# Level 5: volumes
s.run("note_write", "docker run --name box alpine:3.24 sh -c 'echo first note > /note.txt'")
s.run("note_rm", "docker rm box")
s.run("note_gone", "docker run --rm alpine:3.24 cat /note.txt")
s.run("vol_create", "docker volume create notes")
s.run("vol_write", "docker run --rm -v notes:/data alpine:3.24 sh -c 'echo first note > /data/note.txt'")
s.run("vol_read", "docker run --rm -v notes:/data alpine:3.24 cat /data/note.txt")
s.run("vol_ls", "docker volume ls")
# Level 6: Compose
s.run("cd_counter", "cd counter")
s.run("cat_compose", "cat compose.yaml")
s.run("cat_counter", "cat app.py")
s.run("up1", "docker compose up -d", cols=COLS)
time.sleep(3)
s.run("count1", "curl localhost:8000")
s.run("count2", "curl localhost:8000")
s.run("down", "docker compose down", cols=COLS)
s.run("up2", "docker compose up -d", cols=COLS)
time.sleep(3)
s.run("count3", "curl localhost:8000")
s.run("down2", "docker compose down", cols=COLS)
s.run("cd_home2", "cd ..")
# Level 7: small, safe images
s.run("cd_go", "cd go-hello")
s.run("cat_big", "cat Dockerfile.big")
s.run("build_big", "docker build -t hello-go:big -f Dockerfile.big .", cols=COLS)
s.run("cat_small", "cat Dockerfile")
s.run("build_small", "docker build -t hello-go:small .", cols=COLS)
s.run("images_go", "docker image ls hello-go")
s.run("run_big", "docker run --rm hello-go:big")
s.run("run_small", "docker run --rm hello-go:small")
s.run("whoami_big", "docker run --rm hello-go:big whoami")
s.run("whoami_small", "docker run --rm hello-go:small whoami")
s.run("images_all", "docker image ls")

ver = subprocess.run(["docker", "exec", LAPTOP, "docker", "version", "--format", "{{.Client.Version}} / {{.Server.Version}}"],
                     capture_output=True, text=True).stdout.strip()
compose = subprocess.run(["docker", "exec", LAPTOP, "docker", "compose", "version", "--short"], capture_output=True, text=True).stdout.strip()
steps["_meta"] = {"cmd": "", "out": json.dumps({"docker": ver, "compose": compose, "run_at": time.strftime("%Y-%m-%d %H:%M %Z")})}
json.dump(steps, open("demo_output.json", "w"), indent=1)
print("saved demo_output.json")
PY
