cs-image-system-bash-mod-plugin 0.1.6__tar.gz → 0.1.7__tar.gz

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.
@@ -26,3 +26,5 @@ target/
26
26
  tfplan
27
27
  *.tfstate
28
28
  *.tfstate.backup
29
+ REEXAMINE*
30
+ TODO2.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cs-image-system-bash-mod-plugin
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: The shell modification plugin: inline script, script files and an ensure form become Packer shell provisioners
5
5
  Project-URL: Repository, https://github.com/infrastructurebuilder/cs-image-system-3
6
6
  Project-URL: Documentation, https://github.com/infrastructurebuilder/cs-image-system-3/blob/develop/docs/OPERATIONS.md
@@ -9,8 +9,8 @@ Author-email: Mykel Alvis <mykelalvis@infrastructurebuilder.org>
9
9
  License-Expression: Apache-2.0
10
10
  License-File: LICENSE
11
11
  Requires-Python: >=3.13
12
- Requires-Dist: cs-image-system-base==0.1.6
13
- Requires-Dist: cs-image-system-contract==0.1.6
12
+ Requires-Dist: cs-image-system-base==0.1.7
13
+ Requires-Dist: cs-image-system-contract<0.5,>=0.4
14
14
  Requires-Dist: pydantic>=2.13
15
15
  Description-Content-Type: text/markdown
16
16
 
@@ -55,18 +55,15 @@ How a YAML entry selects these classes:
55
55
  looks the builder up by name or alias, reads the builder's own `type`
56
56
  (`bash-remote`) and loads the item into the class registered under that
57
57
  service key for `MOD_BUILDER_ITEM_MODEL`: `BashModItemModel`. The
58
- registry alone decides; the builder model's
59
- `get_target_deferred_type_by_VCT` method, which also named that class
60
- and had no caller, is gone (stage 63, 2026-09-25), as is the plugin's
61
- empty `helpers.py`.
62
- - **The item's `type` is rewritten to the builder's `name` at load**
63
- (stage 63 item 15). The image builder later fetches the builder with
58
+ registry alone decides.
59
+ - **The item's `type` is rewritten to the builder's `name` at load.**
60
+ The image builder later fetches the builder with
64
61
  `ctx.mod_builders.get(mod.get_type())`, a map keyed by builder NAME
