cs-image-system-packer-plugin 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,36 @@
1
+ # SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import logging
6
+ from cs_image_system.base.basic.abstract_plugin_metadata import AbstractPluginMetadata
7
+ from cs_image_system.base.protocols.plugin_metadata import PluginMetadataProtocol
8
+ from cs_image_system.packer_plugin.packer_ebs_builder import PackerEbsImageBuilder, PackerGceImageBuilder
9
+ from .packer_builder import PACKER, PackerVersionChecker
10
+ from .packer_models import PACKER_EBS, PACKER_GCE, PackerEbsImageBuilderModel, PackerGceImageBuilderModel
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+ class PackerEbsTypes(AbstractPluginMetadata, PluginMetadataProtocol):
15
+ def __init__(self) -> None:
16
+ super().__init__( "1", "3.13",
17
+ {PACKER_EBS: [
18
+ PackerEbsImageBuilderModel,
19
+ PackerEbsImageBuilder,
20
+ ],
21
+ PACKER_GCE: [
22
+ PackerGceImageBuilderModel,
23
+ PackerGceImageBuilder,
24
+ ],
25
+ PACKER: [
26
+ PackerVersionChecker,
27
+ ]},
28
+ {PackerEbsImageBuilderModel: PackerEbsImageBuilder,
29
+ PackerGceImageBuilderModel: PackerGceImageBuilder}
30
+ )
31
+
32
+
33
+ def initialize():
34
+ log.debug("Initializing Packer Provider")
35
+ return PackerEbsTypes()
36
+
@@ -0,0 +1,253 @@
1
+ # SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import logging
6
+ import re
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Any, TypeVar
10
+ from warnings import deprecated
11
+
12
+ from cs_image_system.base import utils
13
+ from cs_image_system.base.basic.abstract_version_checker import AbstractVersionChecker
14
+ from cs_image_system.base.basic.asset import AssetSet
15
+ from cs_image_system.base.basic.builder_base_image import ImageBuilderBase
16
+ from cs_image_system.base.basic.builder_base_runtime import RuntimeBuilderBase
17
+ from cs_image_system.base.constants import IMAGES
18
+ from cs_image_system.base.lifecycle import ExecutionLifecyclePhase
19
+ from cs_image_system.base.models.executable import ExecutableModel
20
+ from cs_image_system.base.models.executable_adds import CFExecutables
21
+ from cs_image_system.base.models.base_image import BaseImage
22
+ from cs_image_system.base.models.image import Image
23
+ from cs_image_system.base.basic.builder_base_os import OsBuilderBase
24
+
25
+ from cs_image_system.hashicorp_utils.collector import PackerCollector, PackerVariableDecl
26
+
27
+ from .packer_models import PACKER, PackerImageBuilderModel
28
+
29
+ log = logging.getLogger(__name__)
30
+
31
+
32
+ T = TypeVar("T", bound=PackerImageBuilderModel)
33
+
34
+ class PackerImageBuilder(ImageBuilderBase[T]):
35
+ """Packer image builder implementation."""
36
+
37
+ @classmethod
38
+ def csis_name(cls) -> str:
39
+ return PACKER
40
+
41
+ @property
42
+ def model(self) -> PackerImageBuilderModel:
43
+ return self._model # type: ignore # FIXME: Type check on aisle 4!
44
+
45
+ def get_packer_source_type(self) -> str:
46
+ """The packer source type: the runtime plugin's say (EXPLORE GCP
47
+ increment 1); ``amazon-ebs`` only for a runtime with no opinion."""
48
+ from cs_image_system.base.global_context import GlobalTypeContext
49
+ rtb = GlobalTypeContext().runtime_builders.get(self.model.get_runtime_provider())
50
+ if rtb is not None:
51
+ try:
52
+ return rtb.packer_source_type()
53
+ except NotImplementedError:
54
+ log.debug(f"Runtime {rtb.get_name()} declares no packer source type; assuming amazon-ebs")
55
+ return "amazon-ebs"
56
+
57
+ def build_target_label(self, image: Image | BaseImage) -> str:
58
+ return f"{self.get_packer_source_type()}.{image.get_name()}"
59
+
60
+ def generate_items_before(
61
+ self, phase: ExecutionLifecyclePhase
62
+ ) -> AssetSet: # list[tuple[Path, str]]:
63
+ """Generate a list of items to create before a specific execution lifecycle
64
+ event."""
65
+ items: AssetSet = AssetSet()
66
+ if phase == ExecutionLifecyclePhase.IMAGE_GENERATION:
67
+ col = PackerCollector()
68
+ ws = self.get_discriminator()
69
+ for plugin in self.model.required_plugins:
70
+ col.require_plugin(ws, plugin.name,
71
+ source=getattr(plugin, "source", None),
72
+ version=getattr(plugin, "version", None))
73
+ col.declare_variable(ws, PackerVariableDecl(
74
+ name="release", type="bool", default=True,
75
+ description="Determines if the build is meant to be immutable"))
76
+ col.declare_variable(ws, PackerVariableDecl(
77
+ name="base_image_version", type="string",
78
+ env_var="BASE_IMAGE_VERSION",
79
+ description="The version of the base image to build on top of, used for tagging and versioning the image. Read from the BASE_IMAGE_VERSION environment variable at build time; empty when it is not set."))
80
+ # stage 63 (decided 2026-09-25): an image's `variables:` become packer
81
+ # variables of this builder's roots, each with its declared value as
82
+ # the default (they were read by nothing but a dead generator). The
83
+ # collector refuses one name declared twice with different values.
84
+ for image in self.get_images(os_builder=True) + self.get_images():
85
+ for key, value in sorted((getattr(image, "variables", None) or {}).items()):
86
+ col.declare_variable(ws, image_packer_variable(image.get_name(), key, value))
87
+
88
+ setup_path = self.get_path_for_phase(phase, suffix="-setup.pkr.hcl")
89
+ items.add(setup_path,
90
+ f"# Packer Image Builders generated by {self.__class__.__name__} "
91
+ f"({self.name})")
92
+ items.add(setup_path, "# This file contains the setup configuration for the Packer image builder ")
93
+ items.add(setup_path, "# Packer configuration")
94
+ items.add(setup_path, f"# Generated by {self.model.__class__.__name__}")
95
+ items.add_list(setup_path, col.generate_packer_block(ws))
96
+
97
+ vpath = self.get_path_for_phase(phase, suffix="-vars.pkr.hcl")
98
+ items.add(vpath, "# Packer variables configuration")
99
+ items.add(vpath, f"# Generated by {ws} ({self.model.__class__.__name__})")
100
+ items.add_list(vpath, col.generate_variable_blocks(ws))
101
+ return items
102
+
103
+ def generate_items_during(
104
+ self, phase: ExecutionLifecyclePhase
105
+ ) -> list[tuple[Path, str]]:
106
+ """Generate a list of items to create during a specific execution lifecycle
107
+ event."""
108
+ items: list[tuple[Path, str]] = []
109
+ if phase == ExecutionLifecyclePhase.IMAGE_GENERATION:
110
+ for image in self.get_images(os_builder=True):
111
+ rpath = self.get_path_for_phase(
112
+ phase, f"image-{image.name}", suffix=".pkr.hcl"
113
+ )
114
+ items.append(
115
+ (
116
+ rpath,
117
+ f"# Packer configuration for BASE image {image.name} generated by "
118
+ f"{self.__class__.__name__} ({self.name})",
119
+ )
120
+ )
121
+ for image in self.get_images():
122
+ rpath = self.get_path_for_phase(
123
+ phase, f"image-{image.name}", suffix=".pkr.hcl"
124
+ )
125
+ items.append(
126
+ (
127
+ rpath,
128
+ f"# Packer configuration for image {image.name} generated by "
129
+ f"{self.__class__.__name__} ({self.name})",
130
+ )
131
+ )
132
+ return items
133
+
134
+ def get_commands_to_run_after(
135
+ self, phase: ExecutionLifecyclePhase
136
+ ) -> CFExecutables:
137
+ """Get a list of commands to run after a specific execution lifecycle event."""
138
+ commands_now: list[ExecutableModel] = []
139
+ commands_final: list[ExecutableModel] = []
140
+ if phase == ExecutionLifecyclePhase.IMAGE_GENERATION:
141
+ wd = self.get_path_for_phase(phase, suffix=".tf").parent
142
+ assert wd is not None, (
143
+ f"Working directory for commands to run after {phase.value} cannot be "
144
+ f"None for builder {self.__class__.__name__}"
145
+ )
146
+ for cmd in [["fmt", "."], ["init", "."], ["validate","."]]:
147
+ e = self.get_executable_copy()
148
+ if not e:
149
+ raise ValueError(
150
+ f"Executable for {self.__class__.__name__} is not available "
151
+ f"but is required to run commands after {phase.value}"
152
+ )
153
+ e.args = cmd
154
+ e.working_directory = wd
155
+ commands_now.append(e)
156
+ e = self.get_executable_copy()
157
+ e.args = [
158
+ "build",
159
+ ".",
160
+ ] # This is the command that actually does the image building, so we put
161
+ # it in the "after" list to run after all the setup and validation commands.
162
+ e.working_directory = wd
163
+ commands_final.append(e)
164
+ return CFExecutables(commands_now, commands_final)
165
+
166
+ @deprecated("This method is deprecated and will be removed in a future version. Performed in a setup phase")
167
+ def generate_osbuilder_images(
168
+ self,
169
+ phase: ExecutionLifecyclePhase,
170
+ rbb: RuntimeBuilderBase,
171
+ images: list[Image]
172
+ ) -> tuple[list[tuple[Path, str]], list[ExecutableModel]]:
173
+ from cs_image_system.base.global_context import GlobalTypeContext
174
+
175
+ genfiles: list[tuple[Path, str]] = []
176
+ gencommands: list[ExecutableModel] = []
177
+ ctx = GlobalTypeContext()
178
+ log.debug(f"Generating images for runtime provider: {rbb.get_display_name()}")
179
+ gen_path: Path = ctx.generation_path
180
+ gen_path.parent.mkdir(parents=True, exist_ok=True)
181
+ qv = self.generate_items_during(phase)
182
+ genfiles.extend(qv)
183
+ qc= self.get_commands_to_run_during(phase)
184
+ gencommands.extend(qc.build_executables)
185
+ ctx.extend_finalization_phase(phase, qc.finalize_executables)
186
+ return (genfiles, gencommands)
187
+
188
+ @deprecated("This method is deprecated and will be removed in a future version. Performed in a setup phase")
189
+ def get_sources(self, images: list[Image]) -> list[dict[str, Any]]:
190
+ from cs_image_system.base.global_context import GlobalTypeContext
191
+
192
+ ctx = GlobalTypeContext()
193
+ configured_os_builders: dict[str, OsBuilderBase] = ctx.os_builders
194
+ sources: list[dict[str, Any]] = []
195
+ for image in images:
196
+ # osb = configured_os_builders.get(image.os) if image.os else None
197
+ # assert osb is not None, (
198
+ # f"No OS builder configured for image {image.os} in {image.name}."
199
+ # )
200
+
201
+ s = {}
202
+ s.update(image.__dict__)
203
+ s["packer_source_type"] = "amazon-ebs"
204
+ sources.append(s)
205
+ return sources
206
+
207
+ def ib_generated_path_name(self) -> Path:
208
+ assert self.model.runtime is not None, (
209
+ "Runtime provider must be specified in the builder configuration to "
210
+ "determine generated path name."
211
+ )
212
+
213
+ return Path(utils.safe_name(self.model.runtime)) / IMAGES
214
+
215
+ class PackerVersionChecker(AbstractVersionChecker):
216
+ """Version checker for Packer."""
217
+
218
+ @classmethod
219
+ def csis_name(cls) -> str:
220
+ return PACKER
221
+
222
+
223
+ def get_version_params(self) -> list[str]:
224
+ return ["version", "-machine-readable"]
225
+
226
+ def get_extracted_string(self, res: subprocess.CompletedProcess[str]) -> str | None:
227
+ return res.stdout.strip().splitlines()[0].split(",")[3].strip('"')
228
+
229
+
230
+ _VARIABLE_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*$")
231
+
232
+
233
+ def image_packer_variable(image: str, key: str, value: Any) -> PackerVariableDecl:
234
+ """One `variables:` entry of an image as a packer variable (stage 63):
235
+ a string, number or bool, whose type follows the value; a list, a mapping
236
+ or a name packer cannot take is refused naming the image and the key."""
237
+ name = str(key)
238
+ if not _VARIABLE_NAME.match(name):
239
+ raise ValueError(f"image '{image}': `variables.{name}` is not a packer variable name "
240
+ "(a letter or `_` first, then letters, digits, `_` or `-`)")
241
+ if isinstance(value, bool):
242
+ kind = "bool"
243
+ elif isinstance(value, (int, float)):
244
+ kind = "number"
245
+ elif isinstance(value, str):
246
+ kind = "string"
247
+ else:
248
+ raise ValueError(f"image '{image}': `variables.{name}` must be a string, number or bool, "
249
+ f"not {type(value).__name__}")
250
+ # the description names no image, so two images that give one name the SAME
251
+ # value declare one variable; different values are refused by the collector
252
+ return PackerVariableDecl(name=name, type=kind, default=value,
253
+ description="declared by an image's `variables:`")