empeira 0.1.0.pre.alpha.1

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.
Files changed (274) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +661 -0
  3. data/README.md +150 -0
  4. data/bin/empeira +18 -0
  5. data/config/defaults.yaml +229 -0
  6. data/docs/addons.md +331 -0
  7. data/docs/architecture.md +277 -0
  8. data/docs/configuration.md +1204 -0
  9. data/docs/development.md +472 -0
  10. data/docs/hiera-and-eyaml.md +361 -0
  11. data/docs/installation.md +280 -0
  12. data/docs/networking.md +362 -0
  13. data/docs/nodes.md +642 -0
  14. data/docs/proxy.md +136 -0
  15. data/docs/troubleshooting.md +361 -0
  16. data/lib/empeira/addons/environment.rb +82 -0
  17. data/lib/empeira/addons/error.rb +14 -0
  18. data/lib/empeira/addons/installation.rb +119 -0
  19. data/lib/empeira/addons/manifest.rb +112 -0
  20. data/lib/empeira/addons/service.rb +50 -0
  21. data/lib/empeira/addons/storage.rb +94 -0
  22. data/lib/empeira/agent/acquisition.rb +120 -0
  23. data/lib/empeira/agent/artifact.rb +41 -0
  24. data/lib/empeira/agent/authentication.rb +122 -0
  25. data/lib/empeira/agent/cache.rb +122 -0
  26. data/lib/empeira/agent/cache_storage.rb +82 -0
  27. data/lib/empeira/agent/download.rb +84 -0
  28. data/lib/empeira/agent/native_commands.rb +84 -0
  29. data/lib/empeira/agent/release_sources.rb +64 -0
  30. data/lib/empeira/agent/target.rb +66 -0
  31. data/lib/empeira/agent/versions.rb +29 -0
  32. data/lib/empeira/application.rb +86 -0
  33. data/lib/empeira/build.json +5 -0
  34. data/lib/empeira/build_info.rb +46 -0
  35. data/lib/empeira/cli/addons.rb +45 -0
  36. data/lib/empeira/cli/base.rb +73 -0
  37. data/lib/empeira/cli/completion.rb +42 -0
  38. data/lib/empeira/cli/config.rb +22 -0
  39. data/lib/empeira/cli/main.rb +114 -0
  40. data/lib/empeira/cli/node.rb +87 -0
  41. data/lib/empeira/cli/progress.rb +169 -0
  42. data/lib/empeira/cli/proxy.rb +36 -0
  43. data/lib/empeira/cli/update.rb +23 -0
  44. data/lib/empeira/completion/addons.rb +93 -0
  45. data/lib/empeira/completion/bash.rb +62 -0
  46. data/lib/empeira/completion/catalog.rb +170 -0
  47. data/lib/empeira/configuration/additional_services.rb +74 -0
  48. data/lib/empeira/configuration/addons.rb +85 -0
  49. data/lib/empeira/configuration/agent_schema.rb +163 -0
  50. data/lib/empeira/configuration/agent_values.rb +54 -0
  51. data/lib/empeira/configuration/bootstrap_guests.rb +42 -0
  52. data/lib/empeira/configuration/command_mocks.rb +65 -0
  53. data/lib/empeira/configuration/display.rb +20 -0
  54. data/lib/empeira/configuration/dns_rewrites.rb +58 -0
  55. data/lib/empeira/configuration/document.rb +67 -0
  56. data/lib/empeira/configuration/eyaml.rb +46 -0
  57. data/lib/empeira/configuration/hiera_modulepath.rb +40 -0
  58. data/lib/empeira/configuration/hiera_mount_schema.rb +62 -0
  59. data/lib/empeira/configuration/hiera_mounts.rb +83 -0
  60. data/lib/empeira/configuration/image_schema.rb +68 -0
  61. data/lib/empeira/configuration/loader.rb +70 -0
  62. data/lib/empeira/configuration/merge.rb +48 -0
  63. data/lib/empeira/configuration/network_redirects.rb +73 -0
  64. data/lib/empeira/configuration/node_images.rb +64 -0
  65. data/lib/empeira/configuration/schema.rb +215 -0
  66. data/lib/empeira/configuration/server_mounts.rb +106 -0
  67. data/lib/empeira/configuration/service_configuration.rb +40 -0
  68. data/lib/empeira/configuration/ssh_preferences.rb +83 -0
  69. data/lib/empeira/configuration/user.rb +46 -0
  70. data/lib/empeira/configuration/vm_interfaces.rb +147 -0
  71. data/lib/empeira/control_plane/additional_configurations.rb +106 -0
  72. data/lib/empeira/control_plane/browser.rb +73 -0
  73. data/lib/empeira/control_plane/controller.rb +763 -0
  74. data/lib/empeira/control_plane/discovery.rb +69 -0
  75. data/lib/empeira/control_plane/environment.rb +101 -0
  76. data/lib/empeira/control_plane/files.rb +263 -0
  77. data/lib/empeira/control_plane/health.rb +104 -0
  78. data/lib/empeira/control_plane/plan.rb +356 -0
  79. data/lib/empeira/control_plane/storage.rb +48 -0
  80. data/lib/empeira/errors.rb +9 -0
  81. data/lib/empeira/execution/console.rb +45 -0
  82. data/lib/empeira/execution/diagnostics.rb +210 -0
  83. data/lib/empeira/execution/launch_error.rb +14 -0
  84. data/lib/empeira/execution/result.rb +11 -0
  85. data/lib/empeira/execution/runner.rb +235 -0
  86. data/lib/empeira/execution/terminal.rb +35 -0
  87. data/lib/empeira/hostname_pattern.rb +14 -0
  88. data/lib/empeira/images/configuration.rb +44 -0
  89. data/lib/empeira/images/identity.rb +30 -0
  90. data/lib/empeira/images/recipe.rb +22 -0
  91. data/lib/empeira/images/reference.rb +43 -0
  92. data/lib/empeira/images/source.rb +19 -0
  93. data/lib/empeira/immutable.rb +19 -0
  94. data/lib/empeira/infrastructure/cleanup.rb +328 -0
  95. data/lib/empeira/infrastructure/definition.rb +59 -0
  96. data/lib/empeira/infrastructure/owned_files.rb +63 -0
  97. data/lib/empeira/infrastructure/recovery_inventory.rb +53 -0
  98. data/lib/empeira/infrastructure/service.rb +237 -0
  99. data/lib/empeira/infrastructure/status.rb +73 -0
  100. data/lib/empeira/infrastructure/store.rb +274 -0
  101. data/lib/empeira/modules/events.rb +67 -0
  102. data/lib/empeira/modules/git_safety.rb +60 -0
  103. data/lib/empeira/modules/host_git.rb +91 -0
  104. data/lib/empeira/modules/image.rb +27 -0
  105. data/lib/empeira/modules/installer.rb +67 -0
  106. data/lib/empeira/modules/request.rb +68 -0
  107. data/lib/empeira/modules/state.rb +87 -0
  108. data/lib/empeira/modules/storage.rb +35 -0
  109. data/lib/empeira/network/bootstrap_proxy.rb +157 -0
  110. data/lib/empeira/network/definition.rb +61 -0
  111. data/lib/empeira/network/direct_egress.rb +133 -0
  112. data/lib/empeira/network/egress.rb +27 -0
  113. data/lib/empeira/network/gateway.rb +79 -0
  114. data/lib/empeira/network/host_policy.rb +53 -0
  115. data/lib/empeira/network/naming.rb +28 -0
  116. data/lib/empeira/network/peer/adapter_image.rb +32 -0
  117. data/lib/empeira/network/peer/allocation.rb +45 -0
  118. data/lib/empeira/network/peer/attachment.rb +27 -0
  119. data/lib/empeira/network/peer/backend.rb +108 -0
  120. data/lib/empeira/network/peer/channel.rb +133 -0
  121. data/lib/empeira/network/peer/channel_server.rb +71 -0
  122. data/lib/empeira/network/peer/docker_adapter.rb +165 -0
  123. data/lib/empeira/network/peer/helper_security.rb +40 -0
  124. data/lib/empeira/network/peer/layout.rb +55 -0
  125. data/lib/empeira/network/peer/linux_podman.rb +114 -0
  126. data/lib/empeira/network/peer/podman_machine.rb +112 -0
  127. data/lib/empeira/network/policy.rb +39 -0
  128. data/lib/empeira/network/proxy_bindings.rb +52 -0
  129. data/lib/empeira/network/proxy_policy.rb +64 -0
  130. data/lib/empeira/network/redirects.rb +89 -0
  131. data/lib/empeira/network/resource.rb +50 -0
  132. data/lib/empeira/node/agent_installer.rb +31 -0
  133. data/lib/empeira/node/agent_package.rb +125 -0
  134. data/lib/empeira/node/agent_repository.rb +160 -0
  135. data/lib/empeira/node/apt_configuration.rb +85 -0
  136. data/lib/empeira/node/bootstrap.rb +17 -0
  137. data/lib/empeira/node/certificates.rb +98 -0
  138. data/lib/empeira/node/command_mock.rb +38 -0
  139. data/lib/empeira/node/command_mocks.rb +114 -0
  140. data/lib/empeira/node/container.rb +134 -0
  141. data/lib/empeira/node/container_agent_bootstrap.rb +43 -0
  142. data/lib/empeira/node/container_bootstrap.rb +52 -0
  143. data/lib/empeira/node/container_guest.rb +42 -0
  144. data/lib/empeira/node/container_lifecycle.rb +145 -0
  145. data/lib/empeira/node/container_preparation.rb +144 -0
  146. data/lib/empeira/node/container_state.rb +129 -0
  147. data/lib/empeira/node/definition.rb +20 -0
  148. data/lib/empeira/node/diagnostic_command.rb +19 -0
  149. data/lib/empeira/node/dnf_agent_repository.rb +202 -0
  150. data/lib/empeira/node/external_fact.rb +21 -0
  151. data/lib/empeira/node/image.rb +38 -0
  152. data/lib/empeira/node/interactive_tools.rb +33 -0
  153. data/lib/empeira/node/interface.rb +18 -0
  154. data/lib/empeira/node/inventory.rb +143 -0
  155. data/lib/empeira/node/package_bootstrap.rb +129 -0
  156. data/lib/empeira/node/package_proxy.rb +102 -0
  157. data/lib/empeira/node/package_sources.rb +19 -0
  158. data/lib/empeira/node/puppet_command.rb +12 -0
  159. data/lib/empeira/node/rpm_configuration.rb +62 -0
  160. data/lib/empeira/node/run_request.rb +52 -0
  161. data/lib/empeira/node/runtime_proxy.rb +86 -0
  162. data/lib/empeira/node/service.rb +72 -0
  163. data/lib/empeira/node/ssh_client.rb +25 -0
  164. data/lib/empeira/node/ssh_credentials.rb +78 -0
  165. data/lib/empeira/node/ssh_endpoint.rb +58 -0
  166. data/lib/empeira/node/ssh_identity.rb +37 -0
  167. data/lib/empeira/node/user_ssh.rb +60 -0
  168. data/lib/empeira/node/vm.rb +89 -0
  169. data/lib/empeira/node/vm_bootstrap.rb +53 -0
  170. data/lib/empeira/node/vm_lifecycle.rb +212 -0
  171. data/lib/empeira/node/vm_preflight.rb +37 -0
  172. data/lib/empeira/node/vm_preparation.rb +161 -0
  173. data/lib/empeira/node/vm_state.rb +161 -0
  174. data/lib/empeira/platform/facts.rb +66 -0
  175. data/lib/empeira/platform/kvm.rb +91 -0
  176. data/lib/empeira/platform/locations.rb +51 -0
  177. data/lib/empeira/platform/resolvers.rb +85 -0
  178. data/lib/empeira/platform/routes.rb +50 -0
  179. data/lib/empeira/progress.rb +70 -0
  180. data/lib/empeira/project_root.rb +47 -0
  181. data/lib/empeira/providers/lifecycle.rb +96 -0
  182. data/lib/empeira/providers/registry.rb +21 -0
  183. data/lib/empeira/providers/resource.rb +32 -0
  184. data/lib/empeira/providers.rb +55 -0
  185. data/lib/empeira/runtime/agent_helper.rb +34 -0
  186. data/lib/empeira/runtime/container.rb +184 -0
  187. data/lib/empeira/runtime/diagnostic_execution.rb +52 -0
  188. data/lib/empeira/runtime/docker.rb +87 -0
  189. data/lib/empeira/runtime/image_metadata.rb +83 -0
  190. data/lib/empeira/runtime/interface.rb +8 -0
  191. data/lib/empeira/runtime/mount_probe.rb +57 -0
  192. data/lib/empeira/runtime/podman.rb +122 -0
  193. data/lib/empeira/runtime/recipe_build.rb +103 -0
  194. data/lib/empeira/runtime/registry_diagnostics.rb +23 -0
  195. data/lib/empeira/runtime/service_arguments.rb +55 -0
  196. data/lib/empeira/runtime/service_images.rb +118 -0
  197. data/lib/empeira/runtime/service_inspection.rb +159 -0
  198. data/lib/empeira/runtime/services.rb +120 -0
  199. data/lib/empeira/runtime/update_helper.rb +97 -0
  200. data/lib/empeira/runtime/workspace_network.rb +69 -0
  201. data/lib/empeira/server/environment_configuration.rb +53 -0
  202. data/lib/empeira/server/http.rb +38 -0
  203. data/lib/empeira/server/puppetdb_runtime.rb +74 -0
  204. data/lib/empeira/server/relay_certificate.rb +76 -0
  205. data/lib/empeira/server/runtime.rb +90 -0
  206. data/lib/empeira/services/definition.rb +42 -0
  207. data/lib/empeira/updates/images.rb +104 -0
  208. data/lib/empeira/updates/modules.rb +45 -0
  209. data/lib/empeira/updates/service.rb +45 -0
  210. data/lib/empeira/version_requirement.rb +23 -0
  211. data/lib/empeira/vm/agent.rb +66 -0
  212. data/lib/empeira/vm/bootstrap_requirements.rb +69 -0
  213. data/lib/empeira/vm/certificate_runtime.rb +30 -0
  214. data/lib/empeira/vm/cloud_init.rb +171 -0
  215. data/lib/empeira/vm/disk.rb +136 -0
  216. data/lib/empeira/vm/guest.rb +123 -0
  217. data/lib/empeira/vm/guest_channel.rb +110 -0
  218. data/lib/empeira/vm/guest_network.rb +94 -0
  219. data/lib/empeira/vm/guest_output.rb +52 -0
  220. data/lib/empeira/vm/guest_shell.rb +79 -0
  221. data/lib/empeira/vm/guest_upload.rb +43 -0
  222. data/lib/empeira/vm/image_cache.rb +77 -0
  223. data/lib/empeira/vm/image_source.rb +91 -0
  224. data/lib/empeira/vm/interface.rb +8 -0
  225. data/lib/empeira/vm/interface_ownership.rb +124 -0
  226. data/lib/empeira/vm/interface_routes.rb +101 -0
  227. data/lib/empeira/vm/interfaces.rb +240 -0
  228. data/lib/empeira/vm/management.rb +73 -0
  229. data/lib/empeira/vm/prerequisites.rb +42 -0
  230. data/lib/empeira/vm/qemu.rb +83 -0
  231. data/lib/empeira/vm/qemu_runtime.rb +404 -0
  232. data/lib/empeira/vm/removal.rb +30 -0
  233. data/lib/empeira/vm/root_disk.rb +92 -0
  234. data/lib/empeira/vm/session_lock.rb +56 -0
  235. data/lib/empeira/vm/system_ssh.rb +29 -0
  236. data/lib/empeira/workspace.rb +42 -0
  237. data/lib/empeira.rb +209 -0
  238. data/resources/browser/relay.rb +22 -0
  239. data/resources/gateway/Containerfile +8 -0
  240. data/resources/gateway/bridge.rb +66 -0
  241. data/resources/gateway/configure.rb +323 -0
  242. data/resources/modules/Containerfile +15 -0
  243. data/resources/modules/Gemfile +5 -0
  244. data/resources/modules/Gemfile.lock +105 -0
  245. data/resources/modules/worker.rb +161 -0
  246. data/resources/network/adapter/Containerfile +13 -0
  247. data/resources/network/adapter/bridge_linux.go +51 -0
  248. data/resources/network/adapter/framing.go +45 -0
  249. data/resources/network/adapter/framing_test.go +53 -0
  250. data/resources/network/adapter/go.mod +3 -0
  251. data/resources/network/adapter/main_linux.go +273 -0
  252. data/resources/network/channel.rb +18 -0
  253. data/resources/network/ssh_connect.rb +32 -0
  254. data/resources/nodes/apt_sources.rb +66 -0
  255. data/resources/nodes/debian/Containerfile +10 -0
  256. data/resources/nodes/diagnostic_exec.rb +109 -0
  257. data/resources/nodes/interactive_tools.rb +139 -0
  258. data/resources/nodes/managed_file.rb +91 -0
  259. data/resources/nodes/management/guest_agent.py +415 -0
  260. data/resources/nodes/management/setup.sh +20 -0
  261. data/resources/nodes/management/shell_rc.sh +12 -0
  262. data/resources/nodes/management/system_account.sh +24 -0
  263. data/resources/nodes/management/system_home.sh +15 -0
  264. data/resources/nodes/rpm/Containerfile +10 -0
  265. data/resources/nodes/runtime_proxy.rb +244 -0
  266. data/resources/nodes/ssh/proxy.rb +16 -0
  267. data/resources/nodes/ssh/sshd_config +15 -0
  268. data/resources/nodes/ssh/start +6 -0
  269. data/resources/proxy/Containerfile +10 -0
  270. data/resources/relay/Containerfile +5 -0
  271. data/resources/relay/puppetdb.rb +81 -0
  272. data/resources/relay/server_environment.rb +10 -0
  273. data/resources/relay/server_start.sh +41 -0
  274. metadata +326 -0