65
- only, so an alias must not survive the load. An item declared
66
- `type: bash` (the fixture builder's alias) is loaded as
67
- `type: bash-remote`. Before 2026-09-25 the alias was kept, and
68
- generation stopped with
62
+ only, so an alias must not survive the load: a kept alias would stop
63
+ generation with
69
64
  `AssertionError: No mod builder bash found in context for <image>`.
65
+ An item declared `type: bash` (the fixture builder's alias) is loaded
66
+ as `type: bash-remote`.
70
67
  - An `executables:` entry named `bash` is version-checked by
71
68
  `BashVersionChecker` (regex `.*version\s+([\d\.]+)` over
72
69
  `bash --version`).
@@ -80,21 +77,31 @@ Defined in [bash_models.py](src/cs_image_system/bash_mod_plugin/bash_models.py).
80
77
  [`ModBuilderModel`](../base/src/cs_image_system/base/models/mod_builder.py),
81
78
  which adds nothing to
82
79
  [`BuilderModel`](../base/src/cs_image_system/base/models/builder_model.py)
83
- (`name`, `type`, `description`, `aliases`, `executable`, `is_default`,
84
- `config`, `gitignore`, `tags`). `BashModBuilderModel` is the registered
85
- subclass; it adds no fields.
86
-
87
- | Field | Type | Default | Meaning |
88
- |---|---|---|---|
89
- | `execute_command` | `str \| None` | `None` | Emitted as `execute_command = "..."` on every shell provisioner of every item (Packer's command template, for example `sudo -E bash '{{.Path}}'`). The way to change how the script is invoked. May not be declared together with `configuration_user`. |
90
- | `configuration_user` | `str \| None` | `None` | When set, every shell provisioner of this builder gets `execute_command = "chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'"`, so each item's lines run as that user; the bake user only launches them (stage 63; declared and not read until 2026-09-25). Declaring it together with `execute_command` is refused at load. |
91
- | `environment_vars` | `list[str]` | `[]` | Emitted as `environment_vars = [...]` on every shell provisioner. |
92
- | `expect_disconnect` | `bool` | `False` | Emits `expect_disconnect = true` on every shell provisioner. |
93
-
94
- `extra_arguments` is not a field (stage 63): it was declared and never
95
- read, so it is removed, and a builder that still declares it fails to load
96
- with the unknown-key refusal. Use `execute_command` to change the
97
- invocation.
80
+ (the common builder fields: `name`, `type`, `description`, `aliases`,
81
+ `executable`, `is_default`, `config`, `gitignore`, `tags`;
82
+ [CONFIGURATION section 4.1](../../docs/CONFIGURATION.md#41-fields-common-to-every-builder)
83
+ holds that table). `BashModBuilderModel` is the registered subclass; it
84
+ adds no fields.
85
+
86
+ The keys this plugin adds are `execute_command`, `configuration_user`,
87
+ `environment_vars` and `expect_disconnect`: each is a Packer
88
+ shell-provisioner argument, emitted on every shell provisioner of every
89
+ item of the builder.
90
+ [CONFIGURATION section 7.2](../../docs/CONFIGURATION.md#72-bash-remote)
91
+ holds the field table; [Configuration reference](#configuration-reference)
92
+ below gives this plugin's reading of each key. Rules only this plugin
93
+ enforces:
94
+
95
+ - `execute_command` (Packer's command template, for example
96
+ `sudo -E bash '{{.Path}}'`) is the way to change how the script is
97
+ invoked. `configuration_user` writes its own: every shell provisioner
98
+ of the builder gets `execute_command = "chmod +x {{ .Path }}; sudo su -
99
+ <user> -c '{{ .Vars }} {{ .Path }}'"`, so each item's lines run as that
100
+ user and the bake user only launches them. Because both say how the
101
+ script runs, declaring them together is refused at load.
102
+ - `extra_arguments` is not a field: a builder that declares it fails to
103
+ load with the unknown-key refusal. Use `execute_command` to change the
104
+ invocation.
98
105
 
99
106
  Base fields this plugin constrains: `executable` should name the `bash`
100
107
  executable (the fixture does), though the emission does not invoke it;
@@ -105,13 +112,28 @@ base-model rule). `config:` on the builder is carried but never read.
105
112
 
106
113
  Same file. Extends
107
114
  [`ModItemModel`](../base/src/cs_image_system/base/models/moditem_type.py)
108
- (`SubRootItem` -> `RootItem` -> `NameTyped`).
109
-
110
- | Field | Type | Default | Meaning |
111
- |---|---|---|---|
112
- | `script` | `list[str]` | `[]` | Inline command lines, run in order in one `provisioner "shell"` (`inline = [...]`). Literal shell; no templating. Blank entries are dropped. |
113
- | `scripts` | `list[str]` | `[]` | Script files, relative to the configuration root (the process's working directory, which the configuration load sets to the root), copied beside the Packer root and run as `scripts = [...]`. Blank entries are dropped. |
114
- | `ensure` | `dict[str, Any]` | `{}` | The declarative form. Allowed keys: `packages` (list of names), `files` (list of `{path, content, mode}`; `mode` defaults to `0644`), `services` (list of unit names, enabled and started), `commands` (list of `{run, unless}`; `run` executes only when `unless` fails). Every entry is validated at load by `validate_ensure` (stage 63 item 16): a wrong shape, an unknown key, a bad mode or an all-empty mapping raises `ValueError` naming the item and the entry. |
115
+ (`SubRootItem` -> `RootItem` -> `NameTyped`). The item's own keys are
116
+ `script`, `scripts` and `ensure`; the inherited ones (`name`, `type`,
117
+ `description`, `aliases`, `tags`, `config`) are the modification item's,
118
+ tabled in
119
+ [CONFIGURATION section 7.3](../../docs/CONFIGURATION.md#73-the-modification-item-inside-an-images-modifications),
120
+ and [Configuration reference](#configuration-reference) below reads every
121
+ key as this plugin does.
122
+
123
+ - `script` is the list of inline command lines, run in order in one
124
+ `provisioner "shell"` (`inline = [...]`). Literal shell; no templating.
125
+ Blank entries are dropped.
126
+ - `scripts` is the list of script files, relative to the configuration
127
+ root (the process's working directory, which the configuration load
128
+ sets to the root), copied beside the Packer root and run as
129
+ `scripts = [...]`. Blank entries are dropped.
130
+ - `ensure` is the declarative form. Allowed keys: `packages` (list of
131
+ names), `files` (list of `{path, content, mode}`; `mode` defaults to
132
+ `0644`), `services` (list of unit names, enabled and started),
133
+ `commands` (list of `{run, unless}`; `run` executes only when `unless`
134
+ fails). Every entry is validated at load by `validate_ensure`: a wrong
135
+ shape, an unknown key, a bad mode or an all-empty mapping raises
136
+ `ValueError` naming the item and the entry.
115
137
 
116
138
  Validation at load: at least one of `script`, `scripts`, `ensure` must be
117
139
  non-empty, otherwise `ValueError` ("must supply 'script' (inline lines),
@@ -134,15 +156,13 @@ Derived behaviour:
134
156
  - `remap_self_with_copied_assets(copied)`: replaces each `scripts` entry
135
157
  with its copied path when present in the mapping.
136
158
 
137
- Inherited item fields:
138
-
139
- | Base field | From | Default | Use in this plugin |
140
- |---|---|---|---|
141
- | `name` | `NameTyped` | required | Names the provisioner comment and the local bundle directory (`NN-<name>`). |
142
- | `type` | `ModItemModel` | `default` | Foreign key to a `mod_builders:` entry (see above). |
143
- | `description`, `aliases`, `tags` | `NameTyped` / `RootItem` | | Carried; not emitted. |
144
- | `config` | `RootItem` | `{}` | Not read by this plugin. It takes part in the lineage content hash of the modification and nothing else. |
145
- | `_model_id` | `ModItemModel` | set at load | The owning image's global id; `identified_model` resolves to the image. |
159
+ Of the inherited fields, `name` names the provisioner comment and the
160
+ local bundle directory (`NN-<name>`); `type` is the foreign key to a
161
+ `mod_builders:` entry (see above); `description`, `aliases` and `tags`
162
+ are carried and not emitted; `config` is not read by this plugin and
163
+ takes part in the lineage content hash of the modification and nothing
164
+ else; `_model_id`, set at load, is the owning image's global id, and
165
+ `identified_model` resolves to the image.
146
166
 
147
167
  ### A modification item with only `config:`
148
168
 
@@ -183,8 +203,8 @@ calls, in order:
183
203
  4. `generate_items_during_modification(image, mod, image_builder, phase, build_path)`:
184
204
  - `inline` = `ensure_lines()` followed by the `script` lines; `scripts`
185
205
  = the script files. When both lists are empty it logs a warning and
186
- emits nothing; since stage 63 item 16 an all-empty `ensure` is
187
- refused at load, so an item can no longer reach this.
206
+ emits nothing; an all-empty `ensure` is refused at load, so an item
207
+ cannot reach this.
188
208
  - Appends `# Modifications for <item name> of type <item type> (shell)`.
189
209
  - Then, because Packer's shell provisioner takes either `scripts` or
190
210
  `inline` but never both, one or two `provisioner "shell"` blocks:
@@ -197,7 +217,7 @@ calls, in order:
197
217
  with a `configuration_user`, `chmod +x {{ .Path }}; sudo su - <user>
198
218
  -c '{{ .Vars }} {{ .Path }}'` (Packer's `{{ .Vars }}`, the
199
219
  environment variables, travel inside the command because `su -`
200
- starts a fresh login environment), else none (stage 63).
220
+ starts a fresh login environment), else none.
201
221
  - Every string is rendered as an HCL literal: backslashes and double
202
222
  quotes are escaped, and `${` / `%{` become `$${` / `%%{` so shell text
203
223
  such as `dpkg-query -f='${Package}'` survives Packer's template parser.
@@ -212,9 +232,7 @@ modification bundle records the item: its directory
212
232
  `run.sh` that runs `sh '<script>'` per script file and then
213
233
  `sh ./inline.sh`, and a `MANIFEST.yaml` with `operation: bash` and the
214
234
  item's `idempotent` value. `csis-mods rerun` on the image therefore
215
- re-applies the declarative `ensure` form too (stage 63). Until 2026-09-25
216
- the `ensure` lines were not in the bundle and a rerun replayed only the
217
- script files and the `script` lines.
235
+ re-applies the declarative `ensure` form too.
218
236
 
219
237
  ## Emission
220
238
 
@@ -316,8 +334,8 @@ entry in `cfg/executables.yml`
316
334
  ([the fixture's](../../tests/fixtures/config/cfg/executables.yml) is
317
335
  `name: bash`, `binary: /usr/local/bin/bash`, no `version`), because
318
336
  `validate` checks every builder's `executable` against that list and every
319
- run checks every declared entry (stage 48: the binary must exist as an
320
- absolute path or a name on `PATH`, `BashVersionChecker` must parse a version
337
+ run checks every declared entry (the binary must exist as an absolute
338
+ path or a name on `PATH`, `BashVersionChecker` must parse a version
321
339
  from `<binary> --version`, and when the entry declares `version:` the parsed
322
340
  version must satisfy it). That is the whole extent of the plugin's use of a
323
341
  local bash: it is looked at, never run. The fixture declares no floor;
@@ -365,7 +383,7 @@ variable, profile or file of its own.
365
383
 
366
384
  Every key the model accepts. An unknown key is refused at load
367
385
  (`extra="forbid"` in the shared model config), and `parameters:` is refused
368
- by name (retired in stage 26).
386
+ by name.
369
387
 
370
388
  | Field | Type | Default | Meaning |
371
389
  |---|---|---|---|
@@ -379,24 +397,24 @@ by name (retired in stage 26).
379
397
  | `gitignore` | list[str] | `[]` | Accepted by the base model; nothing in this plugin reads it. |
380
398
  | `tags` | mapping | `{}` | Accepted, not read. |
381
399
  | `execute_command` | str or null | `null` | Emitted verbatim (HCL-escaped) as `execute_command` on every shell block of every item of this builder. Refused together with `configuration_user`. |
382
- | `configuration_user` | str or null | `null` | Read since stage 63 (accepted and ignored until 2026-09-25): every shell block of this builder runs its lines as this user through `execute_command = "chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'"`. Refused together with `execute_command` (`... both say how the script runs; declare one`). |
400
+ | `configuration_user` | str or null | `null` | Every shell block of this builder runs its lines as this user through `execute_command = "chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'"`. Refused together with `execute_command` (`... both say how the script runs; declare one`). |
383
401
  | `environment_vars` | list[str] | `[]` | Emitted as `environment_vars = [...]` on every shell block. |
384
402
  | `expect_disconnect` | bool | `false` | When true, `expect_disconnect = true` on every shell block. |
385
- | `extra_arguments` | | | Refused at load as an unknown key (removed in stage 63; it was accepted and never read). Use `execute_command`. |
403
+ | `extra_arguments` | | | Not a field; refused at load as an unknown key. Use `execute_command`. |
386
404
 
387
405
  ### The item (an image's `modifications:` entry whose `type:` names this builder)
388
406
 
389
407
  | Field | Type | Default | Meaning |
390
408
  |---|---|---|---|
391
409
  | `name` | str | required | Unique within the image. Names the comment line, the bundle directory `NN-<name>` and the lineage record. |
392
- | `type` | str | `default` | The builder's name, an alias, or `default` (or omitted). Rewritten to the builder's name at load (stage 63 item 15). Omitted or `default` with no `is_default: true` mod builder: refused at load (stage 63). |
410
+ | `type` | str | `default` | The builder's name, an alias, or `default` (or omitted). Rewritten to the builder's name at load. Omitted or `default` with no `is_default: true` mod builder: refused at load. |
393
411
  | `description` | str or null | `null` | Accepted, not read. |
394
412
  | `aliases` | list[str] | `[]` | Accepted, not read. |
395
413
  | `tags` | mapping | `{}` | Accepted, not read. |
396
414
  | `config` | mapping | `{}` | Not read by the plugin. Part of the modification's content hash, so a change re-bakes the image. Not passed to the shell. |
397
415
  | `script` | list[str] | `[]` | Inline lines, literal, in order, after the `ensure` lines. Blank and whitespace-only entries are dropped. |
398
416
  | `scripts` | list[str] | `[]` | Script files; each is copied beside the Packer root and run first, in order. Blank entries are dropped. An absolute path is not copied and is emitted as is. |
399
- | `ensure` | mapping | `{}` | Only the four keys below are allowed; any other key is refused at load. Every entry's values are validated at load since stage 63 item 16 (`validate_ensure`; see "What it tests and verifies"). |
417
+ | `ensure` | mapping | `{}` | Only the four keys below are allowed; any other key is refused at load. Every entry's values are validated at load (`validate_ensure`; see "What it tests and verifies"). |
400
418
 
401
419
  The `ensure` keys:
402
420
 
@@ -417,11 +435,11 @@ The `ensure` keys:
417
435
  `idempotent: unknown`, the same as free-form alone. `ensure` lines always
418
436
  precede the `script` lines in the inline block.
419
437
  - **`ensure` whose lists are all empty** (`ensure: {packages: []}`) is
420
- refused at load (stage 63 item 16). Before 2026-09-25 it passed load,
421
- counted as `declared`, and emitted nothing: the builder logged
422
- `Bash modification <name> has no script lines or files; nothing emitted`
423
- and the bake proceeded without a provisioner for it. The bundle directory
424
- still exists, with a `run.sh` that runs nothing.
438
+ refused at load, because such an item would count as `declared` and
439
+ emit nothing: the builder would log
440
+ `Bash modification <name> has no script lines or files; nothing emitted`,
441
+ the bake would proceed without a provisioner for it, and the bundle
442
+ directory would hold a `run.sh` that runs nothing.
425
443
  - **`commands` with `unless`** is guarded; **without `unless`** the command
426
444
  runs on every bake and on every `csis-mods rerun`, so the item is
427
445
  idempotent only if the command is.
@@ -431,7 +449,7 @@ The `ensure` keys:
431
449
  - **`execute_command` set** puts that template on every block of every
432
450
  item of the builder; **`configuration_user` set** instead puts
433
451
  `chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'`
434
- there, so the lines run as that user (stage 63); **both set** is refused
452
+ there, so the lines run as that user; **both set** is refused
435
453
  at load; **neither**, Packer's default applies and the lines run as the
436
454
  bake user. The same for
437
455
  `environment_vars` (emitted only when non-empty) and `expect_disconnect`
@@ -440,11 +458,10 @@ The `ensure` keys:
440
458
  `type`** reaches whichever mod builder is `is_default: true`; in the
441
459
  fixture that is `ansible-default`, so the item is loaded as an ANSIBLE
442
460
  item and its `script`/`scripts`/`ensure` keys are refused as unknown.
443
- With no `is_default: true` mod builder at all, the load is refused
444
- (stage 63): `No default builder found in mod_builder list.` first, and
445
- the orchestrator's `` modification '<name>' names no builder (`type`) ... ``
446
- for an item that reaches it anyway. Until 2026-09-25 such an item
447
- became an `AttributeError` at generation.
461
+ With no `is_default: true` mod builder at all, the load is refused:
462
+ `No default builder found in mod_builder list.` first, and the
463
+ orchestrator's `` modification '<name>' names no builder (`type`) ... ``
464
+ for an item that reaches it anyway.
448
465
  **`type: <alias>`** behaves exactly as the builder's name: the loader
449
466
  rewrites it.
450
467
  - **AWS versus GCE runtime**: the lines are identical; only the `only`
@@ -479,7 +496,7 @@ integer mode no higher than `0777`, and not every kind empty); an item
479
496
  with none of `script`, `scripts`, `ensure`; an item `type:` that names no
480
497
  builder; an item with no `type:` when no mod builder is the default; a
481
498
  builder declaring both `configuration_user` and `execute_command`, or
482
- the removed `extra_arguments` (stage 63). Each is a
499
+ `extra_arguments`. Each is a
483
500
  `ValueError` (or `KeyError` for the type) that stops the load; the CLI
484
501
  prints it and exits 1. Nothing loads, so nothing is generated.
485
502
 
@@ -532,81 +549,41 @@ failure prints `FAIL <image>/<mod>: rc=...` on stderr and exits 1. A
532
549
 
533
550
  ## When it fails
534
551
 
535
- Failures that have happened, in date order:
536
-
537
- - **2026-08-27, three defects found in the pre-V2 builder**
538
- ([LEDGER](../../docs/history/LEDGER.md), "Finding 2026-08-27"). Every
539
- item resolved to the default mod builder (the lookup used the field name
540
- `modifications`, not the item's `type`), so a bash item was recorded as
541
- `ansible-default` with no content; the bash builder wrote raw lines and an
542
- `exit 0` straight into the build file, so any real bash item failed
543
- `packer validate`; and `scripts:` was not a field, so files were dropped.
544
- Fixed at gate 8; guarded by
545
- [test_v2_gate8_modifications.py](../../tests/test_v2_gate8_modifications.py)
546
- and [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py). If
547
- a comment line ever appears outside a `provisioner "shell" {` block, or
548
- `exit 0` appears in a build file, this regressed.
549
- - **2026-09-10, the first `full-test`'s mod-test leg could not start a
550
- container** for an EL 10 image: Docker Hub's `rockylinux` library stops at
551
- 9. The stand-in for `rhel` 10 and above is `almalinux:<version>` since
552
- (`test_image_for` in [mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)).
553
- Symptom then: `docker run` failed to pull and the bash item's test never
554
- ran. If `test-mods` reports `unsupported` with
555
- `no local test image for the root OS family`, the family/version has no
556
- stand-in; set `local_test_image` on the OS builder.
557
- - **Found live, undated, in the sibling shell emitter for image tests**
558
- ([v2_provisioners.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_provisioners.py)):
559
- an `rpm --qf '%{NAME}'` line broke `packer fmt`, because `%{` and `${`
560
- open Packer template directives. This plugin's `_hcl_string` escapes both
561
- the same way. If `packer fmt` or `validate` ever reports
562
- `Invalid template directive` or `Invalid template interpolation` on a
563
- line you wrote, the escape regressed; the line in `generated/` should
564
- read `%%{` / `$${`.
565
- - **2026-09-20, an assertion that could not fail** (image `tests:`, not this
566
- plugin, but the same shell and the same trap;
567
- [OPERATIONS.md](../../docs/OPERATIONS.md), "What a green bake proves").
568
- Under `sh -e`, a command inside an AND-OR list other than the last does
569
- not abort the script, so a `script:` line written `check || { echo no; }`
570
- never fails the build however false `check` is. Two bakes went green
571
- asserting a package that did not exist. If a `script` line must be able
572
- to stop the bake, end it in something that exits nonzero (`|| exit 1`),
573
- not in a braced group that returns 0. The plugin's own `commands` form,
574
- `( unless ) || { run; }`, is deliberate: the group is the LAST element,
575
- so a failing `run` does abort.
576
- - **2026-09-23, an item declared with the builder's alias failed at
577
- generation** (found while writing this README, on a copy of the fixture
578
- with `type: bash` on `derivative-setup`). Load and `validate` passed; the
579
- run log then showed
580
- `AssertionError: No mod builder bash found in context for imgfile-basic-dask`
581
- and the summary was `ok: false` with no validation errors. Fixed by
582
- stage 63 item 15 (2026-09-25): the loader rewrites the alias to the
583
- builder's name, and `tests/test_v2_defects_loading.py` generates that
584
- same fixture copy.
585
-
586
- Failures the code raises that have not been seen outside tests:
552
+ Incidents that have happened, with their cause and fix, are recorded on
553
+ [INCIDENTS.md](../../docs/INCIDENTS.md); what follows keeps each
554
+ failure's symptom, its meaning and what to do.
555
+
556
+ Symptoms of the emission and of the bake, with what each means and what
557
+ to do:
587
558
 
588
559
  | What you see | Where | Meaning and what to do |
589
560
  |---|---|---|
561
+ | A `# Modifications for ...` comment line outside a `provisioner "shell" {` block, or `exit 0` in a build file | `generated/`; `packer validate` fails | The builder wrote raw lines into the build file instead of provisioner blocks. [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py) and [test_modifications.py](../../tests/test_modifications.py) pin the block form; this is a regression of the plugin, not of the item. |
562
+ | A bash item recorded in lineage as `ansible-default` with no content, or its script files dropped | `meta-state/lineage.yaml` after a bake | The item was resolved to the default mod builder instead of the builder its `type:` names. The same two tests pin the lookup by the item's `type`; check the item's `type:` names this builder. |
563
+ | `test-mods` reports `unsupported` with `no local test image for the root OS family` | `test-mods` | The OS family and version have no stand-in container image (`test_image_for` in [mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)): Docker Hub's `rockylinux` library stops at 9, so `rhel` 10 and above stand in as `almalinux:<version>`, and a family outside that table has none. Set `local_test_image` on the OS builder. |
564
+ | `Invalid template directive` or `Invalid template interpolation` from `packer fmt` or `validate` on a line you wrote | the Packer plugin's run-now commands | `%{` and `${` open Packer template directives; this plugin's `_hcl_string` escapes both as `%%{` / `$${` (the sibling shell emitter for image tests, [v2_provisioners.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_provisioners.py), does the same). If the line in `generated/` does not read so, the escape regressed. |
565
+ | A `script:` line written `check || { echo no; }` never fails the build however false `check` is | the bake, green | Under `sh -e` a command inside an AND-OR list other than the last does not abort the script, so a bake can go green asserting a package that does not exist (the same trap holds for an image's `tests:`; [OPERATIONS.md](../../docs/OPERATIONS.md), "What a green bake proves"). If a `script` line must be able to stop the bake, end it in something that exits nonzero (`\|\| exit 1`), not in a braced group that returns 0. The plugin's own `commands` form, `( unless ) \|\| { run; }`, is deliberate: the group is the LAST element, so a failing `run` does abort. |
566
+ | `AssertionError: No mod builder bash found in context for <image>` in the run log, summary `ok: false` with no validation errors, after load and `validate` passed | generation | An item's `type:` reached generation as the builder's alias; the loader rewrites an alias to the builder's name at load (`tests/test_modifications.py` generates a fixture copy with `type: bash`), so a tree whose loader does not is on an older form ([the stage index](../../docs/CHANGES.md#s63)). Write the builder's name, or update. |
590
567
  | `Bash modification '<name>' must supply 'script' (inline lines), 'scripts' (script files) and/or 'ensure' (declarative, idempotent)` | load; CLI exit 1 | The item has none of the three (a `config:`-only item). Add one or remove the item. |
591
568
  | `Bash modification '<name>': unknown ensure keys ['users']` | load; CLI exit 1 | Only `packages`, `files`, `services`, `commands` exist. Move anything else to `commands`. |
592
569
  | A pydantic error naming a key (`Extra inputs are not permitted`) on the builder or the item | load; CLI exit 1 | A misspelt or foreign key (`playbooks:` on a bash item, `script:` on an ansible item). Check the item's `type:` reaches the builder you meant; `type: default` reaches the default builder, which in the fixture is ansible. |
593
- | ``<builder>: `parameters` was retired (stage 26) ...`` | load; CLI exit 1 | Delete `parameters:` from the builder. |
570
+ | ``<builder>: `parameters` was retired ...`` | load; CLI exit 1 | Delete `parameters:` from the builder. |
594
571
  | `KeyError: Could not resolve builder '<type>' referenced in Image(...)` | load; CLI exit 1 | The item's `type:` names no `mod_builders:` entry. |
595
- | `` mod builder '<name>': `configuration_user` and `execute_command` both say how the script runs; declare one `` | load; CLI exit 1 | Stage 63. `configuration_user` writes its own `execute_command`; keep one. To run as another user with a custom command, put the `sudo su - <user>` in your `execute_command`. |
596
- | A pydantic unknown-key error naming `extra_arguments` on a `bash-remote` builder | load; CLI exit 1 | Stage 63 removed the field (it was never read). Delete it; change the invocation with `execute_command`. |
597
- | `No default builder found in mod_builder list.` | load; CLI exit 1 | No mod builder says `is_default: true`. Mark one, or give every item a `type:`. The orchestrator's own form, for an item that reaches it, is `` modification '<name>' names no builder (`type`) and no mod builder is `is_default: true`; name one with `type:` or mark a default `` (stage 63; it used to be an `AttributeError` at generation). |
572
+ | `` mod builder '<name>': `configuration_user` and `execute_command` both say how the script runs; declare one `` | load; CLI exit 1 | `configuration_user` writes its own `execute_command`; keep one. To run as another user with a custom command, put the `sudo su - <user>` in your `execute_command`. |
573
+ | A pydantic unknown-key error naming `extra_arguments` on a `bash-remote` builder | load; CLI exit 1 | The field does not exist. Delete it; change the invocation with `execute_command`. |
574
+ | `No default builder found in mod_builder list.` | load; CLI exit 1 | No mod builder says `is_default: true`. Mark one, or give every item a `type:`. The orchestrator's own form, for an item that reaches it, is `` modification '<name>' names no builder (`type`) and no mod builder is `is_default: true`; name one with `type:` or mark a default ``. |
598
575
  | `Executable bash specified for provider bash-remote not found in executables list.` | `validate`; exit 1 | Add a `bash` entry to `cfg/executables.yml` or change the builder's `executable:`. |
599
576
  | `bash: binary '/usr/local/bin/bash' not found (declared in cfg/executables.yml; an absolute path, or a name on PATH)` | `validate` and every run; exit 1 | The pinned path is wrong on this machine. Symlink the binary there or fix `binary:`. |
600
577
  | ``bash: BashVersionChecker could not parse a version from `/usr/local/bin/bash --version` `` | `validate` and every run | The first line of `--version` has no `version N.N`. Something other than bash is at that path. |
601
578
  | `bash 5.3.15 does not meet its requirement <spec> (cfg/executables.yml)` | `validate` and every run | The floor moved or the machine is behind. |
602
579
  | `FileNotFoundError: Bash modification script mod_image.sh does not exist or is not a file` | generation; run `ok: false`, traceback in the log | The path is resolved from the configuration root, not from the image file's directory or the shell's. Fix the path or `--root-dir`. |
603
580
  | `Bash modification '<name>': ensure declares [...] but every one is empty, so it would emit nothing` | load; CLI exit 1 | Every `ensure` list is empty. Fill one or drop `ensure`. |
604
- | `Bash modification '<name>': ensure.files[N]: `path` is required` (or `ensure.commands[N]: `run` is required`) | load; CLI exit 1 | A `files` entry without `path`, or a `commands` entry without `run`. Before stage 63 this was a `KeyError` at generation. |
605
- | `Bash modification '<name>': ensure.packages must be a list of package names, not str 'git'` | load; CLI exit 1 | `packages:` (or `services:`) given as a string; the old code iterated its letters. Write a list (`packages: [git]`). |
581
+ | `Bash modification '<name>': ensure.files[N]: `path` is required` (or `ensure.commands[N]: `run` is required`) | load; CLI exit 1 | A `files` entry without `path`, or a `commands` entry without `run`. |
582
+ | `Bash modification '<name>': ensure.packages must be a list of package names, not str 'git'` | load; CLI exit 1 | `packages:` (or `services:`) given as a string, which shell would iterate letter by letter. Write a list (`packages: [git]`). |
606
583
  | `Bash modification '<name>': ensure.files[N].mode 644 is not a mode YAML could have read from octal` | load; CLI exit 1 | An unquoted `644`, which YAML reads as a decimal number. Write `mode: "0644"`. |
607
584
  | `Bash modification '<name>': ensure.files[N]: unknown keys [...]` | load; CLI exit 1 | Only `path`, `content`, `mode` on a file and `run`, `unless` on a command. |
608
585
  | `csis ensure: no dnf, yum or apt-get to install <pkg>` | apply / `test-mods` | The image has none of the three package managers. Use a `script` line with the one it has. |
609
- | `dnf` (or `yum`, `apt-get`) error on a package step | apply / `test-mods` | The first package manager found could not install the package, and its message is the real one (no match, no repository, no network from the build subnet). Before stage 63 the line fell through to `apt-get` and showed `apt-get: command not found` last on an EL host. |
586
+ | `dnf` (or `yum`, `apt-get`) error on a package step | apply / `test-mods` | The first package manager found could not install the package, and its message is the real one (no match, no repository, no network from the build subnet); the others are never tried, so no misleading `apt-get: command not found` follows it on an EL host. |
610
587
  | `cmp: command not found` or `base64: command not found` | apply | The image lacks diffutils / coreutils. Add the package to an earlier item's `ensure.packages` or a `script` line. |
611
588
  | `sudo: a terminal is required to read the password` | apply | The bake ssh user has no passwordless `sudo`. This is an OS builder / runtime matter, not the item's. |
612
589
  | `Failed to enable unit: Unit <svc>.service does not exist` | apply | `services:` names a unit the image has not installed yet; install it earlier in the same item's `packages` (packages run before services). |
@@ -638,20 +615,20 @@ Failures the code raises that have not been seen outside tests:
638
615
  [abstract_version_checker.py](../base/src/cs_image_system/base/basic/abstract_version_checker.py).
639
616
  - The image builder that calls it and hosts its output:
640
617
  [packer-plugin](../packer-plugin)
641
- ([packer_ebs_builder.py](../packer-plugin/src/cs_image_system/packer_plugin/packer_ebs_builder.py),
618
+ ([packer_image_builder.py](../packer-plugin/src/cs_image_system/packer_plugin/packer_image_builder.py),
642
619
  [v2_local_mods.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_local_mods.py)).
643
620
  - Lineage records for modifications:
644
621
  [lineage.py](../base/src/cs_image_system/base/lineage.py) (`mod_records`);
645
622
  the release gate in [release.py](../base/src/cs_image_system/base/release.py).
646
623
  - Tests: [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py)
647
624
  (two blocks for files plus inline, builder options, script copying);
648
- [test_v2_gate8_modifications.py](../../tests/test_v2_gate8_modifications.py)
625
+ [test_modifications.py](../../tests/test_modifications.py)
649
626
  (both kinds emitted, unknown type refused, the load rule);
650
- [test_v2_explore_local_mods.py](../../tests/test_v2_explore_local_mods.py)
627
+ [test_modifications.py](../../tests/test_modifications.py)
651
628
  (`ensure` guards, idempotence recorded, the bundle);
652
- [test_v2_explore_mod_tests.py](../../tests/test_v2_explore_mod_tests.py)
629
+ [test_mod_tests.py](../../tests/test_mod_tests.py)
653
630
  (the container tests on the fixture's bash item);
654
- [test_v2_hygiene_three.py](../../tests/test_v2_hygiene_three.py)
631
+ [test_version_checks.py](../../tests/test_version_checks.py)
655
632
  (`BashVersionChecker` found by name, versions judged).
656
633
  - No terraform modules and no hashicorp-utils usage: the plugin writes
657
634
  Packer provisioner lines directly.
@@ -39,18 +39,15 @@ How a YAML entry selects these classes:
39
39
  looks the builder up by name or alias, reads the builder's own `type`
40
40
  (`bash-remote`) and loads the item into the class registered under that
41
41
  service key for `MOD_BUILDER_ITEM_MODEL`: `BashModItemModel`. The
42
- registry alone decides; the builder model's
43
- `get_target_deferred_type_by_VCT` method, which also named that class
44
- and had no caller, is gone (stage 63, 2026-09-25), as is the plugin's
45
- empty `helpers.py`.
46
- - **The item's `type` is rewritten to the builder's `name` at load**
47
- (stage 63 item 15). The image builder later fetches the builder with
42
+ registry alone decides.
43
+ - **The item's `type` is rewritten to the builder's `name` at load.**
44
+ The image builder later fetches the builder with
48
45
  `ctx.mod_builders.get(mod.get_type())`, a map keyed by builder NAME
49
- only, so an alias must not survive the load. An item declared
50
- `type: bash` (the fixture builder's alias) is loaded as
51
- `type: bash-remote`. Before 2026-09-25 the alias was kept, and
52
- generation stopped with
46
+ only, so an alias must not survive the load: a kept alias would stop
47
+ generation with
53
48
  `AssertionError: No mod builder bash found in context for <image>`.
49
+ An item declared `type: bash` (the fixture builder's alias) is loaded
50
+ as `type: bash-remote`.
54
51
  - An `executables:` entry named `bash` is version-checked by
55
52
  `BashVersionChecker` (regex `.*version\s+([\d\.]+)` over
56
53
  `bash --version`).
@@ -64,21 +61,31 @@ Defined in [bash_models.py](src/cs_image_system/bash_mod_plugin/bash_models.py).
64
61
  [`ModBuilderModel`](../base/src/cs_image_system/base/models/mod_builder.py),
65
62
  which adds nothing to
66
63
  [`BuilderModel`](../base/src/cs_image_system/base/models/builder_model.py)
67
- (`name`, `type`, `description`, `aliases`, `executable`, `is_default`,
68
- `config`, `gitignore`, `tags`). `BashModBuilderModel` is the registered
69
- subclass; it adds no fields.
70
-
71
- | Field | Type | Default | Meaning |
72
- |---|---|---|---|
73
- | `execute_command` | `str \| None` | `None` | Emitted as `execute_command = "..."` on every shell provisioner of every item (Packer's command template, for example `sudo -E bash '{{.Path}}'`). The way to change how the script is invoked. May not be declared together with `configuration_user`. |
74
- | `configuration_user` | `str \| None` | `None` | When set, every shell provisioner of this builder gets `execute_command = "chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'"`, so each item's lines run as that user; the bake user only launches them (stage 63; declared and not read until 2026-09-25). Declaring it together with `execute_command` is refused at load. |
75
- | `environment_vars` | `list[str]` | `[]` | Emitted as `environment_vars = [...]` on every shell provisioner. |
76
- | `expect_disconnect` | `bool` | `False` | Emits `expect_disconnect = true` on every shell provisioner. |
77
-
78
- `extra_arguments` is not a field (stage 63): it was declared and never
79
- read, so it is removed, and a builder that still declares it fails to load
80
- with the unknown-key refusal. Use `execute_command` to change the
81
- invocation.
64
+ (the common builder fields: `name`, `type`, `description`, `aliases`,
65
+ `executable`, `is_default`, `config`, `gitignore`, `tags`;
66
+ [CONFIGURATION section 4.1](../../docs/CONFIGURATION.md#41-fields-common-to-every-builder)
67
+ holds that table). `BashModBuilderModel` is the registered subclass; it
68
+ adds no fields.
69
+
70
+ The keys this plugin adds are `execute_command`, `configuration_user`,
71
+ `environment_vars` and `expect_disconnect`: each is a Packer
72
+ shell-provisioner argument, emitted on every shell provisioner of every
73
+ item of the builder.
74
+ [CONFIGURATION section 7.2](../../docs/CONFIGURATION.md#72-bash-remote)
75
+ holds the field table; [Configuration reference](#configuration-reference)
76
+ below gives this plugin's reading of each key. Rules only this plugin
77
+ enforces:
78
+
79
+ - `execute_command` (Packer's command template, for example
80
+ `sudo -E bash '{{.Path}}'`) is the way to change how the script is
81
+ invoked. `configuration_user` writes its own: every shell provisioner
82
+ of the builder gets `execute_command = "chmod +x {{ .Path }}; sudo su -
83
+ <user> -c '{{ .Vars }} {{ .Path }}'"`, so each item's lines run as that
84
+ user and the bake user only launches them. Because both say how the
85
+ script runs, declaring them together is refused at load.
86
+ - `extra_arguments` is not a field: a builder that declares it fails to
87
+ load with the unknown-key refusal. Use `execute_command` to change the
88
+ invocation.
82
89
 
83
90
  Base fields this plugin constrains: `executable` should name the `bash`
84
91
  executable (the fixture does), though the emission does not invoke it;
@@ -89,13 +96,28 @@ base-model rule). `config:` on the builder is carried but never read.
89
96
 
90
97
  Same file. Extends
91
98
  [`ModItemModel`](../base/src/cs_image_system/base/models/moditem_type.py)
92
- (`SubRootItem` -> `RootItem` -> `NameTyped`).
93
-
94
- | Field | Type | Default | Meaning |
95
- |---|---|---|---|
96
- | `script` | `list[str]` | `[]` | Inline command lines, run in order in one `provisioner "shell"` (`inline = [...]`). Literal shell; no templating. Blank entries are dropped. |
97
- | `scripts` | `list[str]` | `[]` | Script files, relative to the configuration root (the process's working directory, which the configuration load sets to the root), copied beside the Packer root and run as `scripts = [...]`. Blank entries are dropped. |
98
- | `ensure` | `dict[str, Any]` | `{}` | The declarative form. Allowed keys: `packages` (list of names), `files` (list of `{path, content, mode}`; `mode` defaults to `0644`), `services` (list of unit names, enabled and started), `commands` (list of `{run, unless}`; `run` executes only when `unless` fails). Every entry is validated at load by `validate_ensure` (stage 63 item 16): a wrong shape, an unknown key, a bad mode or an all-empty mapping raises `ValueError` naming the item and the entry. |
99
+ (`SubRootItem` -> `RootItem` -> `NameTyped`). The item's own keys are
100
+ `script`, `scripts` and `ensure`; the inherited ones (`name`, `type`,
101
+ `description`, `aliases`, `tags`, `config`) are the modification item's,
102
+ tabled in
103
+ [CONFIGURATION section 7.3](../../docs/CONFIGURATION.md#73-the-modification-item-inside-an-images-modifications),
104
+ and [Configuration reference](#configuration-reference) below reads every
105
+ key as this plugin does.
106
+
107
+ - `script` is the list of inline command lines, run in order in one
108
+ `provisioner "shell"` (`inline = [...]`). Literal shell; no templating.
109
+ Blank entries are dropped.
110
+ - `scripts` is the list of script files, relative to the configuration
111
+ root (the process's working directory, which the configuration load
112
+ sets to the root), copied beside the Packer root and run as
113
+ `scripts = [...]`. Blank entries are dropped.
114
+ - `ensure` is the declarative form. Allowed keys: `packages` (list of
115
+ names), `files` (list of `{path, content, mode}`; `mode` defaults to
116
+ `0644`), `services` (list of unit names, enabled and started),
117
+ `commands` (list of `{run, unless}`; `run` executes only when `unless`
118
+ fails). Every entry is validated at load by `validate_ensure`: a wrong
119
+ shape, an unknown key, a bad mode or an all-empty mapping raises
120
+ `ValueError` naming the item and the entry.
99
121
 
100
122
  Validation at load: at least one of `script`, `scripts`, `ensure` must be
101
123
  non-empty, otherwise `ValueError` ("must supply 'script' (inline lines),
@@ -118,15 +140,13 @@ Derived behaviour:
118
140
  - `remap_self_with_copied_assets(copied)`: replaces each `scripts` entry
119
141
  with its copied path when present in the mapping.
120
142
 
121
- Inherited item fields:
122
-
123
- | Base field | From | Default | Use in this plugin |
124
- |---|---|---|---|
125
- | `name` | `NameTyped` | required | Names the provisioner comment and the local bundle directory (`NN-<name>`). |
126
- | `type` | `ModItemModel` | `default` | Foreign key to a `mod_builders:` entry (see above). |
127
- | `description`, `aliases`, `tags` | `NameTyped` / `RootItem` | | Carried; not emitted. |
128
- | `config` | `RootItem` | `{}` | Not read by this plugin. It takes part in the lineage content hash of the modification and nothing else. |
129
- | `_model_id` | `ModItemModel` | set at load | The owning image's global id; `identified_model` resolves to the image. |
143
+ Of the inherited fields, `name` names the provisioner comment and the
144
+ local bundle directory (`NN-<name>`); `type` is the foreign key to a
145
+ `mod_builders:` entry (see above); `description`, `aliases` and `tags`
146
+ are carried and not emitted; `config` is not read by this plugin and
147
+ takes part in the lineage content hash of the modification and nothing
148
+ else; `_model_id`, set at load, is the owning image's global id, and
149
+ `identified_model` resolves to the image.
130
150
 
131
151
  ### A modification item with only `config:`
132
152
 
@@ -167,8 +187,8 @@ calls, in order:
167
187
  4. `generate_items_during_modification(image, mod, image_builder, phase, build_path)`:
168
188
  - `inline` = `ensure_lines()` followed by the `script` lines; `scripts`
169
189
  = the script files. When both lists are empty it logs a warning and
170
- emits nothing; since stage 63 item 16 an all-empty `ensure` is
171
- refused at load, so an item can no longer reach this.
190
+ emits nothing; an all-empty `ensure` is refused at load, so an item
191
+ cannot reach this.
172
192
  - Appends `# Modifications for <item name> of type <item type> (shell)`.
173
193
  - Then, because Packer's shell provisioner takes either `scripts` or
174
194
  `inline` but never both, one or two `provisioner "shell"` blocks:
@@ -181,7 +201,7 @@ calls, in order:
181
201
  with a `configuration_user`, `chmod +x {{ .Path }}; sudo su - <user>
182
202
  -c '{{ .Vars }} {{ .Path }}'` (Packer's `{{ .Vars }}`, the
183
203
  environment variables, travel inside the command because `su -`
184
- starts a fresh login environment), else none (stage 63).
204
+ starts a fresh login environment), else none.
185
205
  - Every string is rendered as an HCL literal: backslashes and double
186
206
  quotes are escaped, and `${` / `%{` become `$${` / `%%{` so shell text
187
207
  such as `dpkg-query -f='${Package}'` survives Packer's template parser.
@@ -196,9 +216,7 @@ modification bundle records the item: its directory
196
216
  `run.sh` that runs `sh '<script>'` per script file and then
197
217
  `sh ./inline.sh`, and a `MANIFEST.yaml` with `operation: bash` and the
198
218
  item's `idempotent` value. `csis-mods rerun` on the image therefore
199
- re-applies the declarative `ensure` form too (stage 63). Until 2026-09-25
200
- the `ensure` lines were not in the bundle and a rerun replayed only the
201
- script files and the `script` lines.
219
+ re-applies the declarative `ensure` form too.
202
220
 
203
221
  ## Emission
204
222
 
@@ -300,8 +318,8 @@ entry in `cfg/executables.yml`
300
318
  ([the fixture's](../../tests/fixtures/config/cfg/executables.yml) is
301
319
  `name: bash`, `binary: /usr/local/bin/bash`, no `version`), because
302
320
  `validate` checks every builder's `executable` against that list and every
303
- run checks every declared entry (stage 48: the binary must exist as an
304
- absolute path or a name on `PATH`, `BashVersionChecker` must parse a version
321
+ run checks every declared entry (the binary must exist as an absolute
322
+ path or a name on `PATH`, `BashVersionChecker` must parse a version
305
323
  from `<binary> --version`, and when the entry declares `version:` the parsed
306
324
  version must satisfy it). That is the whole extent of the plugin's use of a
307
325
  local bash: it is looked at, never run. The fixture declares no floor;
@@ -349,7 +367,7 @@ variable, profile or file of its own.
349
367
 
350
368
  Every key the model accepts. An unknown key is refused at load
351
369
  (`extra="forbid"` in the shared model config), and `parameters:` is refused
352
- by name (retired in stage 26).
370
+ by name.
353
371
 
354
372
  | Field | Type | Default | Meaning |
355
373
  |---|---|---|---|
@@ -363,24 +381,24 @@ by name (retired in stage 26).
363
381
  | `gitignore` | list[str] | `[]` | Accepted by the base model; nothing in this plugin reads it. |
364
382
  | `tags` | mapping | `{}` | Accepted, not read. |
365
383
  | `execute_command` | str or null | `null` | Emitted verbatim (HCL-escaped) as `execute_command` on every shell block of every item of this builder. Refused together with `configuration_user`. |
366
- | `configuration_user` | str or null | `null` | Read since stage 63 (accepted and ignored until 2026-09-25): every shell block of this builder runs its lines as this user through `execute_command = "chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'"`. Refused together with `execute_command` (`... both say how the script runs; declare one`). |
384
+ | `configuration_user` | str or null | `null` | Every shell block of this builder runs its lines as this user through `execute_command = "chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'"`. Refused together with `execute_command` (`... both say how the script runs; declare one`). |
367
385
  | `environment_vars` | list[str] | `[]` | Emitted as `environment_vars = [...]` on every shell block. |
368
386
  | `expect_disconnect` | bool | `false` | When true, `expect_disconnect = true` on every shell block. |
369
- | `extra_arguments` | | | Refused at load as an unknown key (removed in stage 63; it was accepted and never read). Use `execute_command`. |
387
+ | `extra_arguments` | | | Not a field; refused at load as an unknown key. Use `execute_command`. |
370
388
 
371
389
  ### The item (an image's `modifications:` entry whose `type:` names this builder)
372
390
 
373
391
  | Field | Type | Default | Meaning |
374
392
  |---|---|---|---|
375
393
  | `name` | str | required | Unique within the image. Names the comment line, the bundle directory `NN-<name>` and the lineage record. |
376
- | `type` | str | `default` | The builder's name, an alias, or `default` (or omitted). Rewritten to the builder's name at load (stage 63 item 15). Omitted or `default` with no `is_default: true` mod builder: refused at load (stage 63). |
394
+ | `type` | str | `default` | The builder's name, an alias, or `default` (or omitted). Rewritten to the builder's name at load. Omitted or `default` with no `is_default: true` mod builder: refused at load. |
377
395
  | `description` | str or null | `null` | Accepted, not read. |
378
396
  | `aliases` | list[str] | `[]` | Accepted, not read. |
379
397
  | `tags` | mapping | `{}` | Accepted, not read. |
380
398
  | `config` | mapping | `{}` | Not read by the plugin. Part of the modification's content hash, so a change re-bakes the image. Not passed to the shell. |
381
399
  | `script` | list[str] | `[]` | Inline lines, literal, in order, after the `ensure` lines. Blank and whitespace-only entries are dropped. |
382
400
  | `scripts` | list[str] | `[]` | Script files; each is copied beside the Packer root and run first, in order. Blank entries are dropped. An absolute path is not copied and is emitted as is. |
383
- | `ensure` | mapping | `{}` | Only the four keys below are allowed; any other key is refused at load. Every entry's values are validated at load since stage 63 item 16 (`validate_ensure`; see "What it tests and verifies"). |
401
+ | `ensure` | mapping | `{}` | Only the four keys below are allowed; any other key is refused at load. Every entry's values are validated at load (`validate_ensure`; see "What it tests and verifies"). |
384
402
 
385
403
  The `ensure` keys:
386
404
 
@@ -401,11 +419,11 @@ The `ensure` keys:
401
419
  `idempotent: unknown`, the same as free-form alone. `ensure` lines always
402
420
  precede the `script` lines in the inline block.
403
421
  - **`ensure` whose lists are all empty** (`ensure: {packages: []}`) is
404
- refused at load (stage 63 item 16). Before 2026-09-25 it passed load,
405
- counted as `declared`, and emitted nothing: the builder logged
406
- `Bash modification <name> has no script lines or files; nothing emitted`
407
- and the bake proceeded without a provisioner for it. The bundle directory
408
- still exists, with a `run.sh` that runs nothing.
422
+ refused at load, because such an item would count as `declared` and
423
+ emit nothing: the builder would log
424
+ `Bash modification <name> has no script lines or files; nothing emitted`,
425
+ the bake would proceed without a provisioner for it, and the bundle
426
+ directory would hold a `run.sh` that runs nothing.
409
427
  - **`commands` with `unless`** is guarded; **without `unless`** the command
410
428
  runs on every bake and on every `csis-mods rerun`, so the item is
411
429
  idempotent only if the command is.
@@ -415,7 +433,7 @@ The `ensure` keys:
415
433
  - **`execute_command` set** puts that template on every block of every
416
434
  item of the builder; **`configuration_user` set** instead puts
417
435
  `chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'`
418
- there, so the lines run as that user (stage 63); **both set** is refused
436
+ there, so the lines run as that user; **both set** is refused
419
437
  at load; **neither**, Packer's default applies and the lines run as the
420
438
  bake user. The same for
421
439
  `environment_vars` (emitted only when non-empty) and `expect_disconnect`
@@ -424,11 +442,10 @@ The `ensure` keys:
424
442
  `type`** reaches whichever mod builder is `is_default: true`; in the
425
443
  fixture that is `ansible-default`, so the item is loaded as an ANSIBLE
426
444
  item and its `script`/`scripts`/`ensure` keys are refused as unknown.
427
- With no `is_default: true` mod builder at all, the load is refused
428
- (stage 63): `No default builder found in mod_builder list.` first, and
429
- the orchestrator's `` modification '<name>' names no builder (`type`) ... ``
430
- for an item that reaches it anyway. Until 2026-09-25 such an item
431
- became an `AttributeError` at generation.
445
+ With no `is_default: true` mod builder at all, the load is refused:
446
+ `No default builder found in mod_builder list.` first, and the
447
+ orchestrator's `` modification '<name>' names no builder (`type`) ... ``
448
+ for an item that reaches it anyway.
432
449
  **`type: <alias>`** behaves exactly as the builder's name: the loader
433
450
  rewrites it.
434
451
  - **AWS versus GCE runtime**: the lines are identical; only the `only`
@@ -463,7 +480,7 @@ integer mode no higher than `0777`, and not every kind empty); an item
463
480
  with none of `script`, `scripts`, `ensure`; an item `type:` that names no
464
481
  builder; an item with no `type:` when no mod builder is the default; a
465
482
  builder declaring both `configuration_user` and `execute_command`, or
466
- the removed `extra_arguments` (stage 63). Each is a
483
+ `extra_arguments`. Each is a
467
484
  `ValueError` (or `KeyError` for the type) that stops the load; the CLI
468
485
  prints it and exits 1. Nothing loads, so nothing is generated.
469
486
 
@@ -516,81 +533,41 @@ failure prints `FAIL <image>/<mod>: rc=...` on stderr and exits 1. A
516
533
 
517
534
  ## When it fails
518
535
 
519
- Failures that have happened, in date order:
520
-
521
- - **2026-08-27, three defects found in the pre-V2 builder**
522
- ([LEDGER](../../docs/history/LEDGER.md), "Finding 2026-08-27"). Every
523
- item resolved to the default mod builder (the lookup used the field name
524
- `modifications`, not the item's `type`), so a bash item was recorded as
525
- `ansible-default` with no content; the bash builder wrote raw lines and an
526
- `exit 0` straight into the build file, so any real bash item failed
527
- `packer validate`; and `scripts:` was not a field, so files were dropped.
528
- Fixed at gate 8; guarded by
529
- [test_v2_gate8_modifications.py](../../tests/test_v2_gate8_modifications.py)
530
- and [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py). If
531
- a comment line ever appears outside a `provisioner "shell" {` block, or
532
- `exit 0` appears in a build file, this regressed.
533
- - **2026-09-10, the first `full-test`'s mod-test leg could not start a
534
- container** for an EL 10 image: Docker Hub's `rockylinux` library stops at
535
- 9. The stand-in for `rhel` 10 and above is `almalinux:<version>` since
536
- (`test_image_for` in [mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)).
537
- Symptom then: `docker run` failed to pull and the bash item's test never
538
- ran. If `test-mods` reports `unsupported` with
539
- `no local test image for the root OS family`, the family/version has no
540
- stand-in; set `local_test_image` on the OS builder.
541
- - **Found live, undated, in the sibling shell emitter for image tests**
542
- ([v2_provisioners.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_provisioners.py)):
543
- an `rpm --qf '%{NAME}'` line broke `packer fmt`, because `%{` and `${`
544
- open Packer template directives. This plugin's `_hcl_string` escapes both
545
- the same way. If `packer fmt` or `validate` ever reports
546
- `Invalid template directive` or `Invalid template interpolation` on a
547
- line you wrote, the escape regressed; the line in `generated/` should
548
- read `%%{` / `$${`.
549
- - **2026-09-20, an assertion that could not fail** (image `tests:`, not this
550
- plugin, but the same shell and the same trap;
551
- [OPERATIONS.md](../../docs/OPERATIONS.md), "What a green bake proves").
552
- Under `sh -e`, a command inside an AND-OR list other than the last does
553
- not abort the script, so a `script:` line written `check || { echo no; }`
554
- never fails the build however false `check` is. Two bakes went green
555
- asserting a package that did not exist. If a `script` line must be able
556
- to stop the bake, end it in something that exits nonzero (`|| exit 1`),
557
- not in a braced group that returns 0. The plugin's own `commands` form,
558
- `( unless ) || { run; }`, is deliberate: the group is the LAST element,
559
- so a failing `run` does abort.
560
- - **2026-09-23, an item declared with the builder's alias failed at
561
- generation** (found while writing this README, on a copy of the fixture
562
- with `type: bash` on `derivative-setup`). Load and `validate` passed; the
563
- run log then showed
564
- `AssertionError: No mod builder bash found in context for imgfile-basic-dask`
565
- and the summary was `ok: false` with no validation errors. Fixed by
566
- stage 63 item 15 (2026-09-25): the loader rewrites the alias to the
567
- builder's name, and `tests/test_v2_defects_loading.py` generates that
568
- same fixture copy.
569
-
570
- Failures the code raises that have not been seen outside tests:
536
+ Incidents that have happened, with their cause and fix, are recorded on
537
+ [INCIDENTS.md](../../docs/INCIDENTS.md); what follows keeps each
538
+ failure's symptom, its meaning and what to do.
539
+
540
+ Symptoms of the emission and of the bake, with what each means and what
541
+ to do:
571
542
 
572
543
  | What you see | Where | Meaning and what to do |
573
544
  |---|---|---|
545
+ | A `# Modifications for ...` comment line outside a `provisioner "shell" {` block, or `exit 0` in a build file | `generated/`; `packer validate` fails | The builder wrote raw lines into the build file instead of provisioner blocks. [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py) and [test_modifications.py](../../tests/test_modifications.py) pin the block form; this is a regression of the plugin, not of the item. |
546
+ | A bash item recorded in lineage as `ansible-default` with no content, or its script files dropped | `meta-state/lineage.yaml` after a bake | The item was resolved to the default mod builder instead of the builder its `type:` names. The same two tests pin the lookup by the item's `type`; check the item's `type:` names this builder. |
547
+ | `test-mods` reports `unsupported` with `no local test image for the root OS family` | `test-mods` | The OS family and version have no stand-in container image (`test_image_for` in [mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)): Docker Hub's `rockylinux` library stops at 9, so `rhel` 10 and above stand in as `almalinux:<version>`, and a family outside that table has none. Set `local_test_image` on the OS builder. |
548
+ | `Invalid template directive` or `Invalid template interpolation` from `packer fmt` or `validate` on a line you wrote | the Packer plugin's run-now commands | `%{` and `${` open Packer template directives; this plugin's `_hcl_string` escapes both as `%%{` / `$${` (the sibling shell emitter for image tests, [v2_provisioners.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_provisioners.py), does the same). If the line in `generated/` does not read so, the escape regressed. |
549
+ | A `script:` line written `check || { echo no; }` never fails the build however false `check` is | the bake, green | Under `sh -e` a command inside an AND-OR list other than the last does not abort the script, so a bake can go green asserting a package that does not exist (the same trap holds for an image's `tests:`; [OPERATIONS.md](../../docs/OPERATIONS.md), "What a green bake proves"). If a `script` line must be able to stop the bake, end it in something that exits nonzero (`\|\| exit 1`), not in a braced group that returns 0. The plugin's own `commands` form, `( unless ) \|\| { run; }`, is deliberate: the group is the LAST element, so a failing `run` does abort. |
550
+ | `AssertionError: No mod builder bash found in context for <image>` in the run log, summary `ok: false` with no validation errors, after load and `validate` passed | generation | An item's `type:` reached generation as the builder's alias; the loader rewrites an alias to the builder's name at load (`tests/test_modifications.py` generates a fixture copy with `type: bash`), so a tree whose loader does not is on an older form ([the stage index](../../docs/CHANGES.md#s63)). Write the builder's name, or update. |
574
551
  | `Bash modification '<name>' must supply 'script' (inline lines), 'scripts' (script files) and/or 'ensure' (declarative, idempotent)` | load; CLI exit 1 | The item has none of the three (a `config:`-only item). Add one or remove the item. |
575
552
  | `Bash modification '<name>': unknown ensure keys ['users']` | load; CLI exit 1 | Only `packages`, `files`, `services`, `commands` exist. Move anything else to `commands`. |
576
553
  | A pydantic error naming a key (`Extra inputs are not permitted`) on the builder or the item | load; CLI exit 1 | A misspelt or foreign key (`playbooks:` on a bash item, `script:` on an ansible item). Check the item's `type:` reaches the builder you meant; `type: default` reaches the default builder, which in the fixture is ansible. |
577
- | ``<builder>: `parameters` was retired (stage 26) ...`` | load; CLI exit 1 | Delete `parameters:` from the builder. |
554
+ | ``<builder>: `parameters` was retired ...`` | load; CLI exit 1 | Delete `parameters:` from the builder. |
578
555
  | `KeyError: Could not resolve builder '<type>' referenced in Image(...)` | load; CLI exit 1 | The item's `type:` names no `mod_builders:` entry. |
579
- | `` mod builder '<name>': `configuration_user` and `execute_command` both say how the script runs; declare one `` | load; CLI exit 1 | Stage 63. `configuration_user` writes its own `execute_command`; keep one. To run as another user with a custom command, put the `sudo su - <user>` in your `execute_command`. |
580
- | A pydantic unknown-key error naming `extra_arguments` on a `bash-remote` builder | load; CLI exit 1 | Stage 63 removed the field (it was never read). Delete it; change the invocation with `execute_command`. |
581
- | `No default builder found in mod_builder list.` | load; CLI exit 1 | No mod builder says `is_default: true`. Mark one, or give every item a `type:`. The orchestrator's own form, for an item that reaches it, is `` modification '<name>' names no builder (`type`) and no mod builder is `is_default: true`; name one with `type:` or mark a default `` (stage 63; it used to be an `AttributeError` at generation). |
556
+ | `` mod builder '<name>': `configuration_user` and `execute_command` both say how the script runs; declare one `` | load; CLI exit 1 | `configuration_user` writes its own `execute_command`; keep one. To run as another user with a custom command, put the `sudo su - <user>` in your `execute_command`. |
557
+ | A pydantic unknown-key error naming `extra_arguments` on a `bash-remote` builder | load; CLI exit 1 | The field does not exist. Delete it; change the invocation with `execute_command`. |
558
+ | `No default builder found in mod_builder list.` | load; CLI exit 1 | No mod builder says `is_default: true`. Mark one, or give every item a `type:`. The orchestrator's own form, for an item that reaches it, is `` modification '<name>' names no builder (`type`) and no mod builder is `is_default: true`; name one with `type:` or mark a default ``. |
582
559
  | `Executable bash specified for provider bash-remote not found in executables list.` | `validate`; exit 1 | Add a `bash` entry to `cfg/executables.yml` or change the builder's `executable:`. |
583
560
  | `bash: binary '/usr/local/bin/bash' not found (declared in cfg/executables.yml; an absolute path, or a name on PATH)` | `validate` and every run; exit 1 | The pinned path is wrong on this machine. Symlink the binary there or fix `binary:`. |
584
561
  | ``bash: BashVersionChecker could not parse a version from `/usr/local/bin/bash --version` `` | `validate` and every run | The first line of `--version` has no `version N.N`. Something other than bash is at that path. |
585
562
  | `bash 5.3.15 does not meet its requirement <spec> (cfg/executables.yml)` | `validate` and every run | The floor moved or the machine is behind. |
586
563
  | `FileNotFoundError: Bash modification script mod_image.sh does not exist or is not a file` | generation; run `ok: false`, traceback in the log | The path is resolved from the configuration root, not from the image file's directory or the shell's. Fix the path or `--root-dir`. |
587
564
  | `Bash modification '<name>': ensure declares [...] but every one is empty, so it would emit nothing` | load; CLI exit 1 | Every `ensure` list is empty. Fill one or drop `ensure`. |
588
- | `Bash modification '<name>': ensure.files[N]: `path` is required` (or `ensure.commands[N]: `run` is required`) | load; CLI exit 1 | A `files` entry without `path`, or a `commands` entry without `run`. Before stage 63 this was a `KeyError` at generation. |
589
- | `Bash modification '<name>': ensure.packages must be a list of package names, not str 'git'` | load; CLI exit 1 | `packages:` (or `services:`) given as a string; the old code iterated its letters. Write a list (`packages: [git]`). |
565
+ | `Bash modification '<name>': ensure.files[N]: `path` is required` (or `ensure.commands[N]: `run` is required`) | load; CLI exit 1 | A `files` entry without `path`, or a `commands` entry without `run`. |
566
+ | `Bash modification '<name>': ensure.packages must be a list of package names, not str 'git'` | load; CLI exit 1 | `packages:` (or `services:`) given as a string, which shell would iterate letter by letter. Write a list (`packages: [git]`). |
590
567
  | `Bash modification '<name>': ensure.files[N].mode 644 is not a mode YAML could have read from octal` | load; CLI exit 1 | An unquoted `644`, which YAML reads as a decimal number. Write `mode: "0644"`. |
591
568
  | `Bash modification '<name>': ensure.files[N]: unknown keys [...]` | load; CLI exit 1 | Only `path`, `content`, `mode` on a file and `run`, `unless` on a command. |
592
569
  | `csis ensure: no dnf, yum or apt-get to install <pkg>` | apply / `test-mods` | The image has none of the three package managers. Use a `script` line with the one it has. |
593
- | `dnf` (or `yum`, `apt-get`) error on a package step | apply / `test-mods` | The first package manager found could not install the package, and its message is the real one (no match, no repository, no network from the build subnet). Before stage 63 the line fell through to `apt-get` and showed `apt-get: command not found` last on an EL host. |
570
+ | `dnf` (or `yum`, `apt-get`) error on a package step | apply / `test-mods` | The first package manager found could not install the package, and its message is the real one (no match, no repository, no network from the build subnet); the others are never tried, so no misleading `apt-get: command not found` follows it on an EL host. |
594
571
  | `cmp: command not found` or `base64: command not found` | apply | The image lacks diffutils / coreutils. Add the package to an earlier item's `ensure.packages` or a `script` line. |
595
572
  | `sudo: a terminal is required to read the password` | apply | The bake ssh user has no passwordless `sudo`. This is an OS builder / runtime matter, not the item's. |
596
573
  | `Failed to enable unit: Unit <svc>.service does not exist` | apply | `services:` names a unit the image has not installed yet; install it earlier in the same item's `packages` (packages run before services). |
@@ -622,20 +599,20 @@ Failures the code raises that have not been seen outside tests:
622
599
  [abstract_version_checker.py](../base/src/cs_image_system/base/basic/abstract_version_checker.py).
623
600
  - The image builder that calls it and hosts its output:
624
601
  [packer-plugin](../packer-plugin)
625
- ([packer_ebs_builder.py](../packer-plugin/src/cs_image_system/packer_plugin/packer_ebs_builder.py),
602
+ ([packer_image_builder.py](../packer-plugin/src/cs_image_system/packer_plugin/packer_image_builder.py),
626
603
  [v2_local_mods.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_local_mods.py)).
627
604
  - Lineage records for modifications:
628
605
  [lineage.py](../base/src/cs_image_system/base/lineage.py) (`mod_records`);
629
606
  the release gate in [release.py](../base/src/cs_image_system/base/release.py).
630
607
  - Tests: [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py)
631
608
  (two blocks for files plus inline, builder options, script copying);
632
- [test_v2_gate8_modifications.py](../../tests/test_v2_gate8_modifications.py)
609
+ [test_modifications.py](../../tests/test_modifications.py)
633
610
  (both kinds emitted, unknown type refused, the load rule);
634
- [test_v2_explore_local_mods.py](../../tests/test_v2_explore_local_mods.py)
611
+ [test_modifications.py](../../tests/test_modifications.py)
635
612
  (`ensure` guards, idempotence recorded, the bundle);
636
- [test_v2_explore_mod_tests.py](../../tests/test_v2_explore_mod_tests.py)
613
+ [test_mod_tests.py](../../tests/test_mod_tests.py)
637
614
  (the container tests on the fixture's bash item);
638
- [test_v2_hygiene_three.py](../../tests/test_v2_hygiene_three.py)
615
+ [test_version_checks.py](../../tests/test_version_checks.py)
639
616
  (`BashVersionChecker` found by name, versions judged).
640
617
  - No terraform modules and no hashicorp-utils usage: the plugin writes
641
618
  Packer provisioner lines directly.
@@ -4,7 +4,7 @@
4
4
 
5
5
  [project]
6
6
  name = "cs-image-system-bash-mod-plugin"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  description = "The shell modification plugin: inline script, script files and an ensure form become Packer shell provisioners"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Mykel Alvis", email = "mykelalvis@infrastructurebuilder.org" }]
@@ -12,8 +12,8 @@ license = "Apache-2.0"
12
12
  license-files = ["LICENSE"]
13
13
  requires-python = ">=3.13"
14
14
  dependencies = [
15
- "cs-image-system-contract==0.1.6",
16
- "cs-image-system-base==0.1.6",
15
+ "cs-image-system-contract>=0.4,<0.5",
16
+ "cs-image-system-base==0.1.7",
17
17
  "pydantic>=2.13",
18
18
  ]
19
19
 
@@ -9,17 +9,15 @@ import shutil
9
9
  from pathlib import Path
10
10
  from typing import TypeVar, cast
11
11
 
12
- from cs_image_system.contract.assets import AssetSet
13
- from cs_image_system.base.bake_steps import execute_command_line
14
-
15
- from .bash_models import BASH_BUILDER, BASH_EXECUTABLE, BashBuilderModel, BashModItemModel, BashModBuilderModel
16
12
  from cs_image_system.base.basic.abstract_version_checker import AbstractVersionChecker
13
+ from cs_image_system.base.basic.builder_base_image import ImageBuilderBase, build_target_label_of
17
14
  from cs_image_system.base.basic.builder_base_mod import ModBuilderBase
18
- from cs_image_system.contract.lifecycle import ExecutionLifecyclePhase
19
15
  from cs_image_system.base.models.image import Image
20
- from cs_image_system.base.basic.builder_base_image import ImageBuilderBase
21
16
  from cs_image_system.base.models.moditem_type import ModItemModel
22
- from cs_image_system.base.basic.builder_base_image import build_target_label_of
17
+ from cs_image_system.contract.assets import AssetSet
18
+ from cs_image_system.contract.lifecycle import ExecutionLifecyclePhase
19
+
20
+ from .bash_models import BASH_BUILDER, BASH_EXECUTABLE, BashBuilderModel, BashModBuilderModel, BashModItemModel
23
21
 
24
22
  log = logging.getLogger(__name__)
25
23
 
@@ -81,14 +79,14 @@ class BashModBuilder(ModBuilderBase[Q]):
81
79
  for body in parts:
82
80
  lines += ['provisioner "shell" {', f' only = ["{build_target_label_of(ibb, image)}"]']
83
81
  lines += body
84
- command = self.model.effective_execute_command() # stage 63: configuration_user
82
+ command = self.model.effective_execute_command() # configuration_user
85
83
  if command:
86
84
  lines.append(f" execute_command = {_hcl_string(command)}")
87
85
  else:
88
- # stage 86: through the bake's step runner, so `ensure: packages`
86
+ # through the bake's step runner, so `ensure: packages`
89
87
  # and a script's own installs wait out a held package database;
90
88
  # a builder that says how its script runs keeps its own command
91
- lines.append(execute_command_line(" "))
89
+ lines.append(self._get_context().services.commands.execute_command_line(" "))
92
90
  if self.model.environment_vars:
93
91
  lines.append(" environment_vars = [" + ", ".join(_hcl_string(v) for v in self.model.environment_vars) + "]")
94
92
  if self.model.expect_disconnect:
@@ -4,13 +4,15 @@
4
4
 
5
5
  import re
6
6
  from dataclasses import field
7
- from cs_image_system.contract.model_config import CSIS_MODEL_CONFIG
8
- from pydantic.dataclasses import dataclass # stage 23: validation at construction
9
7
  from pathlib import Path
10
8
  from typing import Any
11
9
 
10
+ from pydantic.dataclasses import dataclass # validation at construction
11
+
12
12
  from cs_image_system.base.models.mod_builder import ModBuilderModel
13
13
  from cs_image_system.base.models.moditem_type import ModItemModel
14
+ from cs_image_system.contract.model_config import CSIS_MODEL_CONFIG
15
+
14
16
  BASH_EXECUTABLE: str = "bash"
15
17
 
16
18
  BASH_BUILDER: str = "bash-remote"
@@ -31,8 +33,8 @@ def _mode_text(mode: Any) -> str:
31
33
  """A file mode as the four-digit octal text ``install -m`` takes.
32
34
 
33
35
  YAML reads an unquoted ``0644`` as the integer 420, so an integer is
34
- rendered back in octal (stage 63 item 16, decided 2026-09-25); a
35
- string must already be three or four octal digits."""
36
+ rendered back in octal; a string must already be three or four octal
37
+ digits."""
36
38
  if isinstance(mode, bool):
37
39
  raise ValueError(f"mode {mode!r} is not a file mode")
38
40
  if isinstance(mode, int):
@@ -74,8 +76,8 @@ def _entries(label: str, kind: str, value: Any, allowed: set[str], required: str
74
76
 
75
77
 
76
78
  def validate_ensure(label: str, ensure: Any) -> dict[str, Any]:
77
- """Every ``ensure`` entry checked at load, one rule per kind (stage 63
78
- item 16). A valid mapping comes back UNCHANGED except that an integer
79
+ """Every ``ensure`` entry checked at load, one rule per kind. A valid
80
+ mapping comes back UNCHANGED except that an integer
79
81
  file mode becomes its octal text: the lineage fingerprint hashes this
80
82
  mapping, so a tree that was already valid keeps every fingerprint.
81
83
 
@@ -171,7 +173,7 @@ class BashModItemModel(ModItemModel):
171
173
  pkgs = [str(p) for p in self.ensure.get("packages", []) or []]
172
174
  if pkgs:
173
175
  joined = " ".join(pkgs)
174
- # stage 63 item 16: the FIRST package manager that exists installs,
176
+ # the FIRST package manager that exists installs,
175
177
  # and its failure is the failure (the old chain fell through dnf,
176
178
  # yum and apt-get, so an EL host showed `apt-get: command not found`)
177
179
  out.append(f"for p in {joined}; do rpm -q \"$p\" >/dev/null 2>&1 || dpkg -s \"$p\" >/dev/null 2>&1 || "
@@ -202,7 +204,7 @@ class BashBuilderModel(ModBuilderModel):
202
204
  """Dataclass representing a Bash script modification.
203
205
  """
204
206
  type = BASH_BUILDER
205
- # stage 63: when set, every item's lines run as this user -- the shell
207
+ # when set, every item's lines run as this user -- the shell
206
208
  # provisioner's execute_command becomes `sudo su - <user> -c '...'`, so the
207
209
  # bake user only launches them. Declaring it with execute_command is
208
210
  # refused (one of the two decides how the script runs).
@@ -3,10 +3,12 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  import logging
6
- from .bash_builder import BashModBuilder, BashVersionChecker
7
- from .bash_models import BASH_BUILDER, BASH_EXECUTABLE, BashModItemModel, BashModBuilderModel
6
+
8
7
  from cs_image_system.base.basic.abstract_plugin_metadata import AbstractPluginMetadata
9
8
 
9
+ from .bash_builder import BashModBuilder, BashVersionChecker
10
+ from .bash_models import BASH_BUILDER, BASH_EXECUTABLE, BashModBuilderModel, BashModItemModel
11
+
10
12
  log = logging.getLogger(__name__)
11
13
 
12
14
  class BashModTypes(AbstractPluginMetadata):
@@ -9,14 +9,14 @@ from typing import Any, cast
9
9
 
10
10
  from cs_image_system.bash_mod_plugin.bash_builder import BashModBuilder
11
11
  from cs_image_system.bash_mod_plugin.bash_models import BashModItemModel
12
- from cs_image_system.base.lifecycle import ExecutionLifecyclePhase
12
+ from cs_image_system.contract.lifecycle import ExecutionLifecyclePhase
13
13
 
14
14
 
15
15
  def _builder(**model_fields):
16
16
  fields = {"execute_command": None, "environment_vars": [], "expect_disconnect": False}
17
17
  fields.update(model_fields)
18
18
  model = SimpleNamespace(**fields)
19
- # stage 63: the builder asks the model how the script runs
19
+ # the builder asks the model how the script runs
20
20
  model.effective_execute_command = lambda: model.execute_command
21
21
  b = BashModBuilder.__new__(BashModBuilder)
22
22
  b._model = model # type: ignore[attr-defined]
@@ -25,8 +25,10 @@ def _builder(**model_fields):
25
25
 
26
26
  def _emit(builder, mod):
27
27
  image = SimpleNamespace(name="img")
28
+ # the image builder labels the build target through its runtime; a stub answers here
29
+ ibb = SimpleNamespace(build_target_label=lambda img: f"amazon-ebs.{img.name}")
28
30
  items = builder.generate_items_during_modification(
29
- image, mod, cast(Any, None), ExecutionLifecyclePhase.IMAGE_GENERATION, Path("build.pkr.hcl"))
31
+ image, mod, cast(Any, ibb), ExecutionLifecyclePhase.IMAGE_GENERATION, Path("build.pkr.hcl"))
30
32
  return "\n".join(a.value for a in items)
31
33
 
32
34