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
« 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.
3Two layouts are supported:
51. Legacy single-template layout::
7 packer/template.pkr.hcl
8 packer/variables.pkr.hcl
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.
152. Multi-template subdirectory layout::
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)
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.
28Hard errors (``PackerTemplateDiscoveryError``):
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.
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"""
45from __future__ import annotations
47import os
48import re
49from dataclasses import dataclass
51_TEMPLATE_KEY_RE = re.compile(r"^[a-z][a-z0-9_-]{0,30}$")
54@dataclass
55class _PackerTemplate:
56 """A single Packer template discovered under ``packer/``.
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 """
68 key: str
69 template_path: str
70 variables_path: str
73class PackerTemplateDiscoveryError(ValueError):
74 """Raised when the ``packer/`` layout can't be interpreted unambiguously."""
77def _discover_packer_templates(repo_path: str) -> list[_PackerTemplate]:
78 """Walk ``<repo_path>/packer/`` and return the templates it contains.
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 []
89 legacy_template = os.path.join(packer_dir, "template.pkr.hcl")
90 has_legacy = os.path.isfile(legacy_template)
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 )
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 )
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 )
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 ]
133 return multi_templates