Coverage for dockerfiles / runtime_smoke.py: 100.00%

53 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +0000

1"""Runtime smoke for the distroless service images. 

2 

3Runs as the final Dockerfile stage's only RUN — exec form, as the runtime 

4user — with one argument: the service entry module. It proves builder-to- 

5scratch parity programmatically instead of via a hand-maintained module 

6list: 

7 

8- every stdlib C extension that was importable in the builder stage must 

9 import here too. ``build_scratch_rootfs.py`` records that set in 

10 ``runtime_smoke_manifest.json`` next to this file, derived by actually 

11 importing everything under ``lib-dynload`` in the builder. Import parity 

12 is a strictly stronger completeness proof than the ldd closure alone: it 

13 also catches libraries reached only via ``dlopen``, which ldd cannot see. 

14- the service entry module must import (the full third-party graph); 

15- ``getpass`` must resolve the synthesized passwd identity for the runtime 

16 uid (NSS wiring); 

17- OpenSSL's default trust store must load CA certificates (TLS to AWS); 

18- ``zoneinfo`` must resolve from the staged tzdata. 

19 

20Every failure is collected and reported, then the process exits non-zero so 

21the image build — including CDK deploys — fails instead of the pod. The 

22script and its manifest live in the builder stage's ``/opt/build`` and reach 

23the final stage through a BuildKit bind mount scoped to the smoke RUN alone: 

24the deployed image ships neither of them (deleting files in a later layer 

25would only hide them — layers are additive — so they are never written into 

26the image at all). The manifest is located relative to ``__file__``, so the 

27pair works from any mount target. 

28 

29Only ``json``, ``sys``, ``importlib``, and ``pathlib`` are imported at module 

30scope; everything under test (``ssl``, ``getpass``, ``zoneinfo``, the stdlib 

31extensions, the entry module) is imported inside guarded sections so a single 

32breakage cannot mask the rest of the report. 

33""" 

34 

35from __future__ import annotations 

36 

37import importlib 

38import json 

39import sys 

40from pathlib import Path 

41 

42 

43def main() -> int: 

44 if len(sys.argv) != 2: 

45 print("usage: runtime_smoke.py <service-entry-module>", file=sys.stderr) 

46 return 2 

47 entry_module = sys.argv[1] 

48 

49 manifest_path = Path(__file__).with_name("runtime_smoke_manifest.json") 

50 manifest = json.loads(manifest_path.read_text(encoding="utf-8")) 

51 extensions: list[str] = manifest["stdlib_extensions"] 

52 expected_user: str = manifest["runtime_user"] 

53 

54 failures: list[str] = [] 

55 if not extensions: 

56 failures.append("manifest lists no stdlib extensions; the builder probe broke") 

57 

58 for name in extensions: 

59 try: 

60 # Dynamic import IS the check: the names come from the manifest 

61 # this build's rootfs script derived (root-written, read-only at 

62 # runtime), never from untrusted input. 

63 # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import 

64 importlib.import_module(name) 

65 except BaseException as exc: # noqa: BLE001 — aggregate every breakage 

66 failures.append(f"stdlib extension {name}: {type(exc).__name__}: {exc}") 

67 

68 try: 

69 # The entry module is a literal baked into each Dockerfile's smoke 

70 # RUN, not user input; importing it dynamically is this script's job. 

71 # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import 

72 importlib.import_module(entry_module) 

73 except BaseException as exc: # noqa: BLE001 

74 failures.append(f"entry module {entry_module}: {type(exc).__name__}: {exc}") 

75 

76 actual_user = "<unresolved>" 

77 try: 

78 import getpass 

79 

80 actual_user = getpass.getuser() 

81 if actual_user != expected_user: 

82 failures.append(f"runtime user: expected {expected_user!r}, got {actual_user!r}") 

83 except BaseException as exc: # noqa: BLE001 

84 failures.append(f"runtime identity lookup: {type(exc).__name__}: {exc}") 

85 

86 try: 

87 import ssl 

88 

89 ca_count = ssl.create_default_context().cert_store_stats()["x509_ca"] 

90 if ca_count <= 0: 

91 failures.append("OpenSSL default trust store loaded zero CA certificates") 

92 except BaseException as exc: # noqa: BLE001 

93 failures.append(f"CA trust store: {type(exc).__name__}: {exc}") 

94 

95 try: 

96 import zoneinfo 

97 

98 zoneinfo.ZoneInfo("UTC") 

99 except BaseException as exc: # noqa: BLE001 

100 failures.append(f"zoneinfo/tzdata: {type(exc).__name__}: {exc}") 

101 

102 if failures: 

103 print(f"distroless runtime smoke FAILED ({len(failures)} problem(s)):", file=sys.stderr) 

104 for failure in failures: 

105 print(f" - {failure}", file=sys.stderr) 

106 return 1 

107 

108 print( 

109 f"distroless runtime smoke OK: {len(extensions)} stdlib extensions, " 

110 f"entry module {entry_module}, user {actual_user}, CA trust and tzdata present" 

111 ) 

112 return 0 

113 

114 

115if __name__ == "__main__": 

116 raise SystemExit(main())