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.
- checksums.yaml +7 -0
- data/LICENSE +661 -0
- data/README.md +150 -0
- data/bin/empeira +18 -0
- data/config/defaults.yaml +229 -0
- data/docs/addons.md +331 -0
- data/docs/architecture.md +277 -0
- data/docs/configuration.md +1204 -0
- data/docs/development.md +472 -0
- data/docs/hiera-and-eyaml.md +361 -0
- data/docs/installation.md +280 -0
- data/docs/networking.md +362 -0
- data/docs/nodes.md +642 -0
- data/docs/proxy.md +136 -0
- data/docs/troubleshooting.md +361 -0
- data/lib/empeira/addons/environment.rb +82 -0
- data/lib/empeira/addons/error.rb +14 -0
- data/lib/empeira/addons/installation.rb +119 -0
- data/lib/empeira/addons/manifest.rb +112 -0
- data/lib/empeira/addons/service.rb +50 -0
- data/lib/empeira/addons/storage.rb +94 -0
- data/lib/empeira/agent/acquisition.rb +120 -0
- data/lib/empeira/agent/artifact.rb +41 -0
- data/lib/empeira/agent/authentication.rb +122 -0
- data/lib/empeira/agent/cache.rb +122 -0
- data/lib/empeira/agent/cache_storage.rb +82 -0
- data/lib/empeira/agent/download.rb +84 -0
- data/lib/empeira/agent/native_commands.rb +84 -0
- data/lib/empeira/agent/release_sources.rb +64 -0
- data/lib/empeira/agent/target.rb +66 -0
- data/lib/empeira/agent/versions.rb +29 -0
- data/lib/empeira/application.rb +86 -0
- data/lib/empeira/build.json +5 -0
- data/lib/empeira/build_info.rb +46 -0
- data/lib/empeira/cli/addons.rb +45 -0
- data/lib/empeira/cli/base.rb +73 -0
- data/lib/empeira/cli/completion.rb +42 -0
- data/lib/empeira/cli/config.rb +22 -0
- data/lib/empeira/cli/main.rb +114 -0
- data/lib/empeira/cli/node.rb +87 -0
- data/lib/empeira/cli/progress.rb +169 -0
- data/lib/empeira/cli/proxy.rb +36 -0
- data/lib/empeira/cli/update.rb +23 -0
- data/lib/empeira/completion/addons.rb +93 -0
- data/lib/empeira/completion/bash.rb +62 -0
- data/lib/empeira/completion/catalog.rb +170 -0
- data/lib/empeira/configuration/additional_services.rb +74 -0
- data/lib/empeira/configuration/addons.rb +85 -0
- data/lib/empeira/configuration/agent_schema.rb +163 -0
- data/lib/empeira/configuration/agent_values.rb +54 -0
- data/lib/empeira/configuration/bootstrap_guests.rb +42 -0
- data/lib/empeira/configuration/command_mocks.rb +65 -0
- data/lib/empeira/configuration/display.rb +20 -0
- data/lib/empeira/configuration/dns_rewrites.rb +58 -0
- data/lib/empeira/configuration/document.rb +67 -0
- data/lib/empeira/configuration/eyaml.rb +46 -0
- data/lib/empeira/configuration/hiera_modulepath.rb +40 -0
- data/lib/empeira/configuration/hiera_mount_schema.rb +62 -0
- data/lib/empeira/configuration/hiera_mounts.rb +83 -0
- data/lib/empeira/configuration/image_schema.rb +68 -0
- data/lib/empeira/configuration/loader.rb +70 -0
- data/lib/empeira/configuration/merge.rb +48 -0
- data/lib/empeira/configuration/network_redirects.rb +73 -0
- data/lib/empeira/configuration/node_images.rb +64 -0
- data/lib/empeira/configuration/schema.rb +215 -0
- data/lib/empeira/configuration/server_mounts.rb +106 -0
- data/lib/empeira/configuration/service_configuration.rb +40 -0
- data/lib/empeira/configuration/ssh_preferences.rb +83 -0
- data/lib/empeira/configuration/user.rb +46 -0
- data/lib/empeira/configuration/vm_interfaces.rb +147 -0
- data/lib/empeira/control_plane/additional_configurations.rb +106 -0
- data/lib/empeira/control_plane/browser.rb +73 -0
- data/lib/empeira/control_plane/controller.rb +763 -0
- data/lib/empeira/control_plane/discovery.rb +69 -0
- data/lib/empeira/control_plane/environment.rb +101 -0
- data/lib/empeira/control_plane/files.rb +263 -0
- data/lib/empeira/control_plane/health.rb +104 -0
- data/lib/empeira/control_plane/plan.rb +356 -0
- data/lib/empeira/control_plane/storage.rb +48 -0
- data/lib/empeira/errors.rb +9 -0
- data/lib/empeira/execution/console.rb +45 -0
- data/lib/empeira/execution/diagnostics.rb +210 -0
- data/lib/empeira/execution/launch_error.rb +14 -0
- data/lib/empeira/execution/result.rb +11 -0
- data/lib/empeira/execution/runner.rb +235 -0
- data/lib/empeira/execution/terminal.rb +35 -0
- data/lib/empeira/hostname_pattern.rb +14 -0
- data/lib/empeira/images/configuration.rb +44 -0
- data/lib/empeira/images/identity.rb +30 -0
- data/lib/empeira/images/recipe.rb +22 -0
- data/lib/empeira/images/reference.rb +43 -0
- data/lib/empeira/images/source.rb +19 -0
- data/lib/empeira/immutable.rb +19 -0
- data/lib/empeira/infrastructure/cleanup.rb +328 -0
- data/lib/empeira/infrastructure/definition.rb +59 -0
- data/lib/empeira/infrastructure/owned_files.rb +63 -0
- data/lib/empeira/infrastructure/recovery_inventory.rb +53 -0
- data/lib/empeira/infrastructure/service.rb +237 -0
- data/lib/empeira/infrastructure/status.rb +73 -0
- data/lib/empeira/infrastructure/store.rb +274 -0
- data/lib/empeira/modules/events.rb +67 -0
- data/lib/empeira/modules/git_safety.rb +60 -0
- data/lib/empeira/modules/host_git.rb +91 -0
- data/lib/empeira/modules/image.rb +27 -0
- data/lib/empeira/modules/installer.rb +67 -0
- data/lib/empeira/modules/request.rb +68 -0
- data/lib/empeira/modules/state.rb +87 -0
- data/lib/empeira/modules/storage.rb +35 -0
- data/lib/empeira/network/bootstrap_proxy.rb +157 -0
- data/lib/empeira/network/definition.rb +61 -0
- data/lib/empeira/network/direct_egress.rb +133 -0
- data/lib/empeira/network/egress.rb +27 -0
- data/lib/empeira/network/gateway.rb +79 -0
- data/lib/empeira/network/host_policy.rb +53 -0
- data/lib/empeira/network/naming.rb +28 -0
- data/lib/empeira/network/peer/adapter_image.rb +32 -0
- data/lib/empeira/network/peer/allocation.rb +45 -0
- data/lib/empeira/network/peer/attachment.rb +27 -0
- data/lib/empeira/network/peer/backend.rb +108 -0
- data/lib/empeira/network/peer/channel.rb +133 -0
- data/lib/empeira/network/peer/channel_server.rb +71 -0
- data/lib/empeira/network/peer/docker_adapter.rb +165 -0
- data/lib/empeira/network/peer/helper_security.rb +40 -0
- data/lib/empeira/network/peer/layout.rb +55 -0
- data/lib/empeira/network/peer/linux_podman.rb +114 -0
- data/lib/empeira/network/peer/podman_machine.rb +112 -0
- data/lib/empeira/network/policy.rb +39 -0
- data/lib/empeira/network/proxy_bindings.rb +52 -0
- data/lib/empeira/network/proxy_policy.rb +64 -0
- data/lib/empeira/network/redirects.rb +89 -0
- data/lib/empeira/network/resource.rb +50 -0
- data/lib/empeira/node/agent_installer.rb +31 -0
- data/lib/empeira/node/agent_package.rb +125 -0
- data/lib/empeira/node/agent_repository.rb +160 -0
- data/lib/empeira/node/apt_configuration.rb +85 -0
- data/lib/empeira/node/bootstrap.rb +17 -0
- data/lib/empeira/node/certificates.rb +98 -0
- data/lib/empeira/node/command_mock.rb +38 -0
- data/lib/empeira/node/command_mocks.rb +114 -0
- data/lib/empeira/node/container.rb +134 -0
- data/lib/empeira/node/container_agent_bootstrap.rb +43 -0
- data/lib/empeira/node/container_bootstrap.rb +52 -0
- data/lib/empeira/node/container_guest.rb +42 -0
- data/lib/empeira/node/container_lifecycle.rb +145 -0
- data/lib/empeira/node/container_preparation.rb +144 -0
- data/lib/empeira/node/container_state.rb +129 -0
- data/lib/empeira/node/definition.rb +20 -0
- data/lib/empeira/node/diagnostic_command.rb +19 -0
- data/lib/empeira/node/dnf_agent_repository.rb +202 -0
- data/lib/empeira/node/external_fact.rb +21 -0
- data/lib/empeira/node/image.rb +38 -0
- data/lib/empeira/node/interactive_tools.rb +33 -0
- data/lib/empeira/node/interface.rb +18 -0
- data/lib/empeira/node/inventory.rb +143 -0
- data/lib/empeira/node/package_bootstrap.rb +129 -0
- data/lib/empeira/node/package_proxy.rb +102 -0
- data/lib/empeira/node/package_sources.rb +19 -0
- data/lib/empeira/node/puppet_command.rb +12 -0
- data/lib/empeira/node/rpm_configuration.rb +62 -0
- data/lib/empeira/node/run_request.rb +52 -0
- data/lib/empeira/node/runtime_proxy.rb +86 -0
- data/lib/empeira/node/service.rb +72 -0
- data/lib/empeira/node/ssh_client.rb +25 -0
- data/lib/empeira/node/ssh_credentials.rb +78 -0
- data/lib/empeira/node/ssh_endpoint.rb +58 -0
- data/lib/empeira/node/ssh_identity.rb +37 -0
- data/lib/empeira/node/user_ssh.rb +60 -0
- data/lib/empeira/node/vm.rb +89 -0
- data/lib/empeira/node/vm_bootstrap.rb +53 -0
- data/lib/empeira/node/vm_lifecycle.rb +212 -0
- data/lib/empeira/node/vm_preflight.rb +37 -0
- data/lib/empeira/node/vm_preparation.rb +161 -0
- data/lib/empeira/node/vm_state.rb +161 -0
- data/lib/empeira/platform/facts.rb +66 -0
- data/lib/empeira/platform/kvm.rb +91 -0
- data/lib/empeira/platform/locations.rb +51 -0
- data/lib/empeira/platform/resolvers.rb +85 -0
- data/lib/empeira/platform/routes.rb +50 -0
- data/lib/empeira/progress.rb +70 -0
- data/lib/empeira/project_root.rb +47 -0
- data/lib/empeira/providers/lifecycle.rb +96 -0
- data/lib/empeira/providers/registry.rb +21 -0
- data/lib/empeira/providers/resource.rb +32 -0
- data/lib/empeira/providers.rb +55 -0
- data/lib/empeira/runtime/agent_helper.rb +34 -0
- data/lib/empeira/runtime/container.rb +184 -0
- data/lib/empeira/runtime/diagnostic_execution.rb +52 -0
- data/lib/empeira/runtime/docker.rb +87 -0
- data/lib/empeira/runtime/image_metadata.rb +83 -0
- data/lib/empeira/runtime/interface.rb +8 -0
- data/lib/empeira/runtime/mount_probe.rb +57 -0
- data/lib/empeira/runtime/podman.rb +122 -0
- data/lib/empeira/runtime/recipe_build.rb +103 -0
- data/lib/empeira/runtime/registry_diagnostics.rb +23 -0
- data/lib/empeira/runtime/service_arguments.rb +55 -0
- data/lib/empeira/runtime/service_images.rb +118 -0
- data/lib/empeira/runtime/service_inspection.rb +159 -0
- data/lib/empeira/runtime/services.rb +120 -0
- data/lib/empeira/runtime/update_helper.rb +97 -0
- data/lib/empeira/runtime/workspace_network.rb +69 -0
- data/lib/empeira/server/environment_configuration.rb +53 -0
- data/lib/empeira/server/http.rb +38 -0
- data/lib/empeira/server/puppetdb_runtime.rb +74 -0
- data/lib/empeira/server/relay_certificate.rb +76 -0
- data/lib/empeira/server/runtime.rb +90 -0
- data/lib/empeira/services/definition.rb +42 -0
- data/lib/empeira/updates/images.rb +104 -0
- data/lib/empeira/updates/modules.rb +45 -0
- data/lib/empeira/updates/service.rb +45 -0
- data/lib/empeira/version_requirement.rb +23 -0
- data/lib/empeira/vm/agent.rb +66 -0
- data/lib/empeira/vm/bootstrap_requirements.rb +69 -0
- data/lib/empeira/vm/certificate_runtime.rb +30 -0
- data/lib/empeira/vm/cloud_init.rb +171 -0
- data/lib/empeira/vm/disk.rb +136 -0
- data/lib/empeira/vm/guest.rb +123 -0
- data/lib/empeira/vm/guest_channel.rb +110 -0
- data/lib/empeira/vm/guest_network.rb +94 -0
- data/lib/empeira/vm/guest_output.rb +52 -0
- data/lib/empeira/vm/guest_shell.rb +79 -0
- data/lib/empeira/vm/guest_upload.rb +43 -0
- data/lib/empeira/vm/image_cache.rb +77 -0
- data/lib/empeira/vm/image_source.rb +91 -0
- data/lib/empeira/vm/interface.rb +8 -0
- data/lib/empeira/vm/interface_ownership.rb +124 -0
- data/lib/empeira/vm/interface_routes.rb +101 -0
- data/lib/empeira/vm/interfaces.rb +240 -0
- data/lib/empeira/vm/management.rb +73 -0
- data/lib/empeira/vm/prerequisites.rb +42 -0
- data/lib/empeira/vm/qemu.rb +83 -0
- data/lib/empeira/vm/qemu_runtime.rb +404 -0
- data/lib/empeira/vm/removal.rb +30 -0
- data/lib/empeira/vm/root_disk.rb +92 -0
- data/lib/empeira/vm/session_lock.rb +56 -0
- data/lib/empeira/vm/system_ssh.rb +29 -0
- data/lib/empeira/workspace.rb +42 -0
- data/lib/empeira.rb +209 -0
- data/resources/browser/relay.rb +22 -0
- data/resources/gateway/Containerfile +8 -0
- data/resources/gateway/bridge.rb +66 -0
- data/resources/gateway/configure.rb +323 -0
- data/resources/modules/Containerfile +15 -0
- data/resources/modules/Gemfile +5 -0
- data/resources/modules/Gemfile.lock +105 -0
- data/resources/modules/worker.rb +161 -0
- data/resources/network/adapter/Containerfile +13 -0
- data/resources/network/adapter/bridge_linux.go +51 -0
- data/resources/network/adapter/framing.go +45 -0
- data/resources/network/adapter/framing_test.go +53 -0
- data/resources/network/adapter/go.mod +3 -0
- data/resources/network/adapter/main_linux.go +273 -0
- data/resources/network/channel.rb +18 -0
- data/resources/network/ssh_connect.rb +32 -0
- data/resources/nodes/apt_sources.rb +66 -0
- data/resources/nodes/debian/Containerfile +10 -0
- data/resources/nodes/diagnostic_exec.rb +109 -0
- data/resources/nodes/interactive_tools.rb +139 -0
- data/resources/nodes/managed_file.rb +91 -0
- data/resources/nodes/management/guest_agent.py +415 -0
- data/resources/nodes/management/setup.sh +20 -0
- data/resources/nodes/management/shell_rc.sh +12 -0
- data/resources/nodes/management/system_account.sh +24 -0
- data/resources/nodes/management/system_home.sh +15 -0
- data/resources/nodes/rpm/Containerfile +10 -0
- data/resources/nodes/runtime_proxy.rb +244 -0
- data/resources/nodes/ssh/proxy.rb +16 -0
- data/resources/nodes/ssh/sshd_config +15 -0
- data/resources/nodes/ssh/start +6 -0
- data/resources/proxy/Containerfile +10 -0
- data/resources/relay/Containerfile +5 -0
- data/resources/relay/puppetdb.rb +81 -0
- data/resources/relay/server_environment.rb +10 -0
- data/resources/relay/server_start.sh +41 -0
- metadata +326 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
require_relative 'launch_error'
|
|
6
|
+
|
|
7
|
+
module Empeira
|
|
8
|
+
module Execution
|
|
9
|
+
# rubocop:disable-next Metrics/ClassLength -- Capture, process streams and direct consoles share one execution boundary.
|
|
10
|
+
class Runner
|
|
11
|
+
def initialize(platform: Platform::Facts.new, logger: nil)
|
|
12
|
+
@platform = platform
|
|
13
|
+
@logger = logger
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run(executable, arguments: [], environment: {}, directory: nil, timeout: nil, on_stdout: nil)
|
|
17
|
+
validate!(executable, arguments, environment, timeout)
|
|
18
|
+
log(arguments, environment)
|
|
19
|
+
options = { pgroup: true }
|
|
20
|
+
options[:chdir] = directory.to_s if directory
|
|
21
|
+
result = capture(environment, executable, arguments, options, timeout, on_stdout)
|
|
22
|
+
@logger&.debug({ operation: 'execute', exit_status: result.exit_status, timed_out: result.timed_out })
|
|
23
|
+
result
|
|
24
|
+
rescue SystemCallError => e
|
|
25
|
+
# OS exception messages can include command arguments and credentials.
|
|
26
|
+
raise ExecutionError, "Process could not be executed (#{e.class.name})", cause: nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Inherit caller streams for terminals and unbounded logs; never buffer them.
|
|
30
|
+
# Explicit stream handles avoid hidden globals in process-execution tests.
|
|
31
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
32
|
+
def stream(executable, arguments: [], environment: {}, directory: nil,
|
|
33
|
+
input: $stdin, output: $stdout, error: $stderr)
|
|
34
|
+
validate!(executable, arguments, environment, nil)
|
|
35
|
+
log(arguments, environment)
|
|
36
|
+
options = { in: input, out: output, err: error }
|
|
37
|
+
options[:chdir] = directory.to_s if directory
|
|
38
|
+
pid = Process.spawn(environment, [executable, executable], *arguments, **options)
|
|
39
|
+
_, status = Process.wait2(pid)
|
|
40
|
+
pid = nil
|
|
41
|
+
Result.new(stdout: '', stderr: '', exit_status: status.exitstatus || (128 + status.termsig), timed_out: false)
|
|
42
|
+
rescue SystemCallError => e
|
|
43
|
+
raise ExecutionError, "Streaming process could not execute (#{e.class.name})", cause: nil
|
|
44
|
+
ensure
|
|
45
|
+
reap_stream(pid) if pid
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def console(path, input: $stdin, output: $stdout, guidance: nil)
|
|
49
|
+
Console.new.attach(path, input: input, output: output, guidance: guidance)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A terminal-preserving handoff: signals, process identity and status belong
|
|
53
|
+
# directly to the standalone program, with no forwarding proxy or shell.
|
|
54
|
+
def replace(executable, arguments: [], environment: {})
|
|
55
|
+
validate!(executable, arguments, environment, nil)
|
|
56
|
+
explicit_format!(executable)
|
|
57
|
+
log(arguments, environment)
|
|
58
|
+
Kernel.exec(environment, [executable, executable], *arguments)
|
|
59
|
+
rescue Errno::ENOENT
|
|
60
|
+
raise LaunchError.new('executable or its interpreter was not found', exit_status: 127), cause: nil
|
|
61
|
+
rescue Errno::E2BIG
|
|
62
|
+
raise LaunchError.new('environment and arguments exceed the operating system size limit', exit_status: 126),
|
|
63
|
+
cause: nil
|
|
64
|
+
rescue SystemCallError
|
|
65
|
+
raise LaunchError.new('executable could not be started', exit_status: 126), cause: nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Inherited output streams immediately; closing control cancels the owned guest session.
|
|
69
|
+
def controlled_stream(executable, arguments:, input:, control:, output: $stdout, error: $stderr)
|
|
70
|
+
validate!(executable, arguments, {}, nil)
|
|
71
|
+
log(arguments, {})
|
|
72
|
+
pid = Process.spawn([executable, executable], *arguments, in: input, out: output, err: error, pgroup: true)
|
|
73
|
+
_, status = Process.wait2(pid)
|
|
74
|
+
pid = nil
|
|
75
|
+
Result.new(stdout: '', stderr: '', exit_status: status.exitstatus || (128 + status.termsig), timed_out: false)
|
|
76
|
+
rescue SystemCallError => e
|
|
77
|
+
raise ExecutionError, "Cannot start diagnostic transport (#{e.class.name})", cause: nil
|
|
78
|
+
ensure
|
|
79
|
+
control.close unless control.closed?
|
|
80
|
+
reap_controlled_stream(pid) if pid
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def debug(message)
|
|
84
|
+
@logger&.debug(message)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Keep the caller's input and foreground terminal for authentication, but capture tool output.
|
|
88
|
+
# rubocop:disable-next Metrics/AbcSize -- Paired pipes and reader cleanup bracket one foreground process.
|
|
89
|
+
def buffered(executable, arguments: [], environment: {}, directory: nil, input: $stdin)
|
|
90
|
+
IO.pipe do |stdout, output|
|
|
91
|
+
IO.pipe do |stderr, error|
|
|
92
|
+
readers = [stdout, stderr].map { |io| Thread.new { io.binmode.read } }
|
|
93
|
+
result = stream(executable, arguments: arguments, environment: environment, directory: directory,
|
|
94
|
+
input: input, output: output, error: error)
|
|
95
|
+
output.close
|
|
96
|
+
error.close
|
|
97
|
+
Result.new(stdout: readers[0].value, stderr: readers[1].value,
|
|
98
|
+
exit_status: result.exit_status, timed_out: result.timed_out)
|
|
99
|
+
ensure
|
|
100
|
+
readers&.each { |thread| thread.kill.join }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# Ruby otherwise silently interprets plain executable text with /bin/sh.
|
|
108
|
+
# Native ELF/Mach-O binaries and scripts with an explicit interpreter are
|
|
109
|
+
# the executable formats supported by Linux, WSL2 and macOS.
|
|
110
|
+
def explicit_format!(executable)
|
|
111
|
+
header = File.binread(executable, 4)
|
|
112
|
+
native = ["\x7fELF".b, "\xfe\xed\xfa\xce".b, "\xce\xfa\xed\xfe".b,
|
|
113
|
+
"\xfe\xed\xfa\xcf".b, "\xcf\xfa\xed\xfe".b, "\xca\xfe\xba\xbe".b,
|
|
114
|
+
"\xbe\xba\xfe\xca".b, "\xca\xfe\xba\xbf".b, "\xbf\xba\xfe\xca".b]
|
|
115
|
+
return if header.start_with?('#!') || native.include?(header)
|
|
116
|
+
|
|
117
|
+
raise LaunchError.new('entrypoint must be a native executable or a script with a shebang', exit_status: 126)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def reap_controlled_stream(pid)
|
|
121
|
+
Timeout.timeout(4) { Process.wait(pid) }
|
|
122
|
+
rescue Timeout::Error
|
|
123
|
+
terminate(pid)
|
|
124
|
+
Process.wait(pid)
|
|
125
|
+
rescue Errno::ECHILD
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def reap_stream(pid)
|
|
130
|
+
Process.kill('TERM', pid)
|
|
131
|
+
Timeout.timeout(2) { Process.wait(pid) }
|
|
132
|
+
rescue Timeout::Error
|
|
133
|
+
Process.kill('KILL', pid)
|
|
134
|
+
Process.wait(pid)
|
|
135
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate!(executable, arguments, environment, timeout)
|
|
140
|
+
unless command_string?(executable) && !executable.empty? &&
|
|
141
|
+
arguments.is_a?(Array) && arguments.all? { |arg| command_string?(arg) }
|
|
142
|
+
raise ExecutionError, 'Executable and arguments must be strings without null bytes'
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
validate_environment!(environment)
|
|
146
|
+
validate_timeout!(timeout)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def command_string?(value)
|
|
150
|
+
value.is_a?(String) && !value.include?("\0")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def validate_environment!(environment)
|
|
154
|
+
return if environment.is_a?(Hash) && environment.all? { |key, value| environment_entry?(key, value) }
|
|
155
|
+
|
|
156
|
+
raise ExecutionError, 'Environment must map valid variable names to strings or null'
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def environment_entry?(key, value)
|
|
160
|
+
command_string?(key) && !key.empty? && !key.include?('=') && (value.nil? || command_string?(value))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validate_timeout!(timeout)
|
|
164
|
+
return if timeout.nil? || (timeout.is_a?(Numeric) && timeout.positive? && timeout.finite?)
|
|
165
|
+
|
|
166
|
+
raise ExecutionError, 'Timeout must be a positive finite number'
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def log(arguments, environment)
|
|
170
|
+
@logger&.debug({ operation: 'execute', arguments: arguments.map { '[REDACTED]' },
|
|
171
|
+
environment: environment.transform_values { '[REDACTED]' } })
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def capture(environment, executable, arguments, options, timeout, on_stdout)
|
|
175
|
+
# The two-element executable form prevents Ruby's single-string shell fallback.
|
|
176
|
+
Open3.popen3(environment, [executable, executable], *arguments, **options) do |stdin, stdout, stderr, process|
|
|
177
|
+
completed = false
|
|
178
|
+
stdin.close
|
|
179
|
+
streams = [Thread.new { read_output(stdout, on_stdout) }, Thread.new { stderr.binmode.read }]
|
|
180
|
+
result = collect(process, streams, timeout)
|
|
181
|
+
completed = true
|
|
182
|
+
result
|
|
183
|
+
ensure
|
|
184
|
+
cleanup(process, streams, completed)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def read_output(io, observer)
|
|
189
|
+
return io.binmode.read unless observer
|
|
190
|
+
|
|
191
|
+
buffer = +''.b
|
|
192
|
+
loop do
|
|
193
|
+
chunk = io.readpartial(16_384)
|
|
194
|
+
buffer << chunk
|
|
195
|
+
observer.call(chunk)
|
|
196
|
+
end
|
|
197
|
+
rescue EOFError
|
|
198
|
+
buffer
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def collect(process, streams, timeout)
|
|
202
|
+
Timeout.timeout(timeout) do
|
|
203
|
+
status = process.value
|
|
204
|
+
Result.new(stdout: streams[0].value.freeze, stderr: streams[1].value.freeze,
|
|
205
|
+
exit_status: status.exitstatus, timed_out: false)
|
|
206
|
+
end
|
|
207
|
+
rescue Timeout::Error
|
|
208
|
+
timed_out(process, streams)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def timed_out(process, streams)
|
|
212
|
+
terminate(process.pid)
|
|
213
|
+
process.join
|
|
214
|
+
output = streams.map { |stream| stream.join(1) ? stream.value : '' }
|
|
215
|
+
Result.new(stdout: output[0], stderr: output[1], exit_status: nil, timed_out: true)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def cleanup(process, streams, completed)
|
|
219
|
+
unless completed
|
|
220
|
+
# A reaped group leader does not mean its descendants have exited.
|
|
221
|
+
terminate(process.pid)
|
|
222
|
+
process.join
|
|
223
|
+
end
|
|
224
|
+
ensure
|
|
225
|
+
streams&.each { |thread| thread.kill.join }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def terminate(pid)
|
|
229
|
+
Process.kill('KILL', -pid)
|
|
230
|
+
rescue Errno::ESRCH
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Execution
|
|
5
|
+
module Terminal
|
|
6
|
+
CLEAN_EXIT_SIGNALS = %w[QUIT TSTP].map { |signal| Signal.list.fetch(signal) }.freeze
|
|
7
|
+
|
|
8
|
+
# rubocop:disable-next Metrics/AbcSize -- Signal status and restored handlers share the session cleanup boundary.
|
|
9
|
+
def self.with_interrupts
|
|
10
|
+
return yield unless Thread.current == Thread.main
|
|
11
|
+
|
|
12
|
+
previous = %w[TERM HUP QUIT TSTP].to_h do |signal|
|
|
13
|
+
[signal, Signal.trap(signal) { raise SignalException, signal }]
|
|
14
|
+
end
|
|
15
|
+
yield
|
|
16
|
+
rescue SignalException => e
|
|
17
|
+
raise unless CLEAN_EXIT_SIGNALS.include?(e.signo)
|
|
18
|
+
|
|
19
|
+
# Preserve the signal status after cleanup without a core dump or a suspended raw terminal.
|
|
20
|
+
raise SystemExit, 128 + e.signo
|
|
21
|
+
ensure
|
|
22
|
+
previous&.each { |signal, handler| Signal.trap(signal, handler) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.interactive?(output: $stdout, error: $stderr, environment: ENV)
|
|
26
|
+
[output, error].all? { |io| io.respond_to?(:tty?) && io.tty? } &&
|
|
27
|
+
environment['TERM'].to_s.downcase != 'dumb' && environment['CI'].to_s.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.color?(**options)
|
|
31
|
+
interactive?(**options) && options.fetch(:environment, ENV)['NO_COLOR'].to_s.empty?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
# Full-hostname glob semantics shared by independent hostname policies.
|
|
5
|
+
module HostnamePattern
|
|
6
|
+
def self.valid?(value)
|
|
7
|
+
value.is_a?(String) && value.size.between?(1, 253) && value.match?(/\A[a-z0-9*?][a-z0-9.*?-]*\z/i)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.matches?(pattern, hostname)
|
|
11
|
+
File.fnmatch?(pattern.downcase, hostname.downcase)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Images
|
|
5
|
+
module Configuration
|
|
6
|
+
REPOSITORY = %r{\A[a-z0-9]+(?:[._:/-][a-z0-9]+)*\z}
|
|
7
|
+
TAG = /\A\w[\w.-]{0,127}\z/
|
|
8
|
+
DIGEST = /\Asha256:[a-f0-9]{64}\z/
|
|
9
|
+
ROOT = File.expand_path('../../../resources', __dir__)
|
|
10
|
+
|
|
11
|
+
def self.reference(data, registry: nil)
|
|
12
|
+
return data.fetch('reference') if data['reference']
|
|
13
|
+
|
|
14
|
+
repository = Reference.repository(data.fetch('repository'), registry: registry)
|
|
15
|
+
return "#{repository}@#{data['digest']}" if data['digest']
|
|
16
|
+
|
|
17
|
+
tag = data.fetch('tag')
|
|
18
|
+
"#{repository}:#{tag}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.recipe(data, registry: nil)
|
|
22
|
+
relative = data.fetch('build')
|
|
23
|
+
path = Pathname.new(ROOT).join(relative).realpath
|
|
24
|
+
unless path.to_s.start_with?("#{ROOT}/")
|
|
25
|
+
raise ConfigurationError,
|
|
26
|
+
'images build resource must be packaged with Empeira'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Recipe.render(File.read(path), registry: registry)
|
|
30
|
+
rescue SystemCallError
|
|
31
|
+
raise ConfigurationError, 'images build resource is unavailable', cause: nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.artifact(data, purpose: 'squid', registry: nil)
|
|
35
|
+
recipe = self.recipe(data, registry: registry) if data['build'] && !data['reference']
|
|
36
|
+
{ image: recipe ? local_image(recipe, purpose: purpose) : reference(data, registry: registry), recipe: recipe }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.local_image(recipe, purpose: 'squid')
|
|
40
|
+
"localhost/empeira-#{purpose}:#{Digest::SHA256.hexdigest(recipe)}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module Images
|
|
8
|
+
class Identity
|
|
9
|
+
attr_reader :cache_key, :distribution, :version, :architecture, :source, :revision, :checksum
|
|
10
|
+
|
|
11
|
+
def to_h
|
|
12
|
+
{ 'distribution' => distribution, 'version' => version, 'architecture' => architecture,
|
|
13
|
+
'source' => source, 'revision' => revision, 'checksum' => checksum }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# rubocop:disable-next Metrics/AbcSize -- All fields participate in the immutable identity.
|
|
17
|
+
def initialize(distribution:, version:, architecture:, source:, revision:, checksum:)
|
|
18
|
+
fields = [distribution, version, architecture, source, revision, checksum]
|
|
19
|
+
unless fields.all? { |value| value.is_a?(String) && !value.strip.empty? }
|
|
20
|
+
raise Error, 'Image identity fields must be non-empty strings'
|
|
21
|
+
end
|
|
22
|
+
raise Error, 'Image checksum must be a SHA-256 hex digest' unless checksum.match?(/\A[0-9a-f]{64}\z/i)
|
|
23
|
+
|
|
24
|
+
@distribution, @version, @architecture, @source, @revision, @checksum = fields
|
|
25
|
+
@cache_key = Digest::SHA256.hexdigest(JSON.generate(fields[0...-1] + [checksum.downcase])).freeze
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Images
|
|
5
|
+
# Only reviewed EMPEIRA_BASE_* ARG declarations are dependencies. This is not a Dockerfile parser.
|
|
6
|
+
module Recipe
|
|
7
|
+
BASE = /^ARG (EMPEIRA_BASE_[A-Z0-9_]+)=([^\s]+)$/
|
|
8
|
+
|
|
9
|
+
def self.render(source, registry: nil)
|
|
10
|
+
source.gsub(BASE) do
|
|
11
|
+
name, value = Regexp.last_match.captures
|
|
12
|
+
value = Reference.base(value, registry: registry) unless value.start_with?('@') || value == 'scratch'
|
|
13
|
+
"ARG #{name}=#{value}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.bases(recipe)
|
|
18
|
+
recipe.scan(BASE).to_h.reject { |_, value| value == 'scratch' }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Images
|
|
5
|
+
# Registry selection applies to repository fields, never exact reference overrides.
|
|
6
|
+
module Reference
|
|
7
|
+
HOST = /\A(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::[0-9]+)?\z/
|
|
8
|
+
|
|
9
|
+
def self.hosted?(repository)
|
|
10
|
+
first = repository.split('/').first
|
|
11
|
+
first.include?('.') || first.include?(':') || first == 'localhost'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.registry?(value)
|
|
15
|
+
return true if value.nil?
|
|
16
|
+
return false unless value.is_a?(String) && value.match?(HOST) && hosted?(value)
|
|
17
|
+
|
|
18
|
+
port = value.split(':', 2)[1]
|
|
19
|
+
port.nil? || (1..65_535).cover?(port.to_i)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.repository(value, registry: nil)
|
|
23
|
+
hosted?(value) ? value : "#{registry || 'docker.io'}/#{value}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Reserved recipe base declarations contain a tag or digest, not a YAML reference override.
|
|
27
|
+
def self.base(value, registry: nil)
|
|
28
|
+
path = value.split('@', 2).first.sub(%r{:[^/:]+\z}, '')
|
|
29
|
+
repository(path, registry: registry) + value.delete_prefix(path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.pinned(reference, digest)
|
|
33
|
+
repository = reference.split('@').first.sub(%r{:[^/:]+\z}, '')
|
|
34
|
+
"#{repository}@#{digest}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.host(reference)
|
|
38
|
+
first, path = reference.split('/', 2)
|
|
39
|
+
path && registry?(first) ? first : 'docker.io'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Lint/UnusedMethodArgument -- Preserve explicit adapter keyword contracts.
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Images
|
|
7
|
+
class Source
|
|
8
|
+
def resolve(identity:)
|
|
9
|
+
raise UnavailableFeature, 'This command is not implemented yet.'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def fetch(identity:)
|
|
13
|
+
raise UnavailableFeature, 'This command is not implemented yet.'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Immutable
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def deep_freeze(value)
|
|
8
|
+
case value
|
|
9
|
+
when Hash
|
|
10
|
+
value.each do |key, child|
|
|
11
|
+
deep_freeze(key)
|
|
12
|
+
deep_freeze(child)
|
|
13
|
+
end
|
|
14
|
+
when Array then value.each { |child| deep_freeze(child) }
|
|
15
|
+
end
|
|
16
|
+
value.freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|