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,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module ControlPlane
5
+ class Discovery
6
+ def initialize(plan:, runtime:, state:)
7
+ @plan = plan
8
+ @runtime = runtime
9
+ @state = state
10
+ end
11
+
12
+ def refresh(services: nil, node_resources: nil, reload_proxy: true, force_proxy_reload: false)
13
+ nodes = @state.fetch('nodes', {})
14
+ definitions = node_definitions(nodes)
15
+ expected = nodes.select { |_, record| record['provider'] == 'container' }
16
+ .transform_values { |record| record['id'] }
17
+ resources = inspect(definitions, expected, services, node_resources)
18
+ resources.merge!(lease_resources(nodes))
19
+ @plan.files.hosts(resources, @plan.network)
20
+ refresh_proxy(resources, nodes, reload: reload_proxy, force: force_proxy_reload)
21
+ end
22
+
23
+ private
24
+
25
+ def node_definitions(nodes)
26
+ nodes.select { |_, record| record['provider'] == 'container' }.transform_values do |record|
27
+ Node::Definition.new(hostname: record.fetch('hostname'), workspace: @plan.context.workspace)
28
+ end
29
+ end
30
+
31
+ def refresh_proxy(resources, nodes, reload:, force:)
32
+ changed = @plan.files.proxy_clients(resources, @plan.network, nodes: nodes)
33
+ resource = resources['proxy']
34
+ reload_proxy(resource, changed: changed || force) if reload && resource&.fetch('state') == 'running'
35
+ changed
36
+ end
37
+
38
+ def reload_proxy(resource, changed:)
39
+ checkpoint = { 'id' => resource.fetch('id'), 'fingerprint' => @plan.files.proxy_reload_digest }
40
+ plane = @state.fetch('control_plane')
41
+ return if !changed && plane['proxy_reload'] == checkpoint
42
+
43
+ @runtime.reload_service(resource)
44
+ plane['proxy_reload'] = checkpoint
45
+ end
46
+
47
+ def lease_resources(nodes)
48
+ nodes.filter_map do |name, record|
49
+ address = record.dig('peer', 'ip') || record.dig('definition', 'ip')
50
+ [name, { 'networks' => { @plan.network => { 'IPAddress' => address } } }] if address
51
+ end.to_h
52
+ end
53
+
54
+ def inspect_nodes(definitions, expected)
55
+ definitions.empty? ? {} : @runtime.inspect_services(definitions, expected_ids: expected)
56
+ end
57
+
58
+ def inspect(definitions, expected, services, node_resources)
59
+ if services
60
+ nodes = node_resources || inspect_nodes(definitions, expected)
61
+ return services.merge(nodes.compact)
62
+ end
63
+ @plan.browser_enabled = @state.dig('control_plane', 'services')&.key?('browser')
64
+ expected.merge!(@state.fetch('control_plane').fetch('services').transform_values { |record| record['id'] })
65
+ @runtime.inspect_services(@plan.definitions.merge(definitions), expected_ids: expected).compact
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module ControlPlane
5
+ # Mount destinations exist in state, while control code stays live and read-only.
6
+ class Environment
7
+ CONTROL = '/empeira-control'
8
+
9
+ def initialize(context:, hiera:)
10
+ @project = context.project.path
11
+ @root = root(context)
12
+ @targets = mount_targets(hiera)
13
+ @modules = Modules::Storage.new(context: context).root if @project.join('Puppetfile').file?
14
+ @targets |= ['modules'] if @modules
15
+ @base = context.locations.workspace(context.workspace).join('environments')
16
+ @directory = @base.join(Infrastructure::Definition.fingerprint([@targets, @modules&.to_s]))
17
+ end
18
+
19
+ def mounts
20
+ return [bind(@project, @root)] if @targets.empty?
21
+
22
+ [bind(@project, CONTROL), bind(@directory, @root),
23
+ *(@modules ? [bind(@modules, "#{@root}/modules")] : [])]
24
+ end
25
+
26
+ def prepare
27
+ return if @targets.empty?
28
+ raise Error, 'Managed environment directory must not be a symlink' if @directory.symlink?
29
+
30
+ FileUtils.mkdir_p(@directory, mode: 0o755)
31
+ project_directory('', @directory)
32
+ end
33
+
34
+ def cleanup
35
+ raise Error, 'Managed environment directory must not be a symlink' if @base.symlink?
36
+
37
+ FileUtils.remove_entry_secure(@base) if @base.exist?
38
+ end
39
+
40
+ private
41
+
42
+ def mount_targets(hiera)
43
+ hiera.entries.select { |entry| entry['status'] == 'available' }.map do |entry|
44
+ entry['destination'].delete_prefix("#{@root}/")
45
+ end
46
+ end
47
+
48
+ def root(context)
49
+ "/etc/puppetlabs/code/environments/#{context.configuration.dig('server', 'environment')}"
50
+ end
51
+
52
+ def bind(source, destination)
53
+ "type=bind,src=#{source},dst=#{destination},readonly"
54
+ end
55
+
56
+ def project_directory(relative, destination)
57
+ child_names(relative).each do |name|
58
+ path = relative.empty? ? name : "#{relative}/#{name}"
59
+ create_entry(path, destination.join(name))
60
+ end
61
+ end
62
+
63
+ def child_names(relative)
64
+ source = @project.join(relative)
65
+ children = source.directory? ? source.children.map { |child| child.basename.to_s } : []
66
+ children = [] if relative == 'modules' && @modules
67
+ prefixes = target_prefixes(relative)
68
+ children | prefixes
69
+ end
70
+
71
+ def target_prefixes(relative)
72
+ @targets.filter_map do |target|
73
+ next unless relative.empty? || target.start_with?("#{relative}/")
74
+
75
+ target.delete_prefix(relative.empty? ? '' : "#{relative}/").split('/').first
76
+ end
77
+ end
78
+
79
+ def create_link(relative, destination)
80
+ File.symlink("#{CONTROL}/#{relative}", destination) unless destination.symlink? || destination.exist?
81
+ end
82
+
83
+ def create_entry(relative, destination)
84
+ if @targets.any? { |target| target == relative || target.start_with?("#{relative}/") }
85
+ create_target(relative, destination)
86
+ else
87
+ create_link(relative, destination)
88
+ end
89
+ end
90
+
91
+ def create_target(relative, destination)
92
+ raise Error, 'Managed environment destination is unsafe' if destination.symlink?
93
+
94
+ FileUtils.mkdir_p(destination, mode: 0o755)
95
+ return if @targets.include?(relative) && !(relative == 'modules' && @modules)
96
+
97
+ project_directory(relative, destination)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Empeira
6
+ module ControlPlane
7
+ # Keep the small generated SQL and TLS configuration templates together.
8
+ # rubocop:disable-next Metrics/ClassLength
9
+ class Files
10
+ attr_reader :directory
11
+
12
+ def initialize(context:)
13
+ @context = context
14
+ @directory = context.locations.workspace(context.workspace).join('services')
15
+ end
16
+
17
+ def path(name)
18
+ directory.join(name).to_s
19
+ end
20
+
21
+ def prepare(upstreams: nil, routes: {}, bootstrap: false)
22
+ FileUtils.mkdir_p(directory, mode: 0o755)
23
+ configuration(bootstrap: bootstrap).each do |name, content|
24
+ next if name == 'squid.conf'
25
+
26
+ content = if name == 'Corefile'
27
+ corefile_content(upstreams:, routes:, bootstrap:)
28
+ else
29
+ content.gsub('@HOST_UPSTREAM@', Array(upstreams).join(' '))
30
+ end
31
+ write(name, content)
32
+ end
33
+ prepare_discovery_files
34
+ end
35
+
36
+ def corefile_current?(upstreams:, routes:)
37
+ File.file?(path('Corefile')) && File.binread(path('Corefile')) == corefile_content(upstreams:, routes:)
38
+ end
39
+
40
+ def activate_additional_resolver(upstreams:, routes:)
41
+ write('Corefile', corefile_content(upstreams:, routes:))
42
+ end
43
+
44
+ def configuration(bootstrap: false)
45
+ {
46
+ 'Corefile' => corefile(bootstrap: bootstrap),
47
+ 'squid.conf' => Network::ProxyPolicy.new(@context.configuration.fetch('proxy')).configuration,
48
+ 'jetty.ini' => jetty, 'database.conf' => database, 'database-setup.sql' => database_setup,
49
+ 'puppetdb.conf' => "puppetdb: {}\n",
50
+ 'server-puppetdb.conf' => <<~CONFIG
51
+ [main]
52
+ server_urls = https://puppetdb.empeira.internal:8081
53
+ verify_client_certificate = false
54
+ CONFIG
55
+ }
56
+ end
57
+
58
+ def prepare_discovery_files
59
+ write('proxy-rules.conf', '') unless File.exist?(path('proxy-rules.conf'))
60
+ write('proxy-clients', "127.0.0.1/32\n") unless File.exist?(path('proxy-clients'))
61
+ write('hosts', '') unless File.exist?(path('hosts'))
62
+ end
63
+
64
+ def bind_dns(address)
65
+ IPAddr.new(address)
66
+ write('squid.conf', configuration.fetch('squid.conf').gsub('@EMPEIRA_DNS@', address))
67
+ rescue IPAddr::InvalidAddressError
68
+ raise Error, 'Cannot bind proxy DNS to an invalid CoreDNS address'
69
+ end
70
+
71
+ def cleanup
72
+ FileUtils.rm_f(secret_path)
73
+ end
74
+
75
+ def credentials(existing:)
76
+ return if File.file?(secret_path)
77
+
78
+ if existing
79
+ raise Error,
80
+ 'Database credentials are missing for retained storage; restore the workspace credentials'
81
+ end
82
+
83
+ content = %w[POSTGRES_PASSWORD EMPEIRA_DB_PASSWORD EMPEIRA_READ_PASSWORD].map do |key|
84
+ "#{key}=#{SecureRandom.hex(32)}\n"
85
+ end.join
86
+ File.write(secret_path, content, mode: 'wx', perm: 0o600)
87
+ end
88
+
89
+ def secret_path
90
+ @context.locations.workspace(@context.workspace).join('database.env').to_s
91
+ end
92
+
93
+ # rubocop:disable-next Naming/PredicateMethod -- This atomic write returns whether content changed.
94
+ def write(name, content, mode: 0o644)
95
+ return false if File.file?(path(name)) && File.binread(path(name)) == content
96
+
97
+ Tempfile.create('configuration-', directory) do |file|
98
+ file.chmod(mode)
99
+ file.write(content)
100
+ file.flush
101
+ File.rename(file.path, path(name))
102
+ end
103
+ true
104
+ end
105
+
106
+ def hosts(resources, network)
107
+ naming = Network::Naming.new
108
+ entries = resources.sort.filter_map do |key, resource|
109
+ address = resource.dig('networks', network, 'IPAddress')
110
+ "#{address} #{naming.aliases(key).join(' ')}" if address && !address.empty?
111
+ end
112
+ write('hosts', "#{entries.join("\n")}\n")
113
+ end
114
+
115
+ def proxy_clients(resources, network, nodes: {})
116
+ bindings = Network::ProxyBindings.new(context: @context, resources: resources, network: network, nodes: nodes)
117
+ # The workspace parent is private; Squid's mapped service user must read the bind mount.
118
+ rules_changed = write('proxy-rules.conf', bindings.configuration)
119
+ allowed = proxy_allowed(nodes)
120
+ addresses = resources.slice(*allowed).values.filter_map do |resource|
121
+ resource&.dig('networks', network, 'IPAddress')
122
+ end
123
+ clients_changed = write('proxy-clients', "#{(['127.0.0.1/32'] + addresses.reject(&:empty?).sort).join("\n")}\n")
124
+ rules_changed || clients_changed
125
+ end
126
+
127
+ def proxy_reload_digest
128
+ content = %w[squid.conf proxy-rules.conf proxy-clients].to_h do |name|
129
+ [name, File.binread(path(name))]
130
+ end
131
+ Infrastructure::Definition.fingerprint(content)
132
+ end
133
+
134
+ def proxy_allowed(nodes)
135
+ ['server', *nodes.keys]
136
+ end
137
+
138
+ private
139
+
140
+ def corefile_content(upstreams:, routes:, bootstrap: false)
141
+ corefile(bootstrap: bootstrap).gsub('@HOST_UPSTREAM@', Array(upstreams).join(' ')) +
142
+ resolver_routes(routes, bootstrap: bootstrap) + rewrite_zones(upstreams: upstreams, routes: routes,
143
+ bootstrap: bootstrap)
144
+ end
145
+
146
+ # Reloadable DNS policy is deliberately outside the container definition fingerprint.
147
+ # Exact rewrites use authoritative discovery; unmatched descendants keep normal forwarding.
148
+ # rubocop:disable-next Metrics/MethodLength
149
+ def rewrite_zones(upstreams:, routes:, bootstrap:)
150
+ Configuration::DNSRewrites.entries(@context.configuration).map do |entry|
151
+ source, target = entry.values_at('from', 'to')
152
+ route = routes.keys.select { |zone| source == zone || source.end_with?(".#{zone}") }.max_by(&:size)
153
+ servers = route ? routes.fetch(route) : upstreams
154
+ <<~CONFIG
155
+ #{source}:53 {
156
+ rewrite stop name exact #{source} #{target}
157
+ template IN AAAA . {
158
+ rcode NOERROR
159
+ }
160
+ hosts /empeira/hosts empeira.internal {
161
+ ttl 1
162
+ reload 1s
163
+ }
164
+ #{forwarders(Array(servers).join(' '), bootstrap: bootstrap, authoritative: true)}
165
+ loop
166
+ reload
167
+ }
168
+ CONFIG
169
+ end.join
170
+ end
171
+
172
+ def resolver_routes(routes, bootstrap: false)
173
+ sources = Configuration::DNSRewrites.entries(@context.configuration).map { |entry| entry.fetch('from') }
174
+ routes.except(*sources).map do |zone, addresses|
175
+ forwarding = forwarders(addresses.join(' '), bootstrap: bootstrap)
176
+ "#{zone}:53 {\n template IN AAAA {\n rcode NOERROR\n }\n #{forwarding}\n loop\n reload\n}\n"
177
+ end.join
178
+ end
179
+
180
+ def forwarders(servers, bootstrap: false, authoritative: false)
181
+ additional = @context.configuration.dig('dns', 'additional_resolver') unless bootstrap
182
+ exclusion = authoritative ? " except empeira.internal\n" : ''
183
+ fallback = authoritative ? "forward . #{servers} {\n#{exclusion} }" : "forward . #{servers}"
184
+ return fallback unless additional
185
+
186
+ "forward . #{additional} {\n#{exclusion} next NXDOMAIN\n next_on_nodata\n }\n #{fallback}"
187
+ end
188
+
189
+ # One readable CoreDNS document defines both authoritative and forwarding zones.
190
+ # rubocop:disable-next Metrics/MethodLength
191
+ def corefile(bootstrap: false)
192
+ upstream = @context.configuration.dig('dns', 'upstream')
193
+ servers = upstream['mode'] == 'host' ? '@HOST_UPSTREAM@' : upstream.fetch('servers').join(' ')
194
+ <<~CONFIG
195
+ empeira.internal:53 {
196
+ template IN AAAA {
197
+ rcode NOERROR
198
+ }
199
+ hosts /empeira/hosts {
200
+ ttl 1
201
+ reload 1s
202
+ }
203
+ reload
204
+ }
205
+ .:53 {
206
+ template IN AAAA {
207
+ rcode NOERROR
208
+ }
209
+ hosts /empeira/hosts {
210
+ ttl 1
211
+ reload 1s
212
+ fallthrough
213
+ }
214
+ #{forwarders(servers, bootstrap: bootstrap)}
215
+ loop
216
+ reload
217
+ health :8080
218
+ }
219
+ CONFIG
220
+ end
221
+
222
+ def database_setup
223
+ <<~'SQL'
224
+ \getenv writer_password EMPEIRA_DB_PASSWORD
225
+ \getenv reader_password EMPEIRA_READ_PASSWORD
226
+ SELECT format('CREATE ROLE puppetdb LOGIN PASSWORD %L', :'writer_password')
227
+ WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'puppetdb') \gexec
228
+ SELECT format('CREATE ROLE puppetdb_read LOGIN PASSWORD %L', :'reader_password')
229
+ WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'puppetdb_read') \gexec
230
+ ALTER DATABASE puppetdb OWNER TO puppetdb;
231
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
232
+ GRANT ALL ON SCHEMA public TO puppetdb;
233
+ GRANT USAGE ON SCHEMA public TO puppetdb_read;
234
+ GRANT SELECT ON ALL TABLES IN SCHEMA public TO puppetdb_read;
235
+ ALTER DEFAULT PRIVILEGES FOR ROLE puppetdb IN SCHEMA public GRANT SELECT ON TABLES TO puppetdb_read;
236
+ SQL
237
+ end
238
+
239
+ def jetty
240
+ <<~CONFIG
241
+ [jetty]
242
+ host = 0.0.0.0
243
+ port = 8080
244
+ CONFIG
245
+ end
246
+
247
+ def database
248
+ <<~CONFIG
249
+ database: {
250
+ subname: "//postgres.empeira.internal:5432/puppetdb"
251
+ username: puppetdb
252
+ password: ${EMPEIRA_DB_PASSWORD}
253
+ }
254
+ read-database: {
255
+ subname: "//postgres.empeira.internal:5432/puppetdb"
256
+ username: puppetdb_read
257
+ password: ${EMPEIRA_READ_PASSWORD}
258
+ }
259
+ CONFIG
260
+ end
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module ControlPlane
5
+ class Health
6
+ RUBY = '/opt/puppetlabs/puppet/bin/ruby'
7
+
8
+ def initialize(runtime:, plan:)
9
+ @runtime = runtime
10
+ @plan = plan
11
+ end
12
+
13
+ def wait(key, resources)
14
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @plan.config.dig('server', 'timeout')
15
+ loop do
16
+ return if ready?(key, resources)
17
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
18
+ raise Error,
19
+ "#{key} did not become ready within server.timeout; inspect empeira status and owned service logs\n" \
20
+ "#{@failure}"
21
+ end
22
+
23
+ sleep 2
24
+ end
25
+ end
26
+
27
+ # rubocop:disable-next Metrics/CyclomaticComplexity -- Each service has its own concrete readiness probe.
28
+ def ready?(key, resources)
29
+ resource = resources[key]
30
+ return false unless resources.dig(key, 'state') == 'running'
31
+
32
+ case key
33
+ when 'server' then check(resource, [*tls_curl, 'https://server.empeira.internal:8140/status/v1/simple'],
34
+ 'running')
35
+ when 'postgres' then check(resource, ['psql', '-U', 'postgres', '-d', 'puppetdb', '-Atc', 'SELECT 1'], '1')
36
+ when 'puppetdb-backend'
37
+ check(resources['server'], [*http_curl, 'http://puppetdb-backend.empeira.internal:8080/pdb/query/v4/nodes'])
38
+ when 'puppetdb' then puppetdb_ready?(resources['server'])
39
+ when 'dns' then dns_ready?(resources)
40
+ when 'proxy' then proxy_ready?(resources)
41
+ else false
42
+ end
43
+ end
44
+
45
+ def http_curl
46
+ Server::HTTP.arguments
47
+ end
48
+
49
+ def tls_curl
50
+ Server::HTTP.tls_arguments
51
+ end
52
+
53
+ private
54
+
55
+ def puppetdb_ready?(server)
56
+ path = '/pdb/query/v4/nodes'
57
+ check(server, [*http_curl, "http://puppetdb.empeira.internal:8080#{path}"]) &&
58
+ check(server, [*http_curl, '--cacert', '/etc/puppetlabs/puppet/ssl/certs/ca.pem',
59
+ "https://puppetdb.empeira.internal:8081#{path}"])
60
+ end
61
+
62
+ def check(resource, arguments, expected = nil)
63
+ return false unless resource
64
+ return check_http?(resource, arguments, expected) if http_probe?(arguments)
65
+
66
+ result = @runtime.service_exec(resource, arguments)
67
+ return true if result.success? && (expected.nil? || result.stdout.strip == expected)
68
+
69
+ @failure = Execution::Diagnostics.native(result, operation: 'Probe control-plane readiness',
70
+ tool: arguments.first)
71
+ false
72
+ rescue Empeira::ExecutionError
73
+ false
74
+ end
75
+
76
+ def http_probe?(arguments)
77
+ arguments.first == 'curl' && !arguments.include?('--write-out')
78
+ end
79
+
80
+ def check_http?(resource, arguments, expected)
81
+ result = @runtime.service_exec(resource, [*arguments[0...-1], '--include', arguments.last])
82
+ body = Server::HTTP.response(result)[:body]
83
+ return true if result.success? && (expected.nil? || body.strip == expected)
84
+
85
+ @failure = Server::HTTP.failure(result, url: arguments.last, operation: 'Probe control-plane readiness')
86
+ false
87
+ end
88
+
89
+ def proxy_ready?(resources)
90
+ arguments = %w[curl --silent --max-time 5 --noproxy localhost --proxy
91
+ http://proxy.empeira.internal:3128 --output /dev/null --write-out]
92
+ # Squid's denial must be observable, even when no destination is allowed.
93
+ arguments.push('%{http_code}', 'http://empeira-denied.invalid/') # rubocop:disable Style/FormatStringToken
94
+ check(resources['server'], arguments, '403')
95
+ end
96
+
97
+ def dns_ready?(resources)
98
+ names = resources.keys.map { |key| @plan.naming.hostname(key) }
99
+ code = 'require "resolv"; ARGV.each { |name| Resolv.getaddress(name) }'
100
+ check(resources['server'], [RUBY, '-e', code, *names])
101
+ end
102
+ end
103
+ end
104
+ end