1#!/usr/bin/env python3
2import argparse
3import concurrent.futures
4import json
5from pathlib import Path
6import subprocess
7import time
8import urllib.request
9
10
11def main():
12 parser = argparse.ArgumentParser()
13 parser.add_argument("container")
14 parser.add_argument("--url", default="http://127.0.0.1:7074")
15 parser.add_argument("--clients", type=int, default=40)
16 parser.add_argument("--requests", type=int, default=1000)
17 parser.add_argument("--output", type=Path)
18 parser.add_argument("--proxy-token-file", type=Path, required=True)
19 parser.add_argument("--stop", action="store_true")
20 args = parser.parse_args()
21 info = json.loads(subprocess.check_output(["podman", "inspect", args.container]))[0]
22 host = info["HostConfig"]
23 assert info["Config"]["User"].split(":")[0] not in {"", "0", "root"}
24 assert host["ReadonlyRootfs"] is True
25 assert "no-new-privileges" in host["SecurityOpt"]
26 assert not host["Privileged"] and not host["Devices"]
27 assert host["NetworkMode"] != "host"
28 assert all(m["Destination"] in ["/run/studio-host", "/run/secrets/dashboard-proxy.token", "/data"] for m in info["Mounts"]), info["Mounts"]
29 assert next(m for m in info["Mounts"] if m["Destination"] == "/run/studio-host")["RW"] is False
30 status = subprocess.check_output(["podman", "exec", args.container, "/bin/cat", "/proc/1/status"], text=True)
31 uid = next(line.split()[1:] for line in status.splitlines() if line.startswith("Uid:"))
32 assert all(int(value) != 0 for value in uid), uid
33 for field in ["CapEff", "CapBnd", "CapPrm", "CapAmb"]:
34 assert field + ":\t0000000000000000" in status, status
35 assert "NoNewPrivs:\t1" in status, status
36 mounts = subprocess.check_output(["podman", "exec", args.container, "/bin/cat", "/proc/1/mountinfo"], text=True)
37 root = next(line.split() for line in mounts.splitlines() if line.split()[4] == "/")
38 assert "ro" in root[5].split(","), root
39 assert subprocess.run(["podman", "exec", args.container, "/bin/touch", "/escaped"], capture_output=True).returncode != 0
40 for denied in ["/var/lib/studio/nomad.token", "/run/podman/podman.sock", "/run/libvirt/libvirt-sock", "/dev/zfs", "/opt/studio/current"]:
41 assert subprocess.run(["podman", "exec", args.container, "/bin/test", "-e", denied], capture_output=True).returncode != 0, denied
42 pid = info["State"]["Pid"]
43 for namespace in ["net", "pid", "mnt"]:
44 assert Path(f"/proc/{pid}/ns/{namespace}").stat().st_ino != Path(f"/proc/self/ns/{namespace}").stat().st_ino
45
46 headers = {"User-Name": "fixture", "User-Groups": "infra-admin", "Studio-Proxy-Token": args.proxy_token_file.read_text().strip()}
47 with urllib.request.urlopen(urllib.request.Request(args.url + "/api/host", headers=headers), timeout=10) as response:
48 machine = json.load(response)
49 expected_memory = next(int(line.split()[1]) * 1024 for line in Path("/proc/meminfo").read_text().splitlines() if line.startswith("MemTotal:"))
50 expected_cores = sum(line.startswith("cpu") and line[3:4].isdigit() for line in Path("/proc/stat").read_text().splitlines())
51 assert machine["memory"] == expected_memory and machine["cores"] == expected_cores, machine
52 with urllib.request.urlopen(urllib.request.Request(args.url + "/api/live", headers=headers), timeout=10) as response:
53 for _ in range(20):
54 line = response.readline()
55 if line.startswith(b"data:"):
56 live = json.loads(line[5:])
57 break
58 else:
59 raise AssertionError("host sample did not arrive")
60 assert 0 <= live["host"]["cpu"] <= 100 and live["host"]["memory"] > 0, live
61 assert live["host"]["arc"] is not None, live
62
63 def request(_):
64 start = time.monotonic()
65 req = urllib.request.Request(args.url + "/api/storage", headers=headers)
66 with urllib.request.urlopen(req, timeout=10) as response:
67 assert json.load(response)["pool"]["state"] == "ONLINE"
68 return (time.monotonic() - start) * 1000
69
70 with concurrent.futures.ThreadPoolExecutor(max_workers=args.clients) as clients:
71 latency = sorted(clients.map(request, range(args.requests)))
72 result = {"nonroot_container": "passed", "readonly_rootfs": "passed", "zero_capabilities": "passed",
73 "no_new_privileges": "passed", "private_namespaces": "passed", "control_sockets_and_management_token_absent": "passed",
74 "host_identity_outside_container_quota": "passed", "host_live_sample": "passed",
75 "storage_requests": len(latency), "storage_clients": args.clients,
76 "storage_p95_ms": round(latency[int(len(latency) * .95)], 2), "storage_max_ms": round(latency[-1], 2)}
77 if args.stop:
78 req = urllib.request.Request(args.url + "/api/live", headers=headers)
79 with urllib.request.urlopen(req, timeout=10):
80 started = time.monotonic()
81 stopped = subprocess.run(["podman", "stop", "--time=8", args.container], capture_output=True, text=True, check=True)
82 elapsed = time.monotonic() - started
83 ended = json.loads(subprocess.check_output(["podman", "inspect", args.container]))[0]["State"]
84 assert ended["ExitCode"] == 0 and not ended["OOMKilled"], (ended, stopped.stderr)
85 assert elapsed < 8, elapsed
86 result["stop_with_active_stream"] = "passed"
87 result["stop_seconds"] = round(elapsed, 2)
88 if args.output:
89 args.output.write_text(json.dumps(result, indent=2) + "\n")
90 print(json.dumps(result))
91
92
93if __name__ == "__main__":
94 main()