feat(L08): add Docker fundamentals card

This commit is contained in:
user
2026-07-21 18:20:10 +02:00
commit 2ed1b7954a
39 changed files with 7419 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Small offline state model for teaching container lifecycle; not a container engine."""
import argparse
import hashlib
import json
import shutil
from pathlib import Path
def load(path: Path):
return json.loads(path.read_text()) if path.exists() else {"images": {}, "containers": {}, "volumes": {}}
def save(path: Path, state):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n")
def build(state_path: Path, image: str, context: Path):
dockerfile = context / "Dockerfile"
lines = [line.strip() for line in dockerfile.read_text().splitlines() if line.strip() and not line.lstrip().startswith("#")]
instructions = [line.split(None, 1)[0].upper() for line in lines]
allowed = {"FROM", "LABEL", "COPY", "CMD", "ENTRYPOINT", "ENV", "WORKDIR", "USER"}
if not instructions or instructions[0] != "FROM" or "CMD" not in instructions or any(item not in allowed for item in instructions):
raise SystemExit("invalid Dockerfile for offline lesson model")
digest = hashlib.sha256()
for file_path in sorted(path for path in context.rglob("*") if path.is_file()):
digest.update(str(file_path.relative_to(context)).encode()); digest.update(b"\0"); digest.update(file_path.read_bytes())
value = "sha256:" + digest.hexdigest()
state = load(state_path); state["images"][image] = {"digest": value, "instructions": instructions}; save(state_path, state)
print(f"{value} instructions={len(instructions)}")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("state", type=Path)
sub = parser.add_subparsers(dest="command", required=True)
p = sub.add_parser("build"); p.add_argument("image"); p.add_argument("context", type=Path)
p = sub.add_parser("image-inspect"); p.add_argument("image")
p = sub.add_parser("run"); p.add_argument("name"); p.add_argument("image"); p.add_argument("--volume"); p.add_argument("--bind", type=Path)
p = sub.add_parser("inspect"); p.add_argument("name")
p = sub.add_parser("stop"); p.add_argument("name")
p = sub.add_parser("rm"); p.add_argument("name")
p = sub.add_parser("volume-create"); p.add_argument("name")
p = sub.add_parser("volume-path"); p.add_argument("name")
p = sub.add_parser("volume-rm"); p.add_argument("name")
args = parser.parse_args(); state = load(args.state)
if args.command == "build": build(args.state, args.image, args.context); return
if args.command == "image-inspect": print(json.dumps(state["images"][args.image], sort_keys=True)); return
if args.command == "volume-create":
path = args.state.parent / "volumes" / args.name; path.mkdir(parents=True, exist_ok=True); state["volumes"][args.name] = str(path); save(args.state, state); print(path); return
if args.command == "volume-path": print(state["volumes"][args.name]); return
if args.command == "volume-rm":
path = Path(state["volumes"].pop(args.name)); shutil.rmtree(path); save(args.state, state); print("removed"); return
if args.command == "run":
if args.image not in state["images"] or args.name in state["containers"]: raise SystemExit("invalid image or duplicate container")
if args.volume and args.volume not in state["volumes"]: raise SystemExit("unknown volume")
state["containers"][args.name] = {"image": args.image, "status": "running", "volume": args.volume, "bind": str(args.bind) if args.bind else None}; save(args.state, state); print("running"); return
container = state["containers"].get(args.name)
if not container: raise SystemExit("unknown container")
if args.command == "inspect": print(container["status"]); return
if args.command == "stop": container["status"] = "exited"; save(args.state, state); print("exited"); return
if args.command == "rm":
if container["status"] == "running": raise SystemExit("stop container before rm")
del state["containers"][args.name]; save(args.state, state); print("removed"); return
if __name__ == "__main__": main()