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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Empeira
6
+ module Server
7
+ # Export only the CA-issued service identity needed by the owned relay.
8
+ class RelayCertificate
9
+ NAME = 'puppetdb.empeira.internal'
10
+ SSL = '/etc/puppetlabs/puppet/ssl'
11
+
12
+ def initialize(context:)
13
+ @directory = context.locations.workspace(context.workspace).join('puppetdb-relay')
14
+ end
15
+
16
+ def path(name)
17
+ @directory.join(name).to_s
18
+ end
19
+
20
+ def prepare(runtime:, server:)
21
+ raise Error, 'Relay certificate directory is an unsafe symlink' if @directory.symlink?
22
+ return if File.file?(path('cert.pem')) && File.file?(path('key.pem'))
23
+
24
+ reject_partial_identity!
25
+
26
+ FileUtils.mkdir_p(@directory, mode: 0o700)
27
+ File.chmod(0o700, @directory)
28
+ issue(runtime, server)
29
+ export(runtime, server)
30
+ ensure
31
+ remove_files(%w[cert.pem.pending key.pem.pending]) unless @directory.symlink?
32
+ end
33
+
34
+ def issue(runtime, server)
35
+ result = runtime.service_exec(server, [Node::Certificates::CA, 'ca', 'generate', '--certname', NAME],
36
+ timeout: 60)
37
+ raise Error, 'Puppet CA could not issue the relay certificate; inspect server logs' unless result.success?
38
+ end
39
+
40
+ def export(runtime, server)
41
+ { 'cert.pem' => "#{SSL}/certs/#{NAME}.pem",
42
+ 'key.pem' => "#{SSL}/private_keys/#{NAME}.pem" }.each do |name, source|
43
+ temporary = path("#{name}.pending")
44
+ runtime.copy_from(server, source, temporary)
45
+ File.chmod(name == 'key.pem' ? 0o600 : 0o644, temporary)
46
+ File.rename(temporary, path(name))
47
+ end
48
+ end
49
+
50
+ def reject_partial_identity!
51
+ return unless File.exist?(path('cert.pem')) || File.exist?(path('key.pem'))
52
+
53
+ raise Error, 'PuppetDB relay certificate is incomplete; restore or destroy the workspace'
54
+ end
55
+
56
+ def cleanup
57
+ raise Error, 'Relay certificate directory is an unsafe symlink' if @directory.symlink?
58
+ return unless @directory.directory?
59
+
60
+ remove_files(%w[cert.pem key.pem cert.pem.pending key.pem.pending])
61
+ @directory.rmdir if @directory.children.empty?
62
+ end
63
+
64
+ private
65
+
66
+ def remove_files(names)
67
+ names.each do |name|
68
+ file = path(name)
69
+ File.unlink(file) if File.exist?(file) || File.symlink?(file)
70
+ end
71
+ rescue SystemCallError
72
+ raise Error, 'Cannot remove owned relay certificate files; cleanup is incomplete'
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module Server
5
+ # One Puppet-compatible runtime contract, with OpenVox defaults in configuration.
6
+ class Runtime
7
+ ENVIRONMENT_KEY = /\A[A-Za-z_][A-Za-z0-9_]*\z/
8
+ SEMANTICS = %w[certname dns_alt_names ca_hostname autosign server_hostname environment_timeout java_args
9
+ max_active_instances puppetdb_enabled storeconfigs reports puppetdb_server_urls].freeze
10
+ STARTUP_REQUIRED = %w[entrypoint arguments].freeze
11
+ STARTUP = [*STARTUP_REQUIRED, 'eyaml_keys'].freeze
12
+ PATHS = %w[puppetdb_config].freeze
13
+ FIELDS = { 'startup' => STARTUP, 'paths' => PATHS, 'environment_keys' => SEMANTICS }.freeze
14
+
15
+ def self.validate!(definition, path = 'server.runtime', effective: false)
16
+ validate_definition!(definition, path, effective: effective)
17
+ end
18
+
19
+ def self.validate_definition!(definition, location, effective:)
20
+ validate_fields!(definition, FIELDS.keys, location, effective: effective)
21
+ definition.each do |key, value|
22
+ child = "#{location}.#{key}"
23
+ validate_fields!(value, FIELDS.fetch(key), child, effective: effective)
24
+ validate_component!(key, value, child)
25
+ end
26
+ end
27
+
28
+ def self.validate_component!(key, value, location)
29
+ value.each do |field, item|
30
+ raise ConfigurationError, "#{location}.#{field} is invalid" unless valid_item?(key, field, item)
31
+ end
32
+ return unless key == 'environment_keys' && value.values.uniq.size != value.size
33
+
34
+ raise ConfigurationError, "#{location} must not reuse an environment variable name"
35
+ end
36
+
37
+ def self.valid_item?(component, field, item)
38
+ case component
39
+ when 'startup'
40
+ return valid_arguments?(item) if field == 'arguments'
41
+ return %w[direct staged].include?(item) if field == 'eyaml_keys'
42
+
43
+ safe_path?(item)
44
+ when 'paths' then safe_path?(item)
45
+ else item.is_a?(String) && item.match?(ENVIRONMENT_KEY)
46
+ end
47
+ end
48
+
49
+ def self.validate_fields!(value, allowed, location, effective:)
50
+ raise ConfigurationError, "#{location} must be a mapping" unless value.is_a?(Hash)
51
+
52
+ raise ConfigurationError, "#{location} contains an unsupported field" unless (value.keys - allowed).empty?
53
+ return unless effective
54
+
55
+ required = location.end_with?('.startup') ? STARTUP_REQUIRED : allowed
56
+ missing = required - value.keys
57
+ raise ConfigurationError, "#{location}.#{missing.first} is required" unless missing.empty?
58
+ end
59
+
60
+ def self.safe_path?(value)
61
+ value.is_a?(String) && value.start_with?('/') && value != '/' &&
62
+ value.match?(%r{\A/[A-Za-z0-9_./-]+\z}) && !value.split('/').intersect?(%w[. ..]) &&
63
+ !value.include?('//')
64
+ end
65
+
66
+ def self.valid_arguments?(value)
67
+ value.is_a?(Array) && value.all? do |item|
68
+ item.is_a?(String) && !item.empty? && !item.match?(/[\x00-\x1f\x7f]/)
69
+ end
70
+ end
71
+
72
+ def self.resolve(config)
73
+ definition = config.fetch('server').fetch('runtime')
74
+ validate!(definition, effective: true)
75
+ new(definition)
76
+ end
77
+ attr_reader :startup, :paths, :environment_keys
78
+
79
+ def initialize(definition)
80
+ @startup = definition.fetch('startup')
81
+ @paths = definition.fetch('paths')
82
+ @environment_keys = definition.fetch('environment_keys')
83
+ end
84
+
85
+ def environment(values)
86
+ values.to_h { |semantic, value| [environment_keys.fetch(semantic.to_s), value] }
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module Services
5
+ # This domain describes containers without selecting a container engine.
6
+ class Definition
7
+ attr_reader :key, :options, :workspace
8
+
9
+ def initialize(key:, workspace:, **options)
10
+ @key = key
11
+ @workspace = workspace
12
+ @options = Immutable.deep_freeze(options.transform_keys(&:to_s))
13
+ end
14
+
15
+ def name
16
+ "empeira-#{workspace.id}-#{key}"
17
+ end
18
+
19
+ def fingerprint
20
+ Infrastructure::Definition.fingerprint(options.except('dns', 'runtime_environment', 'runtime_ip'))
21
+ end
22
+
23
+ def ownership_labels
24
+ { 'io.empeira.managed-by' => 'empeira', 'io.empeira.workspace' => workspace.id,
25
+ 'io.empeira.purpose' => key }
26
+ end
27
+
28
+ def labels
29
+ ownership_labels.merge('io.empeira.definition' => fingerprint)
30
+ end
31
+
32
+ def verify!(resource, expected_id: nil)
33
+ return unless resource
34
+ return if resource.fetch('name') == name &&
35
+ ownership_labels.all? { |key, value| resource.fetch('labels')[key] == value } &&
36
+ (expected_id.nil? || expected_id == resource.fetch('id'))
37
+
38
+ raise Providers::OwnershipError, "Service resource ownership or recorded ID conflicts: #{key}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module Updates
5
+ # Container artifacts only; existing containers and immutable VM bases stay attached to their revisions.
6
+ class Images
7
+ def initialize(context:, runtime:, progress: Progress.new)
8
+ @context = context
9
+ @config = context.configuration
10
+ @runtime = runtime
11
+ @progress = progress
12
+ end
13
+
14
+ def refresh
15
+ @state = Infrastructure::Store.new(context: @context).load
16
+ if @state && @state.fetch('runtime') != @context.container_engine
17
+ raise Error, 'Workspace belongs to another runtime; select its owning runtime before updating images'
18
+ end
19
+
20
+ artifacts = catalog
21
+ @runtime.with_image_updates do
22
+ artifacts.each_with_index do |entry, index|
23
+ update(entry, index, artifacts.size)
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def update(entry, index, total)
31
+ label = entry.fetch(:label)
32
+ @progress.count(index, total, "#{label} (#{index}/#{total})")
33
+ result = @runtime.refresh_image(entry.fetch(:image), **entry.except(:image, :label)) do |status|
34
+ @progress.count(index, total, "#{label} — #{status} (#{index + 1}/#{total})")
35
+ end
36
+ @progress.count(index + 1, total, "#{label} — #{result} (#{index + 1}/#{total})")
37
+ rescue Error => e
38
+ raise e.class, "#{label} (#{entry.fetch(:image)}): #{e.message}", cause: nil
39
+ end
40
+
41
+ def catalog
42
+ (service_images + utility_images + node_images).uniq { |entry| entry.fetch(:image) }
43
+ end
44
+
45
+ def service_images
46
+ references = [
47
+ ['Configuration server', reference(@config.dig('images', 'server'))],
48
+ ['CoreDNS', reference(@config.dig('images', 'dns'))],
49
+ ['Browser', reference(@config.dig('browser', 'image'))]
50
+ ]
51
+ references.concat(database_images)
52
+ references.concat(@config.dig('containers', 'additional').map do |entry|
53
+ [entry.fetch('name'), reference(entry.fetch('image'))]
54
+ end)
55
+ references.map { |label, image| { image: image, label: label } }
56
+ end
57
+
58
+ def database_images
59
+ return [] unless @config.dig('puppetdb', 'enabled')
60
+
61
+ [['PuppetDB', reference(@config.dig('images', 'puppetdb'))],
62
+ ['PostgreSQL', reference(@config.dig('images', 'postgres'))]]
63
+ end
64
+
65
+ def utility_images
66
+ installer = Empeira::Modules::Image.new(@config.dig('images', 'r10k'),
67
+ registry: @config.dig('images', 'registry'))
68
+ adapter = Network::Peer::AdapterImage.new(context: @context)
69
+ direct = ControlPlane::Plan.new(context: @context).gateway_artifact
70
+ [utility_artifact('relay', 'Service relay', purpose: 'relay'), utility_artifact('proxy', 'Squid proxy'),
71
+ direct.merge(label: 'Workspace gateway'),
72
+ { label: 'r10k helper', image: installer.reference, recipe: installer.recipe, files: installer.files },
73
+ { label: 'VM network helper', image: adapter.image, recipe: adapter.recipe, files: adapter.files }]
74
+ end
75
+
76
+ def utility_artifact(key, label, **)
77
+ Empeira::Images::Configuration.artifact(
78
+ @config.dig('images', key), registry: @config.dig('images', 'registry'), **
79
+ ).merge(label: label)
80
+ end
81
+
82
+ def node_images
83
+ architecture = @runtime.architecture
84
+ node_variants.map do |os, version|
85
+ request = Node::RunRequest.from_config(hostname: 'image-update', provider: 'container', config: @config)
86
+ .with(os: os, version: version)
87
+ image = Node::Image.new(config: @config, request: request, architecture: architecture)
88
+ { label: "#{os.capitalize} #{version} node image", image: image.reference,
89
+ recipe: image.recipe, files: image.build_files }
90
+ end
91
+ end
92
+
93
+ def node_variants
94
+ used = @state.to_h.fetch('nodes', {}).values.select { |record| record['provider'] == 'container' }
95
+ [@config.fetch('node_defaults').values_at('os', 'version'),
96
+ *used.map { |record| record.values_at('os', 'version') }].uniq
97
+ end
98
+
99
+ def reference(config)
100
+ Empeira::Images::Configuration.reference(config, registry: @config.dig('images', 'registry'))
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module Updates
5
+ # Caller owns the existing workspace lock. r10k updates the live module directory in place.
6
+ class Modules
7
+ def initialize(context:, runtime:, runner:, progress: Progress.new)
8
+ @context = context
9
+ @runtime = runtime
10
+ @runner = runner
11
+ @progress = progress
12
+ @storage = Empeira::Modules::Storage.new(context: context)
13
+ @state = Empeira::Modules::State.new(context: context)
14
+ end
15
+
16
+ def synchronize
17
+ request = Empeira::Modules::Request.new(context: @context)
18
+ request.warnings.each { |message| @progress.warning(message) }
19
+ return unless request.present?
20
+
21
+ Empeira::Modules::GitSafety.new(project: @context.project.path, runner: @runner, progress: @progress)
22
+ .verify(@storage.root, recommend: true)
23
+ install(request)
24
+ end
25
+
26
+ private
27
+
28
+ # rubocop:disable-next Metrics/AbcSize -- Preserve the ordered direct-sync and success-marker boundary.
29
+ def install(request)
30
+ @progress.stage(0, 'Reading Puppetfile...')
31
+ image = Empeira::Modules::Image.new(@context.configuration.dig('images', 'r10k'),
32
+ registry: @context.configuration.dig('images', 'registry'))
33
+ @runtime.ensure_image(image.reference, recipe: image.recipe, files: image.files)
34
+ @storage.prepare
35
+ @state.prepare
36
+ @state.invalidate
37
+ git = Empeira::Modules::HostGit.new(runner: @runner, project: @context.project.path, progress: @progress)
38
+ installer = Empeira::Modules::Installer.new(runtime: @runtime, image: image.reference,
39
+ git: git, progress: @progress)
40
+ names = installer.synchronize(request, @storage.root, state: @state)
41
+ @state.record(@storage.root, names)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module Updates
5
+ class Service
6
+ TARGETS = %w[images modules all].freeze
7
+
8
+ def initialize(context: nil, runner: nil, runtimes: nil, build_info: BuildInfo.load, progress: Progress.new)
9
+ @context = context
10
+ @runner = runner
11
+ @runtimes = runtimes
12
+ @build_info = build_info
13
+ @progress = progress
14
+ end
15
+
16
+ def self_update
17
+ raise UnavailableFeature,
18
+ 'Empeira self-update is not implemented yet. Package replacement requires release packaging.'
19
+ end
20
+
21
+ def update(target)
22
+ raise Error, "Select an update target: #{TARGETS.join(', ')}" unless TARGETS.include?(target)
23
+ return update_artifacts(target) unless target == 'all'
24
+
25
+ raise UnavailableFeature,
26
+ 'update all is unavailable: it includes Empeira itself, and self-update is not implemented.'
27
+ end
28
+
29
+ private
30
+
31
+ def update_artifacts(target)
32
+ @build_info.require_compatible!(@context.configuration.dig('requirements', 'empeira'))
33
+ Infrastructure::Store.new(context: @context).with_lock do
34
+ runtime = @runtimes.build(@context.container_engine, context: @context, runner: @runner)
35
+ runtime.check_available!
36
+ if target == 'modules'
37
+ Modules.new(context: @context, runtime: runtime, runner: @runner, progress: @progress).synchronize
38
+ else
39
+ Images.new(context: @context, runtime: runtime, progress: @progress).refresh
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems/requirement'
4
+ require 'rubygems/version'
5
+
6
+ module Empeira
7
+ class VersionRequirement
8
+ def self.parse(value)
9
+ return if value.nil?
10
+
11
+ raise ArgumentError unless valid?(value)
12
+
13
+ Gem::Requirement.new(*value.split(',').map(&:strip))
14
+ rescue ArgumentError
15
+ raise ConfigurationError, 'requirements.empeira must be a RubyGems version requirement or null', cause: nil
16
+ end
17
+
18
+ def self.valid?(value)
19
+ value.is_a?(String) && !value.strip.empty? && value.split(',', -1).none? { |part| part.strip.empty? }
20
+ end
21
+ private_class_method :valid?
22
+ end
23
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module VM
5
+ # Guest package installation uses only the existing allowlisted HTTP/HTTPS proxy.
6
+ class Agent
7
+ PUPPET = Node::Certificates::PUPPET
8
+
9
+ def initialize(context:, guest:, runtime: nil, progress: Progress.new)
10
+ @context = context
11
+ @guest = guest
12
+ @runtime = runtime
13
+ @progress = progress
14
+ end
15
+
16
+ def ensure_installed(record, requirements: nil, proxy_url: nil)
17
+ requirements ||= BootstrapRequirements.new(context: @context, os: record.fetch('os'),
18
+ version: record.fetch('version'),
19
+ architecture: record.fetch('architecture'))
20
+ unless requirements.required?
21
+ verify_existing_agent!(record)
22
+ return
23
+ end
24
+ raise Error, 'Managed agent installation requires the bootstrap proxy' unless proxy_url
25
+
26
+ @proxy_url = proxy_url
27
+ @requirements = requirements
28
+ install_selected_agent(record, requirements)
29
+ ensure
30
+ @proxy_url = nil
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :proxy_url
36
+
37
+ def verify_existing_agent!(record)
38
+ result = @guest.run(record, [PUPPET, '--version'])
39
+ return if result.success?
40
+
41
+ details = Execution::Diagnostics.command(result, operation: 'Existing VM Puppet agent verification',
42
+ tool: 'puppet')
43
+ raise Error, 'No usable agent was found and managed bootstrap is unavailable; ' \
44
+ "check bootstrap.guests metadata\n#{details}", cause: nil
45
+ end
46
+
47
+ def install_selected_agent(record, requirements)
48
+ execute = ->(arguments) { @guest.run(record, arguments, timeout: 300) }
49
+ copy = ->(source, destination, mode) { @guest.copy_to(record, source, destination, mode: mode) }
50
+ @runtime ||= Runtime.registry.build(@context.container_engine, context: @context, runner: Execution::Runner.new)
51
+ installer = Node::AgentInstaller.build(context: @context, runtime: @runtime, requirements: requirements,
52
+ execute: execute, copy: copy, progress: @progress)
53
+ installer.install(proxy_url: proxy_url)
54
+ execute!(record, [PUPPET, '--version'], 'VM Puppet agent verification')
55
+ end
56
+
57
+ def execute!(record, arguments, operation)
58
+ result = @guest.run(record, arguments, timeout: 300)
59
+ return if result.success?
60
+
61
+ details = Execution::Diagnostics.command(result, operation: operation, tool: arguments.first)
62
+ raise Error, "#{operation} failed; VM retained for diagnosis\n#{details}", cause: nil
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Empeira
6
+ module VM
7
+ # Reviewed image metadata determines installation needs before any download.
8
+ class BootstrapRequirements
9
+ attr_reader :repository, :agent, :guest, :target
10
+
11
+ def initialize(context:, os:, version:, agent_required: nil, distribution_required: false,
12
+ architecture: context.platform.architecture)
13
+ config = context.configuration
14
+ @target = ::Empeira::Agent::Target.new(os: os, release: version, architecture: architecture.to_s)
15
+ @os = os
16
+ @architecture = architecture.to_s
17
+ @guest = config.fetch('bootstrap').fetch('guests').fetch(os).fetch(version)
18
+ @agent_required = agent_required.nil? ? !guest.fetch('agent_preinstalled') : agent_required
19
+ @distribution_required = distribution_required || @agent_required
20
+ return unless @agent_required || @distribution_required
21
+
22
+ load_agent(config) if @agent_required
23
+ rescue KeyError
24
+ raise ConfigurationError,
25
+ 'No reviewed agent source exists for this guest'
26
+ end
27
+
28
+ def required?
29
+ @agent_required
30
+ end
31
+
32
+ def destinations
33
+ distribution_destinations = @distribution_required ? guest_destinations : []
34
+ distribution_destinations.uniq
35
+ end
36
+
37
+ def report
38
+ "Managed bootstrap destinations: #{destinations.join(', ')}"
39
+ end
40
+
41
+ def rpm_options
42
+ return [] unless %w[rocky almalinux].include?(@os)
43
+
44
+ architecture = ImageSource::ARCHITECTURES.fetch(@architecture)
45
+ base = guest.fetch('baseurl')
46
+ { 'baseos' => 'BaseOS', 'appstream' => 'AppStream', 'extras' => 'extras' }.flat_map do |id, directory|
47
+ ["--setopt=#{id}.mirrorlist=", "--setopt=#{id}.metalink=",
48
+ "--setopt=#{id}.baseurl=#{base}/#{directory}/#{architecture}/os/"]
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def guest_destinations
55
+ destinations = guest.fetch('destinations')
56
+ return destinations unless @os == 'ubuntu'
57
+
58
+ irrelevant = @architecture == 'arm64' ? %w[archive.ubuntu.com security.ubuntu.com] : ['ports.ubuntu.com']
59
+ destinations - irrelevant
60
+ end
61
+
62
+ def load_agent(config)
63
+ @agent = config.fetch('agent')
64
+ install = @agent.fetch('install')
65
+ @repository = target.source(install)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module VM
5
+ # Reuse the same targeted CA enrollment rules as container nodes.
6
+ class CertificateRuntime
7
+ def initialize(runtime:, guest:, record:)
8
+ @runtime = runtime
9
+ @guest = guest
10
+ @record = record
11
+ end
12
+
13
+ def service_exec(resource, arguments, timeout: 60)
14
+ return @guest.run(@record, arguments, timeout: timeout) if resource['vm']
15
+
16
+ @runtime.service_exec(resource, arguments, timeout: timeout)
17
+ end
18
+
19
+ def copy_from(resource, source, destination)
20
+ @runtime.copy_from(resource, source, destination)
21
+ end
22
+
23
+ def copy_to(resource, source, destination)
24
+ return @guest.copy_to(@record, source, destination) if resource['vm']
25
+
26
+ @runtime.copy_to(resource, source, destination)
27
+ end
28
+ end
29
+ end
30
+ end