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,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Empeira
6
+ module CLI
7
+ class Addons < Base
8
+ desc 'list', 'List configured addons and local installation status'
9
+ def list
10
+ service = application.addons
11
+ service.names.each do |name|
12
+ say "#{name}: #{local_status(service.installation(name))}"
13
+ end
14
+ end
15
+
16
+ desc 'info NAME', 'Show source, reference, commit, path and manifest'
17
+ def info(name)
18
+ entry = application.addons.installation(name)
19
+ say "Addon: #{name}\nGit: #{entry.source}\nRef: #{entry.ref}"
20
+ local = entry.local
21
+ return say('Status: missing; run empeira addons sync') unless local
22
+
23
+ commit, path, manifest = local
24
+ say "Commit: #{commit}\nPath: #{path}\n#{YAML.dump(manifest.data)}"
25
+ end
26
+
27
+ %w[sync update].each do |operation|
28
+ desc operation, 'Synchronize configured Git addons without starting infrastructure'
29
+ define_method(operation) do
30
+ application.addons.synchronize
31
+ say 'Configured addons synchronized.'
32
+ end
33
+ end
34
+
35
+ no_commands do
36
+ def local_status(entry)
37
+ local = entry.local
38
+ local ? "installed (#{entry.ref}, #{local.first})" : "missing (#{entry.ref})"
39
+ rescue Empeira::Addons::Error => e
40
+ "invalid (#{e.message})"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative 'progress'
5
+
6
+ module Empeira
7
+ module CLI
8
+ class Base < Thor
9
+ check_unknown_options!
10
+ class_option :container_engine, type: :string, enum: Configuration::Schema::CONTAINER_ENGINES,
11
+ desc: 'Override runtime.container_engine'
12
+
13
+ def self.exit_on_failure?
14
+ true
15
+ end
16
+
17
+ # Extract only the explicit guest argv before Thor reparses a nested subcommand.
18
+ def self.start(given_args = ARGV, config = {})
19
+ boundary = given_args.index('--')
20
+ prefix = boundary && given_args.take(boundary)
21
+ if prefix && (prefix.each_cons(2).include?(%w[node exec]) ||
22
+ (name == 'Empeira::CLI::Node' && prefix.first == 'exec'))
23
+ config = config.merge(guest_arguments: given_args.drop(boundary + 1))
24
+ given_args = prefix
25
+ end
26
+ super
27
+ end
28
+
29
+ def initialize(args = [], local_options = {}, config = {})
30
+ @guest_arguments = config[:guest_arguments]
31
+ @addon_arguments = config[:addon_arguments]
32
+ super
33
+ end
34
+
35
+ # Thor groups presentation helpers outside its command DSL.
36
+ # rubocop:disable-next Metrics/BlockLength
37
+ no_commands do
38
+ def application(node: false, progress: Empeira::Progress.new, recovery: false)
39
+ overrides = runtime_overrides
40
+ overrides = Configuration::Merge.call(overrides, node_overrides) if node
41
+ Application.new(project_path: Dir.pwd, overrides: overrides, progress: progress, recovery: recovery,
42
+ recovery_path: recovery ? options[:project_path] : nil)
43
+ end
44
+
45
+ def progressing(message, node: false, recovery: false)
46
+ renderer = ProgressRenderer.new
47
+ progress = Empeira::Progress.new(listener: renderer)
48
+ renderer.during do
49
+ progress.run(message) { yield application(node: node, progress: progress, recovery: recovery) }
50
+ end
51
+ end
52
+
53
+ def runtime_overrides
54
+ return {} unless options[:container_engine]
55
+
56
+ { 'runtime' => { 'container_engine' => options[:container_engine] } }
57
+ end
58
+
59
+ def node_overrides
60
+ values = options.slice('os', 'version', 'memory', 'cpus').to_h
61
+ %w[memory cpus].each { |key| values[key] = parse_integer(key, values[key]) if values.key?(key) }
62
+ { 'node_defaults' => values }
63
+ end
64
+
65
+ def parse_integer(key, value)
66
+ Integer(value, 10)
67
+ rescue ArgumentError
68
+ raise ConfigurationError, "node_defaults.#{key} must be a positive integer"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../completion/catalog'
4
+ require_relative '../completion/bash'
5
+
6
+ module Empeira
7
+ module CLI
8
+ class Completion < Base
9
+ desc 'bash', 'Print Bash completion for sourcing in your shell'
10
+ def bash
11
+ say Empeira::Completion::Bash.generate
12
+ end
13
+
14
+ desc 'candidates INDEX', 'Return completion candidates', hide: true
15
+ option :null, type: :boolean, default: false, hide: true
16
+ def candidates(index)
17
+ words = $stdin.read.split("\0", -1)
18
+ words.pop
19
+ cursor = Integer(index, 10) - 1
20
+ return if cursor.negative?
21
+
22
+ emit_candidates(words, cursor)
23
+ rescue ArgumentError
24
+ raise Error, 'Completion index must be an integer'
25
+ end
26
+
27
+ no_commands do
28
+ def emit_candidates(words, cursor)
29
+ catalog = Empeira::Completion::Catalog.new
30
+ values = catalog.candidates(words, cursor)
31
+ addon = catalog.addon_completion
32
+ if addon&.file_completion
33
+ values += ["\x1f#{addon.file_completion}", Empeira::Completion::Bash.path_prefix(addon.file_prefix),
34
+ addon.replacement.to_s]
35
+ end
36
+ separator = options[:null] ? "\0" : "\n"
37
+ values.each { |value| $stdout.write("#{value}#{separator}") }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module CLI
5
+ class Config < Base
6
+ desc 'show', 'Display the effective configuration'
7
+ def show
8
+ app = application
9
+ data = Configuration::Display.redact(app.context.configuration)
10
+ mounts = ControlPlane::Plan.new(context: app.context).hiera.entries
11
+ data['effective_hiera_mounts'] = mounts unless mounts.empty?
12
+ say YAML.dump(data)
13
+ end
14
+
15
+ desc 'validate', 'Validate the effective configuration'
16
+ def validate
17
+ application.context.configuration
18
+ say 'Configuration is valid.'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'config'
5
+ require_relative 'node'
6
+ require_relative 'proxy'
7
+ require_relative 'completion'
8
+ require_relative 'update'
9
+ require_relative 'addons'
10
+
11
+ module Empeira
12
+ module CLI
13
+ class Main < Base
14
+ # Strip the opaque addon argv before Thor can interpret --help, --version,
15
+ # assignments, or a literal -- as Core options.
16
+ def self.start(given_args = ARGV, config = {})
17
+ index = addon_index(given_args)
18
+ if index && given_args[index + 1]&.match?(Configuration::Addons::NAME)
19
+ config = config.merge(addon_arguments: given_args.drop(index + 2))
20
+ given_args = [*given_args[index, 2], *given_args.take(index)]
21
+ end
22
+ super
23
+ end
24
+
25
+ def self.addon_index(arguments)
26
+ index = 0
27
+ index += arguments[index].include?('=') ? 1 : 2 while arguments[index]&.start_with?('--container-engine')
28
+ index if arguments[index] == 'addon'
29
+ end
30
+
31
+ desc 'addon NAME [ARGUMENTS...]', 'Run a configured standalone addon'
32
+ def addon(name)
33
+ application.addons.execute(name, @addon_arguments || [])
34
+ end
35
+
36
+ desc 'addons SUBCOMMAND', 'Manage configured Git addons'
37
+ subcommand 'addons', Addons
38
+
39
+ desc 'version', 'Show build version'
40
+ option :verbose, type: :boolean, desc: 'Include revision and build time'
41
+ map '--version' => :version
42
+ def version
43
+ info = BuildInfo.load
44
+ say(options[:verbose] ? JSON.pretty_generate(info.to_h) : "Empeira #{info.version}")
45
+ end
46
+
47
+ desc 'config SUBCOMMAND', 'Show or validate project configuration'
48
+ subcommand 'config', Config
49
+ desc 'proxy SUBCOMMAND', 'Explain hostname-specific proxy policy'
50
+ subcommand 'proxy', Proxy
51
+ desc 'node SUBCOMMAND', 'Container and VM test-node lifecycle'
52
+ subcommand 'node', Node
53
+ desc 'completion SUBCOMMAND', 'Shell completion'
54
+ subcommand 'completion', Completion
55
+
56
+ desc 'status', 'Inspect workspace infrastructure without changing state'
57
+ def status
58
+ application.infrastructure.status.each { |key, value| say "#{key}: #{value}" }
59
+ end
60
+
61
+ desc 'up', 'Ensure isolated workspace infrastructure'
62
+ def up
63
+ addon_error = nil
64
+ result = progressing('Preparing workspace...') do |app|
65
+ ready = app.infrastructure.up
66
+ begin
67
+ app.addons.synchronize(missing_only: true)
68
+ rescue Empeira::Addons::Error => e
69
+ addon_error = e
70
+ end
71
+ ready
72
+ end
73
+ say(result.changed ? 'Workspace control plane is ready.' : 'Workspace control plane is already ready.')
74
+ raise addon_error if addon_error
75
+ end
76
+
77
+ desc 'browser', 'Start the disposable internal browser and print its loopback HTTPS URL'
78
+ def browser
79
+ url = progressing('Preparing internal browser...') { |app| app.infrastructure.browser }
80
+ say url
81
+ end
82
+
83
+ desc 'down', 'Remove owned workspace infrastructure'
84
+ def down
85
+ result = progressing('Stopping workspace...') { |app| app.infrastructure.down }
86
+ action = result.changed ? 'removed' : 'already absent'
87
+ say "Workspace services #{action}; persistent data retained."
88
+ end
89
+
90
+ desc 'destroy', 'Permanently remove services, CA and database volumes'
91
+ option :project_path, type: :string,
92
+ desc: 'Former absolute Git root path for recovery without a control repository'
93
+ def destroy
94
+ progressing('Removing workspace...', recovery: true) { |app| app.infrastructure.destroy }
95
+ say 'Workspace infrastructure and persistent data removed.'
96
+ end
97
+
98
+ desc 'self-update', 'Update only Empeira (not implemented yet)'
99
+ def self_update
100
+ Updates::Service.new.self_update
101
+ end
102
+
103
+ desc 'update SUBCOMMAND', 'Update managed targets (not implemented yet)'
104
+ subcommand 'update', Update
105
+
106
+ %w[images].each do |operation|
107
+ desc "#{operation} [SUBCOMMAND]", 'Not implemented yet'
108
+ define_method(operation) do |*_arguments|
109
+ raise UnavailableFeature, 'This command is not implemented yet.'
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module CLI
5
+ class Node < Base
6
+ map 'run' => :run_node, 'shell' => :shell_node, 'exec' => :exec_node
7
+
8
+ desc 'exec HOSTNAME -- COMMAND [ARGUMENTS...]', 'Run a root guest command without SSH or implicit shell parsing'
9
+ option :timeout, type: :numeric, desc: 'Guest command timeout in seconds (default: unlimited)'
10
+ def exec_node(hostname, *_arguments)
11
+ arguments = @guest_arguments || []
12
+ Empeira::Node::DiagnosticCommand.validate!(arguments, options[:timeout])
13
+ result = application.nodes.exec(name: hostname, arguments: arguments, timeout: options[:timeout])
14
+ if result.timed_out
15
+ warn 'Guest command timed out; its process groups were stopped.'
16
+ raise SystemExit, 124
17
+ end
18
+ raise SystemExit, result.exit_status
19
+ end
20
+
21
+ desc 'run HOSTNAME', 'Create a container or VM node and run its agent'
22
+ option :provider, type: :string, default: 'container', enum: Empeira::Node.registry.names,
23
+ desc: 'Node provider (default: container)'
24
+ option :os, type: :string, desc: 'Operating system'
25
+ option :version, type: :string, desc: 'Operating system version'
26
+ option :memory, type: :string, desc: 'Memory in MiB (positive integer)'
27
+ option :cpus, type: :string, desc: 'CPU count (positive integer)'
28
+ def run_node(hostname)
29
+ progressing('Preparing node...', node: true) do |app|
30
+ app.run_node(hostname: hostname, provider: options[:provider])
31
+ end
32
+ end
33
+
34
+ desc 'ssh HOSTNAME', 'Open system SSH (VM: managed identity; container: local user authentication)'
35
+ option :user, type: :string, desc: 'Remote login user (overrides personal SSH preferences)'
36
+ option :identity, type: :string, desc: 'SSH identity file (VM default: managed key)'
37
+ option :port, type: :string, desc: 'System SSH guest port (default: 22)'
38
+ def ssh(hostname)
39
+ result = application.nodes.ssh(name: hostname, user: options[:user], identity: options[:identity],
40
+ port: ssh_port)
41
+ raise SystemExit, result.exit_status unless result.success?
42
+ end
43
+
44
+ no_commands do
45
+ def ssh_port
46
+ return unless options[:port]
47
+
48
+ Configuration::SSHPreferences.port!(Integer(options[:port], 10))
49
+ rescue ArgumentError
50
+ raise ConfigurationError, 'SSH port must be an integer from 1 to 65535'
51
+ end
52
+ end
53
+
54
+ %w[start stop destroy shell console puppet logs].each do |operation|
55
+ descriptions = { 'shell' => 'Open an interactive root terminal (runtime exec or VM VirtIO PTY)',
56
+ 'console' => 'Attach to the private VM serial console; Ctrl+] detaches',
57
+ 'destroy' => 'Remove an owned node without version or guest-access requirements' }
58
+ desc "#{operation} HOSTNAME", descriptions.fetch(operation, "#{operation.capitalize} an existing node")
59
+ if operation == 'destroy'
60
+ option :project_path, type: :string,
61
+ desc: 'Former absolute Git root path for recovery without a control repository'
62
+ end
63
+ define_method(operation == 'shell' ? 'shell_node' : operation) do |hostname|
64
+ if %w[shell console ssh logs].include?(operation)
65
+ result = application.nodes.public_send(operation, name: hostname)
66
+ raise SystemExit, result.exit_status if operation == 'ssh' && !result.success?
67
+ raise Error, "Node #{operation} failed" unless result.success?
68
+ else
69
+ progressing("#{operation.capitalize} node...", recovery: operation == 'destroy') do |app|
70
+ app.nodes.public_send(operation, name: hostname)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ desc 'list', 'List workspace nodes and observed runtime state'
77
+ def list
78
+ rows = application.nodes.list
79
+ say 'HOSTNAME PROVIDER RUNTIME OS/VERSION STATE PUPPET_EXIT NETWORK'
80
+ rows.each do |row|
81
+ say [row['hostname'], row['provider'], row['runtime'], "#{row['os']}/#{row['version']}",
82
+ row['state'], row['last_puppet_exit'] || '-', row['network'] || '-'].join(' ')
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ module Empeira
6
+ module CLI
7
+ # The cursor stays on the second row while a workflow is active.
8
+ class ProgressDisplay
9
+ DEFAULT_COLUMNS = 80
10
+ MIN_BAR_COLUMNS = 4
11
+
12
+ def initialize(io:, columns: nil)
13
+ @io = io
14
+ @columns = columns
15
+ @drawn = false
16
+ end
17
+
18
+ def render(event, waiting: false)
19
+ return finish unless event.state == :active
20
+
21
+ available = [terminal_columns - 1, 0].max
22
+ message = ProgressRenderer.clean_message(event.message)
23
+ message += waiting == true ? ' (still working)' : " (still working #{waiting})" if waiting
24
+ @io.print("\r\e[1A") if @drawn
25
+ @io.print("\r\e[2K", progress_line(event.percent, available), "\n\r\e[2K", truncate(message, available))
26
+ @drawn = true
27
+ @io.flush
28
+ end
29
+
30
+ def finish
31
+ return unless @drawn
32
+
33
+ @io.print("\r\e[2K\e[1A\r\e[2K")
34
+ @io.flush
35
+ @drawn = false
36
+ end
37
+
38
+ private
39
+
40
+ def progress_line(percent, available)
41
+ label = format('%3d%%', percent)
42
+ bar_width = available - label.length - 3
43
+ if bar_width < MIN_BAR_COLUMNS
44
+ compact = "[#{percent}%]"
45
+ compact = "#{percent}%" if compact.length > available
46
+ return compact.length <= available ? compact : ''
47
+ end
48
+
49
+ filled = (bar_width * percent / 100.0).round
50
+ "[#{'█' * filled}#{'-' * (bar_width - filled)}] #{label}"
51
+ end
52
+
53
+ def truncate(message, available)
54
+ return '' if available.zero?
55
+ return message if message.bytesize <= available
56
+
57
+ suffix = available >= 4 ? '...' : ''
58
+ budget = available - suffix.length
59
+ result = +''
60
+ message.each_grapheme_cluster do |cluster|
61
+ break if result.bytesize + cluster.bytesize > budget
62
+
63
+ result << cluster
64
+ end
65
+ result << suffix
66
+ end
67
+
68
+ def terminal_columns
69
+ value = @columns ? @columns.call : @io.winsize.last
70
+ value.is_a?(Integer) && value.positive? ? value : DEFAULT_COLUMNS
71
+ rescue IOError, SystemCallError, NoMethodError
72
+ DEFAULT_COLUMNS
73
+ end
74
+ end
75
+
76
+ class ProgressRenderer
77
+ SPINNER = ['|', '/', '-', '\\'].freeze
78
+ SPINNER_INTERVAL = 0.25
79
+
80
+ def self.clean_message(message)
81
+ message.to_s.scrub.gsub(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/, ' ')
82
+ end
83
+
84
+ def initialize(io: $stderr, interactive: nil, columns: nil, heartbeat_interval: 10, clock: nil)
85
+ @io = io
86
+ active = interactive.nil? ? interactive_output? : interactive
87
+ @display = ProgressDisplay.new(io: io, columns: columns) if active
88
+ @heartbeat_interval = heartbeat_interval
89
+ @clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
90
+ @spinner = 0
91
+ @mutex = Mutex.new
92
+ @condition = ConditionVariable.new
93
+ end
94
+
95
+ def call(event)
96
+ @mutex.synchronize do
97
+ @io.print("\n") if @event&.state == :suspended && event.state == :failed
98
+ @event = event
99
+ @spinner = 0
100
+ @next_tick = @clock.call + @heartbeat_interval
101
+ render_state(event)
102
+ @condition.signal
103
+ end
104
+ end
105
+
106
+ def during
107
+ @finished = false
108
+ heartbeat = Thread.new { heartbeat_loop }
109
+ yield
110
+ ensure
111
+ if heartbeat
112
+ @mutex.synchronize do
113
+ @finished = true
114
+ @condition.signal
115
+ end
116
+ heartbeat.join
117
+ end
118
+ @mutex.synchronize { @display&.finish }
119
+ end
120
+
121
+ private
122
+
123
+ def render_state(event)
124
+ if event.state == :warning
125
+ @display&.finish
126
+ @io.puts(self.class.clean_message(event.message))
127
+ @io.flush
128
+ elsif %i[suspended resumed].include?(event.state)
129
+ @display&.finish
130
+ @io.print("\n") if event.state == :resumed
131
+ else
132
+ render(event)
133
+ end
134
+ end
135
+
136
+ def interactive_output?
137
+ Execution::Terminal.interactive?(error: @io)
138
+ end
139
+
140
+ def heartbeat_loop
141
+ @mutex.synchronize do
142
+ until @finished
143
+ tick
144
+ delay = @event&.state == :active ? [@next_tick - @clock.call, 0].max : nil
145
+ @condition.wait(@mutex, delay) unless @finished
146
+ end
147
+ end
148
+ end
149
+
150
+ def tick
151
+ return unless @event&.state == :active && @clock.call >= @next_tick
152
+
153
+ render(@event, waiting: @display ? SPINNER[@spinner % SPINNER.size] : true)
154
+ @spinner += 1
155
+ @next_tick = @clock.call + (@display ? SPINNER_INTERVAL : @heartbeat_interval)
156
+ end
157
+
158
+ def render(event, waiting: false)
159
+ return @display.render(event, waiting: waiting) if @display
160
+
161
+ message = self.class.clean_message(event.message)
162
+ text = format('[%<percent>3d%%] %<message>s%<wait>s', percent: event.percent, message: message,
163
+ wait: waiting ? ' (still working)' : '')
164
+ @io.puts(text)
165
+ @io.flush
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Empeira
4
+ module CLI
5
+ class Proxy < Base
6
+ desc 'show HOSTNAME', 'Explain the normal proxy destination policy for a hostname'
7
+ def show(hostname)
8
+ config = application.context.configuration.fetch('proxy')
9
+ resolver = Network::HostPolicy.new(config)
10
+ say "Effective proxy policy for #{hostname.downcase}"
11
+ say access_message(config)
12
+ display_list('Global', config.fetch('global'))
13
+ display_matches(resolver.matching(hostname))
14
+ display_list('Allowed', resolver.resolve(hostname))
15
+ say 'Managed bootstrap destinations are separate from this normal policy.'
16
+ end
17
+ no_commands do
18
+ def access_message(config)
19
+ return "All workspace nodes may use this policy after 'empeira up' reconciles the proxy." if
20
+ config.fetch('enabled')
21
+
22
+ 'The normal workspace proxy is disabled.'
23
+ end
24
+
25
+ def display_matches(rules)
26
+ display_list('Matched rules', rules.map { |rule| rule.fetch('hosts').join(', ') })
27
+ end
28
+
29
+ def display_list(title, entries)
30
+ say "#{title}:"
31
+ entries.each { |entry| say " #{entry}" }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Empeira
6
+ module CLI
7
+ class Update < Base
8
+ Updates::Service::TARGETS.each do |target|
9
+ desc target,
10
+ { 'all' => 'Update all managed targets, including Empeira (unavailable)',
11
+ 'modules' => 'Synchronize Puppetfile module artifacts',
12
+ 'images' => 'Refresh configured container image artifacts' }.fetch(target)
13
+ define_method(target) do
14
+ if target == 'all'
15
+ application.updates.update(target)
16
+ else
17
+ progressing("Updating #{target}...") { |app| app.updates.update(target) }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end