Coverage for app/services/packer_discovery.py: 100.00%

37 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-25 16:05 +0000

1"""Discover Packer templates inside a cloned app repository. 

2 

3Two layouts are supported: 

4 

51. Legacy single-template layout:: 

6 

7 packer/template.pkr.hcl 

8 packer/variables.pkr.hcl 

9 

10 Produces a single ``_PackerTemplate(key="default", ...)``. This is 

11 the layout used by single-image apps: same image name 

12 (``<app_id>-<tag>``), same lock key, same phase names without any 

13 ``[key]`` suffix. 

14 

152. Multi-template subdirectory layout:: 

16 

17 packer/<key>/template.pkr.hcl 

18 packer/<key>/variables.pkr.hcl (optional) 

19 packer/<other>/template.pkr.hcl 

20 packer/_common/scripts/... (ignored — no template.pkr.hcl) 

21 

22 Produces one ``_PackerTemplate`` per subdirectory, sorted by key. 

23 Subdirectories without a ``template.pkr.hcl`` are silently ignored, 

24 which lets templates share helper files under ``packer/_common`` 

25 or ``packer/scripts`` without forcing the discovery to know about 

26 them. 

27 

28Hard errors (``PackerTemplateDiscoveryError``): 

29 

30* Both legacy file AND one or more subdirectory templates present. 

31 The two layouts are mutually exclusive; mixing them would silently 

32 bias which template a deploy sees, so we refuse to guess. 

33* A subdirectory's name does not match ``[a-z][a-z0-9_-]{0,30}``. The 

34 key becomes part of an image name (``<app_id>-<key>-<tag>``) and a 

35 terraform variable suffix (``image_name_<key>``), both of which have 

36 stricter syntax than a generic directory name — surfacing the 

37 problem as a hard error is friendlier than silently ignoring a 

38 typo'd subdirectory. 

39 

40The module is intentionally self-contained — only stdlib imports — 

41so the worker can reuse the exact same discovery logic without 

42dragging in any web framework deps. 

43""" 

44 

45from __future__ import annotations 

46 

47import os 

48import re 

49from dataclasses import dataclass 

50 

51_TEMPLATE_KEY_RE = re.compile(r"^[a-z][a-z0-9_-]{0,30}$") 

52 

53 

54@dataclass 

55class _PackerTemplate: 

56 """A single Packer template discovered under ``packer/``. 

57 

58 Attributes: 

59 key: Stable identifier for the template. ``"default"`` for the 

60 legacy single-template layout; the subdirectory name for 

61 the multi-template layout. 

62 template_path: Absolute path to the ``template.pkr.hcl`` file. 

63 variables_path: Absolute path to the (optional) sibling 

64 ``variables.pkr.hcl``. Callers must check ``os.path.isfile`` 

65 before reading — not every template declares variables. 

66 """ 

67 

68 key: str 

69 template_path: str 

70 variables_path: str 

71 

72 

73class PackerTemplateDiscoveryError(ValueError): 

74 """Raised when the ``packer/`` layout can't be interpreted unambiguously.""" 

75 

76 

77def _discover_packer_templates(repo_path: str) -> list[_PackerTemplate]: 

78 """Walk ``<repo_path>/packer/`` and return the templates it contains. 

79 

80 See the module docstring for the full layout / error rules. Returns 

81 an empty list when there is no ``packer/`` directory or the 

82 directory contains no recognisable templates (subdirectories 

83 without ``template.pkr.hcl`` are ignored, not an error). 

84 """ 

85 packer_dir = os.path.join(repo_path, "packer") 

86 if not os.path.isdir(packer_dir): 

87 return [] 

88 

89 legacy_template = os.path.join(packer_dir, "template.pkr.hcl") 

90 has_legacy = os.path.isfile(legacy_template) 

91 

92 multi_templates: list[_PackerTemplate] = [] 

93 bad_keys: list[str] = [] 

94 for entry in sorted(os.listdir(packer_dir)): 

95 sub = os.path.join(packer_dir, entry) 

96 if not os.path.isdir(sub): 

97 continue 

98 tmpl = os.path.join(sub, "template.pkr.hcl") 

99 if not os.path.isfile(tmpl): 

100 continue 

101 if not _TEMPLATE_KEY_RE.match(entry): 

102 bad_keys.append(entry) 

103 continue 

104 multi_templates.append( 

105 _PackerTemplate( 

106 key=entry, 

107 template_path=tmpl, 

108 variables_path=os.path.join(sub, "variables.pkr.hcl"), 

109 ) 

110 ) 

111 

112 if bad_keys: 

113 raise PackerTemplateDiscoveryError( 

114 f"Packer template subdirectories with invalid keys (must match " f"[a-z][a-z0-9_-]{{0,30}}): {bad_keys}" 

115 ) 

116 

117 if has_legacy and multi_templates: 

118 raise PackerTemplateDiscoveryError( 

119 "App repository has BOTH packer/template.pkr.hcl (legacy layout) AND " 

120 f"packer/<key>/template.pkr.hcl subdirectories ({[t.key for t in multi_templates]}). " 

121 "Choose one layout — remove the legacy file or the subdirectories." 

122 ) 

123 

124 if has_legacy: 

125 return [ 

126 _PackerTemplate( 

127 key="default", 

128 template_path=legacy_template, 

129 variables_path=os.path.join(packer_dir, "variables.pkr.hcl"), 

130 ) 

131 ] 

132 

133 return multi_templates