@@ -0,0 +1,472 @@
1
+ # Development
2
+
3
+ ## Environment and quality checks
4
+
5
+ Install Git, Ruby 3.4, Bundler and the platform prerequisites in
6
+ [installation](installation.md). The gem requires Ruby 3.4+; Ruby 3.2 and 3.3 are
7
+ unsupported. Normal Ubuntu CI runs deterministic RSpec on Ruby 3.4 and 4.0, with
8
+ RuboCop and both dependency audits only on Ruby 3.4. Release builds use Ruby 3.4.
9
+ Use synthetic control repositories and disposable credentials.
10
+
11
+ ```console
12
+ bundle install
13
+ bundle exec rspec
14
+ bundle exec rubocop
15
+ bundle exec bundler-audit check --update
16
+ bundle exec bundler-audit check --gemfile-lock resources/modules/Gemfile.lock
17
+ ```
18
+
19
+ `bundle exec rake` also runs RSpec. Normal tests use isolated temporary projects,
20
+ local child processes and loopback fixtures. Explicit integration gates are pending
21
+ unless enabled; a pending test is not a successful runtime check. Security auditing
22
+ covers known Ruby advisories in both resolutions, not all container OS packages.
23
+ To reproduce either CI test job, activate the corresponding Ruby, install the locked
24
+ bundle and run `bundle exec rspec --exclude-pattern 'spec/integration/**/*_spec.rb'`.
25
+ Runtime and VM compatibility remain separate manual gates.
26
+
27
+ Addon tests (`spec/addon_*_spec.rb`) use the existing loopback Git fixture,
28
+ concurrent CLI subprocesses, a real PTY and the generated Bash completion. They
29
+ exercise branches, tags and commit refs without external Git services or container
30
+ startup. The complete `spec/fixtures/addon` is also the documented minimal addon.
31
+ When extending configuration, review `Schema::ADDON_PRIVATE_PATHS`: secret fields
32
+ and opaque content require explicit exclusion from the addon environment API.
33
+
34
+ Runtime proxy regression coverage runs real guest helpers against isolated fake
35
+ guest roots, checks foreign APT/DNF ownership, and exercises privileged VM command
36
+ environment separately from bootstrap. Never execute these helpers against the
37
+ developer's `/etc` in unit tests.
38
+
39
+ `spec/interactive_tools_spec.rb` executes native Bash against synthetic startup
40
+ files, covering login/non-login behavior, PATH preservation, duplicate prevention
41
+ and ownership failures. The manual runtime management gate also checks installed
42
+ agent commands on retained guests after restart.
43
+
44
+ The manual runtime management gate checks real VirtIO execution while Puppet sets
45
+ root shadow to `!`, replaces sudoers, removes the regular `empeira` account and
46
+ changes, restarts and disables system SSH. It verifies subsequent `node puppet`,
47
+ retry after exits 4/6, user SSH without the management endpoint, verified upload
48
+ cleanup, stop/start, interface facts and normal proxy reconciliation. The gate
49
+ combines a thin disk, first-catalog dummy/VLAN facts and normal proxy package
50
+ installation. `spec/vm_guest_spec.rb` covers streaming, full output, native status,
51
+ interruption, binary upload and operation identity against isolated Unix peers.
52
+ `spec/vm_guest_agent_spec.rb` runs the guest adapter against a synthetic private
53
+ transport and temporary files; its simulated numeric-UID boundary is not a real
54
+ root-lock test. `spec/vm_runtime_spec.rb` verifies ownership and QEMU endpoints.
55
+ `spec/vm_system_account_spec.rb` checks one-time regular account preparation with
56
+ synthetic passwd/shadow data. Run real gates with available runtime and acceleration:
57
+
58
+ ```bash
59
+ EMPEIRA_RUNTIME_MANAGEMENT_INTEGRATION=1 EMPEIRA_VM_RUNTIME=docker \
60
+ EMPEIRA_RUNTIME_MANAGEMENT_PROVIDERS=vm \
61
+ bundle exec rspec spec/integration/node_runtime_management_spec.rb
62
+ ```
63
+
64
+ Add `--example 'after Puppet locks root'` for the dedicated account-independent
65
+ management and user SSH acceptance scenario. The fixture uses persistent guest
66
+ files, disables the automatic Puppet service through ordinary Puppet resources,
67
+ and waits for completed user sessions to disappear before deleting their account.
68
+ The CLI agent runs remain real and include native exits 4/6 and retry.
69
+
70
+ Select `container` and `EMPEIRA_VM_RUNTIME=podman` for the corresponding rootless
71
+ Podman package/PATH/reconciliation gate. These remain manual, capability-dependent
72
+ tests rather than additions to the automatic heavy CI matrix.
73
+ For available Rocky Linux images, `EMPEIRA_DNF_RUNTIME_INTEGRATION=1` enables
74
+ `spec/integration/node_dnf_proxy_spec.rb` for containers and accelerated VMs. It
75
+ installs through a Puppet-owned additional signed repository while retaining the
76
+ original sources, then verifies restart and proxy disable/enable reconciliation.
77
+ The gate defaults to Rocky 9; set `EMPEIRA_DNF_GUEST_VERSION=8` to exercise Rocky 8.
78
+
79
+ Network regression gates exercise both node providers through their production
80
+ transports. The DNS gate checks live rewrite reloads without replacing nodes or
81
+ DNS. The redirect gate checks payload preservation, flow revocation and refusal
82
+ of external fallback under both runtimes. Run them on a capable local host:
83
+
84
+ ```bash
85
+ EMPEIRA_DNS_NODE_INTEGRATION=1 EMPEIRA_VM_RUNTIME=docker \
86
+ bundle exec rspec spec/integration/dns_rewrites_nodes_spec.rb
87
+ EMPEIRA_INTEGRATION=direct-egress EMPEIRA_REDIRECT_VM=1 \
88
+ EMPEIRA_REQUIRED_RUNTIMES=docker,podman \
89
+ bundle exec rspec spec/integration/network_redirects_spec.rb
90
+ ```
91
+
92
+ For workflow changes, parse `.github/workflows/*.yml` and run `actionlint` when
93
+ available. Review action pins, triggers and permissions as well as YAML syntax.
94
+ See [contributing](../CONTRIBUTING.md) for review, commit and label conventions.
95
+
96
+ ### Ruby 4.0 compatibility
97
+
98
+ Ruby 3.4 and 4.0 are supported. The deterministic suite passed locally on Linux
99
+ x86_64 with Ruby 3.4.7 and 4.0.7. Ruby 4.0 validation used an official Ruby container;
100
+ it does not establish macOS, container-engine or accelerated VM integration results.
101
+
102
+ Source development uses Bundler 4.0.22 from `Gemfile.lock`, matching the existing
103
+ module-helper tooling. With Ruby 4.0.7, the previous Bundler 2.4.19 redefined
104
+ `Gem::Platform` constants and emitted warnings in child processes. This failed 14
105
+ strict stderr, streaming, Bash completion and Puppetfile installer tests. Updating
106
+ Bundler preserves those output assertions; no application output filtering or
107
+ warning suppression is used. The dependency gem versions are unchanged.
108
+
109
+ The test checkout must be writable: the QEMU monitor-directory cleanup fixture
110
+ uses the checkout as a synthetic home. A read-only container bind mount caused one
111
+ additional filesystem failure; the compatibility run uses a writable disposable
112
+ copy. This fixture does not launch QEMU or establish real VM compatibility.
113
+
114
+ ## Real runtime tests
115
+
116
+ Docker needs a reachable Linux engine 28+ with isolated bridge and helper support.
117
+ Podman needs rootless Netavark; cgroup v2 is required for enforced limits and systemd
118
+ nodes. Bind mounts must be visible at canonical host paths. Runtime tests pull public
119
+ images and need sufficient memory, disk space and registry/package access. Chromium
120
+ requires substantially more storage than the standard OpenVox smoke.
121
+
122
+ Run one explicit scope and runtime through the existing test driver:
123
+
124
+ ```bash
125
+ EMPEIRA_TEST_SCOPE=containers EMPEIRA_REQUIRED_RUNTIMES=docker \
126
+ bundle exec ruby script/integration.rb
127
+ EMPEIRA_TEST_SCOPE=services EMPEIRA_REQUIRED_RUNTIMES=podman \
128
+ bundle exec ruby script/integration.rb
129
+ ```
130
+
131
+ | Scope | Existing suites / behavior |
132
+ | --- | --- |
133
+ | `containers` | Runtime contracts and real Ubuntu/RPM node lifecycle |
134
+ | `services` | Control plane, exported resources, proxy policy, OpenVox View and generated service YAML |
135
+ | `nodes` | Default OpenVox lifecycle and signed DNF installation |
136
+ | `network` | Gateway allow/deny/revocation and transparent TCP redirects from a managed container and Puppet server; Docker also exercises DNS resolvers/rewrites |
137
+ | `images` | Native metadata, immutable pins, reconciliation and synthetic registry authentication |
138
+ | `lifecycle` | Isolated runtime smoke, Hiera/EYAML, SSH, ownership and cleanup |
139
+ | `mounts` | Read-only/writable server mounts and selective reconciliation |
140
+ | `development` | Environment caching, live code and package bootstrap |
141
+ | `node-tools` | Diagnostic tools and agent-free node recipes |
142
+ | `modules` | Synthetic Git/r10k synchronization, live mounts and offline reuse |
143
+ | `update-plane` | Disposable helpers, artifact refresh and public Forge acquisition without infrastructure |
144
+ | `browser` | Real Chromium UI, internal reachability and isolation |
145
+ | `vm` | Accelerated VM lifecycle, proxy policy and dummy/VLAN interface reconciliation |
146
+ | `redirect-nodes` | TCP redirects from existing container/accelerated VM nodes and Puppet server; preserved HTTP bytes, target recreation, connection revocation and external canary |
147
+ | `dns-nodes` | DNS rewrite reload/removal with real container and accelerated VM nodes |
148
+ | `shared-network` | Production container/VM TCP/UDP peer lifecycle |
149
+ | `full` | All listed container scopes; excludes VM/dns-nodes/redirect-nodes/shared-network |
150
+
151
+ The `Manual runtime integration` workflow exposes these scopes through
152
+ `workflow_dispatch`. It selects one engine and has no automatic or scheduled heavy
153
+ run. A required unavailable runtime fails instead of becoming a successful skip.
154
+ Public-source outages and registry rate limits remain real validation failures.
155
+
156
+ The focused Squid regression uses a synthetic HTTP/HTTPS server on an isolated
157
+ private RFC 1918 subnet. Allowed and denied domains resolve to the same address;
158
+ the test checks domain allowlists, hostname rules, proxy reconciliation and blocked
159
+ direct egress for both engines:
160
+
161
+ ```bash
162
+ EMPEIRA_INTEGRATION=1 EMPEIRA_REQUIRED_RUNTIMES=docker,podman \
163
+ bundle exec rspec spec/integration/control_plane_spec.rb --example 'allows selected'
164
+ ```
165
+
166
+ The automatic PR workflow runs the existing Ubuntu default node lifecycle test:
167
+
168
+ ```bash
169
+ EMPEIRA_INTEGRATION=1 EMPEIRA_REQUIRED_RUNTIMES=docker \
170
+ bundle exec rspec spec/integration/node_spec.rb \
171
+ --example 'docker runs the default OpenVox node' --format documentation
172
+ ```
173
+
174
+ It creates a temporary Git control repository with an empty `.empeira.yaml`, applies
175
+ a real Hiera-backed file resource, verifies default agent installation and CA
176
+ signing, checks a second run returns zero changes, and waits for the actual catalog
177
+ and changed/unchanged reports in PuppetDB. It also exercises state retention, live
178
+ edits, certificate cleanup/reuse and CA recreation. Failure diagnostics are bounded
179
+ and sanitized. Cleanup runs after failures and verifies that workspace-labeled
180
+ containers, networks and volumes are absent. No global prune is used.
181
+ The workflow reads all software pins from `config/defaults.yaml`; it needs no
182
+ repository secrets and uses public artifacts only.
183
+
184
+ Before the PR smoke, its disposable hosted Docker daemon uses Google's public
185
+ [Docker Hub cache](https://docs.cloud.google.com/artifact-registry/docs/pull-cached-dockerhub-images)
186
+ at `mirror.gcr.io`. Setup preserves existing daemon settings and verifies the active
187
+ mirror after restart. This also covers Docker Hub base images used by local builds;
188
+ GHCR images keep their original source. Image references, configured digest pins,
189
+ TLS verification and the complete smoke assertions remain unchanged. Cache hits
190
+ avoid the shared runner's anonymous Docker Hub pull quota. Cache misses still use
191
+ Docker Hub, and acquisition failures remain failures. The public-only smoke uses a
192
+ private, empty `DOCKER_CONFIG` directory instead of inheriting runner registry
193
+ credentials. Failures include bounded, sanitized Docker daemon diagnostics so a
194
+ failed mirror and canonical-registry fallback remain visible. Local engines and
195
+ workspace DNS, gateway and isolation policy are unaffected by this CI-only setup.
196
+
197
+ The APT authentication regression uses a local HTTPS repository with synthetic Basic
198
+ credentials and an ephemeral GPG signing key. It checks the helper's effective
199
+ `auth.conf.d` configuration, private file mode and exact credential bytes, then
200
+ verifies metadata returns 401 before one interactive login and 200 after the retry.
201
+ The selected package download must reuse that login and pass its SHA-256 check.
202
+ It requires host `gpg`/`gpgconf` and a locally reachable container engine; helper and
203
+ host TLS verification remain enabled. Run it explicitly for either engine:
204
+
205
+ ```bash
206
+ EMPEIRA_INTEGRATION=agent-auth EMPEIRA_REQUIRED_RUNTIMES=docker \
207
+ bundle exec rspec spec/integration/agent_repository_auth_spec.rb --example docker
208
+ EMPEIRA_INTEGRATION=agent-auth EMPEIRA_REQUIRED_RUNTIMES=podman \
209
+ bundle exec rspec spec/integration/agent_repository_auth_spec.rb --example podman
210
+ ```
211
+
212
+ Lightweight CI runs deterministic RSpec excluding `spec/integration`, RuboCop and
213
+ both dependency audits. Both quality and PR smoke support `main` and `initial`
214
+ during the transition; no branch migration is performed by a workflow.
215
+
216
+ ## VM and shared-network prerequisites
217
+
218
+ Use real KVM on Linux/WSL2 or HVF on macOS, QEMU, `qemu-img`, `xorriso`, OpenSSH and
219
+ any required firmware. Preflight runs before image/cache or runtime mutation.
220
+ Rootless Podman also needs writable TUN/KVM inside its namespace. See
221
+ [VM installation](installation.md#vm-prerequisites) and
222
+ [networking](networking.md#runtime-attachment-and-privilege-boundary).
223
+
224
+ The VM scope also includes `spec/integration/vm_interfaces_spec.rb`. It observes real
225
+ Linux dummy/VLAN types, VLAN IDs, addresses and activation, checks Facter JSON, and
226
+ reads a file written by the first catalog from the ordinary network bindings. It
227
+ creates the VM with both a 32 GiB disk and interface rules, verifies the thin overlay,
228
+ unchanged base checksum and root filesystem growth, and checks package installation.
229
+ Later interface edits and removal use `vm.disk: 48` while retaining the original
230
+ 32 GiB capacity through reconciliation and restart. The deterministic VM provider
231
+ regressions verify root growth precedes packages and interfaces, and that failed
232
+ growth blocks both features, enrollment and Puppet.
233
+ The real gate also tests idempotent `up`, stop/start restoration, address/VLAN edits and removal,
234
+ with managed SSH, DNS, proxy, gateway redirects and Puppet TLS checks. This gate
235
+ requires the same capable disposable host; deterministic guest models do not prove
236
+ real kernel or Facter compatibility.
237
+
238
+ The real VM lifecycle regression verifies the default managed SSH login and explicit
239
+ overrides, then exercises retained disk and console recovery. A second case injects
240
+ a permanent guest error at the final APT backup removal: the restored archive must
241
+ remain verifiable, Puppet must stay blocked, and SSH/console access must release the
242
+ workspace lock while preventing concurrent stop of that instance. The injected error
243
+ does not reproduce an unexplained historical SSH disconnect.
244
+ The lifecycle case also verifies a 32 GiB thin overlay, unchanged base checksum,
245
+ grown root partition/filesystem, package installation and `df -h /`. After changing
246
+ `vm.disk` to 48 GiB, workspace reconciliation and restart must retain the 32 GiB disk.
247
+ The VM lifecycle and container `lifecycle` smoke also load personal SSH defaults
248
+ before creating nodes. They exercise hostname rules, `~/` identity expansion,
249
+ independent CLI overrides and the unchanged internal bootstrap/Puppet transports.
250
+
251
+ ```bash
252
+ EMPEIRA_VM_INTEGRATION=1 EMPEIRA_VM_RUNTIME=docker \
253
+ bundle exec rspec spec/integration/vm_spec.rb --format documentation
254
+ ```
255
+
256
+ ```bash
257
+ EMPEIRA_TEST_SCOPE=vm EMPEIRA_REQUIRED_RUNTIMES=docker \
258
+ bundle exec ruby script/integration.rb
259
+ EMPEIRA_TEST_SCOPE=shared-network EMPEIRA_REQUIRED_RUNTIMES=podman \
260
+ bundle exec ruby script/integration.rb
261
+ ```
262
+
263
+ GitHub VM scopes require a dedicated disposable self-hosted Linux/AMD64 runner
264
+ labeled `empeira-vm`, with the selected runtime and acceleration already configured.
265
+ Ordinary GitHub-hosted runners are not assumed to have KVM/HVF. Use only trusted
266
+ manual revisions on that runner; do not expose it to fork PR code. A macOS/HVF
267
+ host runs the same local gates separately. Report Linux/WSL2, macOS, amd64 and
268
+ arm64 execution independently.
269
+
270
+ The lower-level regression proofs remain opt-in:
271
+
272
+ ```bash
273
+ EMPEIRA_SHARED_NETWORK=1 bundle exec rspec spec/integration/shared_network_spec.rb
274
+ EMPEIRA_MACOS_NETWORK=podman bundle exec rspec spec/integration/macos_shared_network_spec.rb
275
+ EMPEIRA_MACOS_NETWORK=docker bundle exec rspec spec/integration/macos_shared_network_spec.rb
276
+ EMPEIRA_SHARED_GUEST_CHECK=1 bundle exec rspec spec/integration/shared_guest_artifact_spec.rb
277
+ ```
278
+
279
+ The last command is an explicit TCG test-artifact diagnostic, not evidence of
280
+ accelerated product VM support. These proofs do not substitute for the production
281
+ peer lifecycle gate. Test arbitrary TCP/UDP between providers, internal browser
282
+ reachability, DNS, egress revocation, unchanged node identities and owned cleanup.
283
+
284
+ The network scope includes real transparent redirect tests for both runtimes.
285
+ The accelerated scope adds a managed QEMU VM to the same traffic assertions:
286
+
287
+ ```bash
288
+ EMPEIRA_TEST_SCOPE=redirect-nodes EMPEIRA_REQUIRED_RUNTIMES=docker bundle exec ruby script/integration.rb
289
+ EMPEIRA_TEST_SCOPE=redirect-nodes EMPEIRA_REQUIRED_RUNTIMES=podman bundle exec ruby script/integration.rb
290
+ ```
291
+
292
+ It verifies DELETE/path/Host/custom header/body and a complete 64 KiB response,
293
+ same-service hairpin connections, changed source/target ports, multiple sources,
294
+ owned target recreation at a different IP, unchanged node/service identities,
295
+ preserved connections on identical `up`, revoked established connections on removal,
296
+ and zero requests to an external canary even with an overlapping egress grant.
297
+ This manual scope requires KVM/HVF preflight and is excluded from `full` and normal CI.
298
+
299
+ The Docker network scope includes real CoreDNS tests for exact DNS rewrites:
300
+
301
+ ```sh
302
+ EMPEIRA_INTEGRATION=1 EMPEIRA_REQUIRED_RUNTIMES=docker \
303
+ bundle exec rspec spec/integration/dns_rewrites_spec.rb
304
+ EMPEIRA_TEST_SCOPE=dns-nodes EMPEIRA_REQUIRED_RUNTIMES=docker \
305
+ bundle exec ruby script/integration.rb
306
+ ```
307
+
308
+ The first gate uses isolated CoreDNS and DNS-client containers to check A/AAAA/CNAME
309
+ answers, dynamic service addresses, exact matching, reload/removal and no external
310
+ fallback. The manual `dns-nodes` scope additionally requires accelerated QEMU and
311
+ tests both real container and VM nodes across repeated `up` without restarting
312
+ them. It is deliberately separate from ordinary CI and container-only `full`.
313
+
314
+ ## Build information and releases
315
+
316
+ GitHub tags are the released-version authority. `BuildInfo` embeds version, revision
317
+ and UTC build time without requiring Git at runtime. Source checkouts report
318
+ `development`; the gem packaging fallback is `0.0.0.development`.
319
+
320
+ The manual release workflow accepts `tag`, `channel` (`alpha`, `beta`, `rc`, `stable`)
321
+ and `dry_run` (default true). Dispatch it from `main`; a prerelease tag must match
322
+ its channel, for example `v0.1.0-alpha.1`. Other branches and existing tags/releases
323
+ are rejected. Quality and real Docker/OpenVox gates must pass before the build.
324
+
325
+ The build uses the existing `rake build_info` task in a temporary source staging
326
+ directory, builds an installable gem, and checks its contents, license, normalized
327
+ Gem version and exact embedded metadata. RubyGems normalizes a tag such as
328
+ `v0.1.0-alpha.1` to gem version `0.1.0.pre.alpha.1`; CLI BuildInfo retains
329
+ `0.1.0-alpha.1`. No source version file or generated commit is maintained.
330
+
331
+ Test the build locally without creating tags or contacting GitHub:
332
+
333
+ ```bash
334
+ EMPEIRA_BUILD_VERSION=v0.1.0-alpha.1 \
335
+ EMPEIRA_RELEASE_CHANNEL=alpha \
336
+ EMPEIRA_BUILD_REVISION=0123456789abcdef0123456789abcdef01234567 \
337
+ EMPEIRA_BUILD_TIME=2026-01-01T00:00:00Z \
338
+ bundle exec ruby script/release.rb build
339
+ ```
340
+
341
+ The result is in ignored `pkg/`. Source `lib/empeira/build.json` is untouched.
342
+ The lower-level task also supports `rake 'build_info[OUTPUT_PATH]'`; without that
343
+ argument it writes the ignored source metadata. Remove source metadata to return
344
+ that checkout to development mode.
345
+
346
+ For publishing, only the final job gets `contents: write`. It downloads the exact
347
+ built gem, checks its checksum and metadata, confirms the selected revision is still
348
+ `main`, creates the tag atomically and prepares a draft release with its asset and
349
+ [GitHub native release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes).
350
+ It verifies the draft's tag revision, release channel and sole uploaded gem's name,
351
+ size and SHA-256 before publishing. A final API read verifies the published state
352
+ and the same tag/asset; missing assets, a retained draft or an API error fail the run.
353
+ Alpha/beta/rc are prereleases and do not become latest stable.
354
+ Dry-run performs all quality/build gates but skips the entire publishing job.
355
+
356
+ ### Release failure and recovery
357
+
358
+ Tag creation and GitHub release publication are separate operations. A failed run
359
+ can leave a tag with no visible release, an incomplete draft, or an already published
360
+ release when the final verification failed. None of these states is treated as a
361
+ successful workflow run. Validation reports an existing draft/published **GitHub
362
+ release** first; an existing **tag** without a visible release gets a separate
363
+ diagnostic. A read token may not see a retained draft, so maintainers must inspect
364
+ both the release list and the failed run with write access.
365
+
366
+ 1. Inspect the failed publishing step, the tag's commit, draft/published state and
367
+ attached assets. Compare the tag with the tested revision and the gem with the
368
+ build job's checksum and embedded BuildInfo. Workflow artifacts are retained for
369
+ seven days; do not substitute an unverified rebuild.
370
+ 2. For a tag without a release or an incomplete draft, a maintainer may manually
371
+ complete the original publication at that exact tested revision using the verified
372
+ artifact and native release notes. Verify all assets before publishing the draft.
373
+ If the original artifact or validation evidence is unavailable, choose a new version.
374
+ 3. If publication already happened, inspect and verify the remote release instead
375
+ of retrying publication. An incorrect or incomplete published release requires a
376
+ corrected release under a new version. A failed verification does not prove that
377
+ publication was rolled back.
378
+
379
+ The workflow never resumes an existing tag/release, deletes tags, moves tags or
380
+ reuses published version names. Rerunning it with the same retained tag is rejected;
381
+ another workflow run requires a new unused version. Manual recovery does not turn
382
+ the original failed run green.
383
+
384
+ With [GitHub Immutable Releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases)
385
+ enabled, published assets cannot be added, replaced or deleted, and the associated
386
+ tag cannot be moved or deleted while the release exists. Even deleting that release
387
+ does not permit reuse of its tag name. Drafts remain editable until publication;
388
+ the workflow therefore uploads and verifies the entire asset set before publishing.
389
+ Title and release-note edits do not repair incorrect immutable assets.
390
+
391
+ Release notes use merged PR labels from `.github/release.yml`, independently of
392
+ Conventional Commit types. See [label conventions](../CONTRIBUTING.md#release-note-labels).
393
+ The first distribution is a GitHub-attached Ruby gem; RubyGems.org publication,
394
+ standalone Linux/macOS binaries and automatic self-update are not implemented.
395
+
396
+ ## Dependency and image provenance
397
+
398
+ Thor is the sole application runtime gem. Development tools and the isolated r10k
399
+ installer are separate dependency resolutions. Dependencies are not vendored into
400
+ the release gem. Check upstream licensing and advisories before upgrades:
401
+
402
+ - [Thor](https://rubygems.org/gems/thor), [RSpec](https://rubygems.org/gems/rspec),
403
+ [RuboCop](https://rubygems.org/gems/rubocop), [Rake](https://rubygems.org/gems/rake)
404
+ and [Bundler](https://rubygems.org/gems/bundler): MIT.
405
+ - [bundler-audit](https://github.com/rubysec/bundler-audit): GPL-3.0-or-later development tool.
406
+ - [r10k](https://github.com/puppetlabs/r10k) and
407
+ [puppet_forge](https://github.com/puppetlabs/forge-ruby): Apache-2.0; reviewed versions
408
+ and checksums are in `resources/modules/Gemfile.lock`. Log4r's packaged
409
+ `doc/content/license.html` supplies permissive Ruby-style terms.
410
+ - The official [Go builder](https://github.com/docker-library/golang) uses BSD-style
411
+ Go licensing; its notice is retained in the adapter image. The adapter uses only
412
+ the standard library. Go is not a host prerequisite and no compiled adapter is bundled.
413
+ - GitHub-owned checkout/upload/download actions and ruby/setup-ruby are pinned by
414
+ commit; their MIT notices stay upstream. No action code is copied into the project.
415
+
416
+ Preserve package/image notices in locally built helpers. Registry pins establish
417
+ artifact integrity, not a universal signature or vulnerability guarantee.
418
+ See [image provenance](configuration.md#control-plane-images-and-provenance) and
419
+ [node image provenance](nodes.md#node-images-and-fidelity) for component sources.
420
+
421
+ The private adapter uses Python 3 already supplied by the reviewed cloud images;
422
+ it introduces no Python packages or guest-agent package bootstrap dependency.
423
+ QEMU Guest Agent was evaluated: its `guest-exec-status` returns captured output
424
+ only after exit, may report truncation and offers no command cancellation API.
425
+ The owned VirtIO adapter provides immediate streaming and process-group cleanup.
426
+ See the [QGA protocol reference](https://www.qemu.org/docs/master/interop/qemu-ga-ref.html).
427
+
428
+ Run `EMPEIRA_INTEGRATION=server-environment bundle exec rspec spec/integration/server_environment_spec.rb`
429
+ to check native timeout 0, live edits, CA retention, PuppetDB and idempotent up
430
+ under Docker/rootless Podman with the default server and a custom entrypoint
431
+ that ignores timeout environment mapping.
432
+ The same gate verifies startup and idempotent `up` with an explicit
433
+ `environment.conf` timeout override, preserving the file and distinguishing its
434
+ effective value from the native server setting.
435
+
436
+ Run the isolated diagnostics/recovery gate with each native runtime:
437
+
438
+ ```sh
439
+ EMPEIRA_DIAGNOSTIC_INTEGRATION=1 EMPEIRA_DIAGNOSTIC_RUNTIME=docker bundle exec rspec spec/integration/node_diagnostics_spec.rb
440
+ EMPEIRA_DIAGNOSTIC_INTEGRATION=1 EMPEIRA_DIAGNOSTIC_RUNTIME=podman bundle exec rspec spec/integration/node_diagnostics_spec.rb
441
+ ```
442
+
443
+ Both runs include container and QEMU/KVM nodes; select either with
444
+ `EMPEIRA_DIAGNOSTIC_PROVIDERS=container` or `vm` when the other is unavailable.
445
+ The VM gate requires acceleration and exercises root locked by Puppet, disabled
446
+ SSHD, separated exec streams, exact exits, timeouts, concurrent VirtIO sessions,
447
+ real PTY resize/job control/fullscreen output and terminal restoration, restart,
448
+ serial console without the guest service, and cleanup after removal of the server
449
+ and control repository with an incompatible inventory. Projects and resources
450
+ are disposable and isolated; production infrastructure is never used.
451
+
452
+ ## DNS endpoint, node identity and Hiera selection gates
453
+
454
+ These opt-in gates use disposable synthetic control repositories and owned helpers.
455
+ The DNS upstream is synthetic; no production DNS server is needed. The identity
456
+ gate requires working KVM/HVF and checks Ubuntu containers and VMs, multi-component
457
+ FQDNs, native Facter values, certnames, restart, legacy fact upgrades and an actual
458
+ first catalog using shared/provider Hiera data. Physical host files are checked for
459
+ changes. Run both container engines where available:
460
+
461
+ ```bash
462
+ EMPEIRA_HIERA_SELECTION_INTEGRATION=1 EMPEIRA_HIERA_SELECTION_RUNTIME=docker bundle exec rspec spec/integration/hiera_selection_spec.rb
463
+ EMPEIRA_DNS_REDIRECT_INTEGRATION=1 EMPEIRA_DNS_REDIRECT_RUNTIME=docker bundle exec rspec spec/integration/dns_redirects_spec.rb
464
+ EMPEIRA_NODE_IDENTITY_INTEGRATION=1 EMPEIRA_NODE_IDENTITY_RUNTIME=docker bundle exec rspec spec/integration/node_identity_spec.rb
465
+ ```
466
+
467
+ Repeat with `podman`. Hiera selection uses the configured server image, including
468
+ priority/merge lookups and a negative production-node fixture with no Empeira fact.
469
+ The DNS gate checks UDP/TCP, internal/external names, NXDOMAIN, large-answer fallback,
470
+ unavailable/recreated CoreDNS, a second workspace with a different subnet and
471
+ CoreDNS address, rule removal and idempotent reconciliation. Node identity also
472
+ checks both virtual DNS transports before and after node stop/start.