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,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'pathname'
5
+ require 'tempfile'
6
+ require 'fileutils'
7
+ require 'strscan'
8
+ require 'shellwords'
9
+
10
+ module Empeira
11
+ # Executed with the installed agent Ruby in the owned guest, never on the host.
12
+ # rubocop:disable-next Metrics/ClassLength -- Standalone guest helper contains both native package-manager adapters.
13
+ class RuntimeProxyGuest
14
+ APT_PATH = '/etc/apt/apt.conf.d/90-empeira-proxy'
15
+ DNF_PATH = '/etc/dnf/dnf.conf'
16
+ PROFILE_PATH = '/etc/profile.d/90-empeira-proxy.sh'
17
+ BEGIN_MARKER = "# BEGIN EMPEIRA RUNTIME PROXY\n"
18
+ END_MARKER = "# END EMPEIRA RUNTIME PROXY\n"
19
+
20
+ def initialize(request, root: Pathname('/'))
21
+ @desired = request.fetch('desired')
22
+ @accepted = request.fetch('accepted')
23
+ @root = Pathname(root)
24
+ end
25
+
26
+ def reconcile
27
+ changed = reconcile_apt if path('/etc/apt').directory?
28
+ changed = reconcile_dnf || changed if path('/etc/dnf').directory?
29
+ reconcile_profile || !!changed
30
+ end
31
+
32
+ private
33
+
34
+ def profile_content(definition)
35
+ variables = %w[HTTP_PROXY HTTPS_PROXY http_proxy https_proxy].to_h { |key| [key, definition&.fetch('url') || ''] }
36
+ variables.merge!(%w[NO_PROXY no_proxy].to_h { |key| [key, definition&.fetch('no_proxy') || ''] })
37
+ variables.merge!('ALL_PROXY' => '', 'all_proxy' => '')
38
+ exports = variables.map do |key, value|
39
+ "export #{key}=#{Shellwords.escape(value)}\n"
40
+ end.join
41
+ "# Managed by Empeira: normal runtime proxy environment.\n#{exports}"
42
+ end
43
+
44
+ # rubocop:disable-next Naming/PredicateMethod -- Clear stale container image environment when disabled.
45
+ def reconcile_profile
46
+ target = path(PROFILE_PATH)
47
+ current = read(target)
48
+ content = profile_content(@desired)
49
+ if current && ![@desired, *@accepted, nil].map { |entry| profile_content(entry) }.include?(current)
50
+ raise 'Foreign or modified /etc/profile.d/90-empeira-proxy.sh; refusing to replace it'
51
+ end
52
+ return false if content == current
53
+
54
+ write(target, content)
55
+ true
56
+ end
57
+
58
+ def path(value)
59
+ @root.join(value.delete_prefix('/'))
60
+ end
61
+
62
+ def read(target)
63
+ target.parent.ascend do |parent|
64
+ raise "Unsafe proxy configuration parent: #{parent}" if parent.symlink?
65
+ end
66
+ stat = target.lstat
67
+ unless stat.file? && stat.nlink == 1 && stat.uid == Process.euid
68
+ raise "Unsafe or unowned proxy configuration: #{target}"
69
+ end
70
+
71
+ target.binread
72
+ rescue Errno::ENOENT
73
+ nil
74
+ end
75
+
76
+ def write(target, content, mode: 0o644)
77
+ FileUtils.mkdir_p(target.parent, mode: 0o755)
78
+ Tempfile.create('.empeira-proxy-', target.parent) do |file|
79
+ file.chmod(mode)
80
+ file.write(content)
81
+ file.flush
82
+ file.fsync
83
+ File.rename(file.path, target)
84
+ end
85
+ end
86
+
87
+ def apt_content(definition, foreign = {})
88
+ return unless definition
89
+
90
+ url = definition.fetch('url')
91
+ globals = "// Managed by Empeira: normal proxy, no bootstrap credentials.\n" \
92
+ "Acquire::http::Proxy \"#{url}\";\nAcquire::https::Proxy \"#{url}\";\n"
93
+ globals + direct_lines(definition).reject { |line| foreign.key?(line.split.first.downcase) }.join
94
+ end
95
+
96
+ def direct_lines(definition)
97
+ definition.fetch('direct').flat_map do |host|
98
+ %w[http https].map { |protocol| "Acquire::#{protocol}::Proxy::#{host} \"DIRECT\";\n" }
99
+ end
100
+ end
101
+
102
+ def owned_apt?(current, definition)
103
+ globals = apt_content(definition.merge('direct' => []))
104
+ return false unless current.start_with?(globals)
105
+
106
+ lines = current.delete_prefix(globals).lines
107
+ lines.uniq == lines && (lines - direct_lines(definition)).empty?
108
+ end
109
+
110
+ # rubocop:disable-next Naming/PredicateMethod -- Reconciliation mutates the APT fragment.
111
+ def reconcile_apt
112
+ target = path(APT_PATH)
113
+ current = read(target)
114
+ verify_apt_file(current)
115
+
116
+ foreign = @desired ? foreign_apt_settings(target) : {}
117
+ verify_apt_globals(foreign) if @desired
118
+ content = apt_content(@desired, foreign)
119
+ return false if content == current
120
+
121
+ content ? write(target, content) : target.unlink
122
+ true
123
+ end
124
+
125
+ def verify_apt_file(current)
126
+ return unless current
127
+ return if @accepted.any? { |entry| owned_apt?(current, entry) }
128
+
129
+ raise 'Foreign or modified /etc/apt/apt.conf.d/90-empeira-proxy; refusing to replace or remove it'
130
+ end
131
+
132
+ def foreign_apt_settings(target)
133
+ files = path('/etc/apt/apt.conf.d').glob('*') + [path('/etc/apt/apt.conf')]
134
+ files.each_with_object({}) do |file, settings|
135
+ next if file == target || !file.exist? || file.directory?
136
+
137
+ settings.merge!(apt_settings(read(file)).transform_keys(&:downcase))
138
+ end
139
+ end
140
+
141
+ def verify_apt_globals(settings)
142
+ settings.slice('acquire::http::proxy', 'acquire::https::proxy').each do |key, value|
143
+ next if [@desired.fetch('url'), "#{@desired.fetch('url')}/"].include?(value)
144
+
145
+ raise "Foreign APT #{key} conflicts with the normal Empeira proxy"
146
+ end
147
+ end
148
+
149
+ # Parse native flat and nested scopes; host-specific proxy/DIRECT keys remain untouched.
150
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- Parse flat and nested native APT scopes without evaluating configuration.
151
+ def apt_settings(content)
152
+ scanner = StringScanner.new(content)
153
+ scopes = []
154
+ statement = []
155
+ values = {}
156
+ until scanner.eos?
157
+ next if scanner.scan(%r{\s+|//[^\n]*|/\*.*?\*/|#(?!(?:include|clear)\b)[^\n]*}m)
158
+
159
+ token = scanner.scan(/"(?:\\.|[^"\\])*"|[^\s{};]+|[{};]/)
160
+ raise 'Cannot safely parse foreign APT configuration' unless token
161
+ raise 'Foreign APT #include/#clear requires explicit proxy configuration review' if token.start_with?('#')
162
+
163
+ case token
164
+ when '{' then scopes << statement.join('::')
165
+ statement = []
166
+ when '}' then scopes.pop
167
+ statement = []
168
+ when ';'
169
+ values[[*scopes, statement.first].compact.join('::')] =
170
+ statement.last&.delete_prefix('"')&.delete_suffix('"')
171
+ statement = []
172
+ else statement << token
173
+ end
174
+ end
175
+ values
176
+ end
177
+
178
+ def dnf_content(definition)
179
+ return '' unless definition
180
+
181
+ "#{BEGIN_MARKER}proxy=#{definition.fetch('url')}\n#{END_MARKER}"
182
+ end
183
+
184
+ # rubocop:disable-next Naming/PredicateMethod -- Reconciliation mutates the DNF main block.
185
+ def reconcile_dnf
186
+ target = path(DNF_PATH)
187
+ current = read(target)
188
+ raise 'Missing /etc/dnf/dnf.conf; refusing to initialize foreign configuration' unless current
189
+
190
+ block = current[/#{Regexp.escape(BEGIN_MARKER)}.*?#{Regexp.escape(END_MARKER)}/m]
191
+ verify_dnf_block(current, block)
192
+ foreign = block ? current.sub(block, '') : current
193
+ desired = dnf_content(@desired)
194
+ content = integrate_dnf(foreign, desired)
195
+ return false if current == content
196
+
197
+ write(target, content, mode: target.stat.mode & 0o777)
198
+ true
199
+ end
200
+
201
+ def verify_dnf_block(current, block)
202
+ return unless current.include?(BEGIN_MARKER) || current.include?(END_MARKER)
203
+ return if owned_dnf_block?(current, block)
204
+
205
+ raise 'Modified Empeira DNF proxy block; refusing to replace or remove it'
206
+ end
207
+
208
+ def owned_dnf_block?(current, block)
209
+ block && @accepted.map { |entry| dnf_content(entry) }.include?(block) &&
210
+ current.scan(BEGIN_MARKER).size == 1 && current.scan(END_MARKER).size == 1 &&
211
+ current.match?(/^\s*\[main\][^\n]*\n#{Regexp.escape(block)}/i)
212
+ end
213
+
214
+ def integrate_dnf(content, desired)
215
+ sections = content.split(/(?=^\s*\[)/)
216
+ main = sections.grep(/\A\s*\[main\]\s*\n/i)
217
+ raise 'DNF requires one unambiguous [main] section for normal proxy integration' unless main.size == 1
218
+
219
+ verify_dnf_proxy(main.first) if @desired
220
+ # Insert directly after the section header without changing any existing lines.
221
+ replacement = main.first.sub(/(\A\s*\[main\][^\n]*\n)/i) { "#{Regexp.last_match(1)}#{desired}" }
222
+ sections.map { |section| section.equal?(main.first) ? replacement : section }.join
223
+ end
224
+
225
+ def verify_dnf_proxy(section)
226
+ section.each_line do |line|
227
+ match = /^\s*proxy\s*=\s*(.*?)\s*$/i.match(line)
228
+ next unless match
229
+ next if match[1] == @desired.fetch('url')
230
+
231
+ raise 'Foreign DNF [main] proxy conflicts with the normal Empeira proxy; repository overrides are preserved'
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ if $PROGRAM_NAME == __FILE__
238
+ begin
239
+ puts JSON.generate('changed' => Empeira::RuntimeProxyGuest.new(JSON.parse(ARGV.fetch(0))).reconcile)
240
+ rescue StandardError => e
241
+ warn e.message
242
+ exit 1
243
+ end
244
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # OpenSSH ProxyCommand delegates only the byte transport to the central runner.
4
+ require_relative '../../../lib/empeira'
5
+
6
+ runtime, id, port = ARGV
7
+ port ||= '22'
8
+ unless Empeira::Runtime.registry.names.include?(runtime) && id&.match?(/\A[a-f0-9]{12,64}\z/) &&
9
+ port.match?(/\A[0-9]{1,5}\z/) && port.to_i.between?(1, 65_535) && ARGV.size <= 3
10
+ abort 'Invalid managed SSH transport'
11
+ end
12
+ result = Empeira::Execution::Runner.new.stream(runtime, arguments: [
13
+ 'exec', '--interactive', id, '/opt/puppetlabs/puppet/bin/ruby',
14
+ '/usr/local/libexec/empeira-ssh-connect.rb', port
15
+ ])
16
+ exit result.exit_status
@@ -0,0 +1,15 @@
1
+ Port 22
2
+ AddressFamily inet
3
+ PermitRootLogin prohibit-password
4
+ PasswordAuthentication no
5
+ KbdInteractiveAuthentication no
6
+ PubkeyAuthentication yes
7
+ AuthenticationMethods publickey
8
+ AuthorizedKeysFile .ssh/authorized_keys
9
+ UsePAM yes
10
+ DisableForwarding yes
11
+ PermitTunnel no
12
+ X11Forwarding no
13
+ PermitUserEnvironment no
14
+ PrintMotd no
15
+ Subsystem sftp internal-sftp
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ # Initialize disposable node host keys, then hand control to sshd or systemd.
3
+ set -eu
4
+ mkdir -p /run/sshd
5
+ ssh-keygen -A
6
+ exec "$@"
@@ -0,0 +1,10 @@
1
+ ARG EMPEIRA_BASE_UBUNTU=library/ubuntu@sha256:da6fc2be547864451aa253836dd926da33623312df4a9a243e35dc877c378a78
2
+ FROM ${EMPEIRA_BASE_UBUNTU}
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+ RUN ["apt-get", "-o", "APT::Update::Error-Mode=any", "update"]
5
+ RUN ["apt-get", "install", "--yes", "--no-install-recommends", "ca-certificates"]
6
+ RUN ["sed", "-i", "/Types: deb/a Snapshot: yes", "/etc/apt/sources.list.d/ubuntu.sources"]
7
+ RUN ["apt-get", "--snapshot", "20261001T000000Z", "-o", "APT::Update::Error-Mode=any", "update"]
8
+ RUN ["apt-get", "--snapshot", "20261001T000000Z", "install", "--yes", "--no-install-recommends", "squid=7.2-2ubuntu2.2"]
9
+ RUN ["apt-get", "clean"]
10
+ USER proxy
@@ -0,0 +1,5 @@
1
+ # Official Ruby 3.4.11 on Alpine 3.23; no package installation or project build context.
2
+ ARG EMPEIRA_BASE_RUBY=library/ruby:3.4.11-alpine3.23
3
+ FROM ${EMPEIRA_BASE_RUBY}
4
+ USER 65534:65534
5
+ ENTRYPOINT ["ruby"]
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'openssl'
5
+ require 'resolv'
6
+ require 'timeout'
7
+
8
+ # Two fixed listeners for one backend. The TLS endpoint satisfies the PuppetDB
9
+ # terminus; the isolated Empeira network is the security boundary.
10
+ BACKEND = 'puppetdb-backend.empeira.internal'
11
+ MAX_CONNECTIONS = 64
12
+ tokens = Queue.new
13
+ MAX_CONNECTIONS.times { tokens << true }
14
+ context = OpenSSL::SSL::SSLContext.new
15
+ context.cert = OpenSSL::X509::Certificate.new(File.read('/empeira-relay/cert.pem'))
16
+ context.key = OpenSSL::PKey.read(File.read('/empeira-relay/key.pem'))
17
+ context.verify_mode = OpenSSL::SSL::VERIFY_NONE
18
+ listeners = { TCPServer.new('0.0.0.0', 8080) => false, TCPServer.new('0.0.0.0', 8081) => true }
19
+
20
+ def backend_address
21
+ Resolv::DNS.open(nameserver: [ENV.fetch('EMPEIRA_DNS_IP')], search: [], ndots: 1) do |dns|
22
+ dns.timeouts = 2
23
+ dns.getresource(BACKEND, Resolv::DNS::Resource::IN::A).address.to_s
24
+ end
25
+ end
26
+
27
+ def respond(connection, status)
28
+ connection.write("HTTP/1.1 #{status}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n")
29
+ rescue IOError, SystemCallError
30
+ nil
31
+ end
32
+
33
+ def pump(source, destination)
34
+ loop { destination.write(source.readpartial(16_384)) }
35
+ rescue EOFError
36
+ destination.close_write if destination.is_a?(TCPSocket)
37
+ end
38
+
39
+ # rubocop:disable-next Metrics/BlockLength -- Bounded bidirectional connection handling is kept together.
40
+ loop do
41
+ IO.select(listeners.keys).first.each do |listener|
42
+ socket = listener.accept
43
+ begin
44
+ tokens.pop(true)
45
+ rescue ThreadError
46
+ respond(socket, '503 Service Unavailable') unless listeners.fetch(listener)
47
+ socket.close
48
+ next
49
+ end
50
+ Thread.new(socket, listeners.fetch(listener)) do |client, tls|
51
+ connection = nil
52
+ upstream = nil
53
+ sender = nil
54
+ begin
55
+ connection = tls ? OpenSSL::SSL::SSLSocket.new(client, context) : client
56
+ Timeout.timeout(5) { connection.accept } if tls
57
+ request = Timeout.timeout(10) { connection.gets("\n", 8192) }
58
+ unless request&.match?(%r{\A(?:GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH) /\S* HTTP/1\.[01]\r?\n\z})
59
+ respond(connection, '400 Bad Request')
60
+ next
61
+ end
62
+ upstream = Socket.tcp(backend_address, 8080, connect_timeout: 5)
63
+ upstream.write(request)
64
+ Timeout.timeout(120) do
65
+ sender = Thread.new { pump(connection, upstream) }
66
+ pump(upstream, connection)
67
+ end
68
+ rescue Timeout::Error
69
+ respond(connection, '504 Gateway Timeout') if connection && !upstream
70
+ rescue IOError, SystemCallError, Resolv::ResolvError, SocketError, OpenSSL::SSL::SSLError
71
+ respond(connection, '502 Bad Gateway') if connection && !upstream
72
+ ensure
73
+ sender&.kill&.join
74
+ upstream&.close
75
+ connection&.close
76
+ client.close unless client.closed?
77
+ tokens << true
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: AGPL-3.0-only
4
+
5
+ require 'puppet'
6
+
7
+ # Read native server settings without selecting an environment.conf override.
8
+ Puppet.initialize_settings(ARGV)
9
+ Puppet.settings.preferred_run_mode = :server
10
+ print Puppet.settings.value(:environment_timeout)
@@ -0,0 +1,41 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ # Keep the standard Puppet config read-only outside upstream ownership sweeps.
5
+ stage_eyaml() {
6
+ for key in private_key public_key; do
7
+ source="/empeira-eyaml/${key}.pkcs7.pem"
8
+ destination="/etc/puppetlabs/puppet/eyaml/${key}.pkcs7.pem"
9
+ mode=0644
10
+ if [ "$key" = private_key ]; then mode=0600; fi
11
+ install -o puppet -g puppet -m "$mode" "$source" "${destination}.pending"
12
+ mv -f "${destination}.pending" "$destination"
13
+ done
14
+ }
15
+
16
+ if [ "${1:-}" = refresh-eyaml ]; then
17
+ stage_eyaml
18
+ exit 0
19
+ fi
20
+
21
+ upstream_entrypoint="$1"
22
+ config_destination="$2"
23
+ eyaml_mode="$3"
24
+ puppetdb_mode="$4"
25
+ shift 4
26
+ # Use Puppet's own writer: retain every other setting in the configured confdir.
27
+ # Environment-variable mappings are only supplementary image startup hints.
28
+ confdir="${config_destination%/*}"
29
+ /opt/puppetlabs/bin/puppet config set environment_timeout 0 --section server --confdir "$confdir"
30
+ native_timeout=$(/opt/puppetlabs/puppet/bin/ruby /empeira-server/environment.rb --confdir "$confdir")
31
+ if [ "$native_timeout" != 0 ]; then
32
+ echo "Native Puppetserver [server] environment_timeout is not 0 after configuration" >&2
33
+ exit 1
34
+ fi
35
+ if [ "$eyaml_mode" = staged ]; then
36
+ stage_eyaml
37
+ fi
38
+ if [ "$puppetdb_mode" = puppetdb ]; then
39
+ ln -sfn /empeira-server/puppetdb.conf "$config_destination"
40
+ fi
41
+ exec "$upstream_entrypoint" "$@"