feat(L08): add Docker fundamentals card
This commit is contained in:
Executable
+68
@@ -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()
|
||||
Reference in New Issue
Block a user