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,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
# Native CLIs own registry authentication, manifest negotiation and transport.
|
|
6
|
+
module ImageMetadata
|
|
7
|
+
def with_image_updates
|
|
8
|
+
previous = @image_metadata
|
|
9
|
+
@image_metadata = {}
|
|
10
|
+
yield
|
|
11
|
+
ensure
|
|
12
|
+
@image_metadata = previous
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def remote_digest(image)
|
|
16
|
+
return @image_metadata.fetch(image) if @image_metadata&.key?(image)
|
|
17
|
+
|
|
18
|
+
digest = platform_digest(remote_descriptors(image))
|
|
19
|
+
@image_metadata[image] = digest if @image_metadata
|
|
20
|
+
digest
|
|
21
|
+
rescue Error => e
|
|
22
|
+
raise Providers::ExecutionError, "Image metadata check failed for #{image}: #{e.message}", cause: nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# rubocop:disable-next Metrics/AbcSize -- Validate descriptor shape, platform and unambiguous digest together.
|
|
28
|
+
def platform_digest(descriptors)
|
|
29
|
+
unless descriptors.is_a?(Array) && descriptors.all?(Hash)
|
|
30
|
+
raise Providers::ExecutionError, 'Runtime returned malformed image descriptors'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
selected = descriptors.select { |entry| matching_platform?(entry['platform']) }
|
|
34
|
+
digests = selected.map { |entry| entry['digest'] }.uniq
|
|
35
|
+
unless digests.size == 1 && valid_digest?(digests.first)
|
|
36
|
+
raise Providers::ExecutionError, "No unambiguous Linux/#{architecture} manifest digest in runtime metadata"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
digests.first
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def matching_platform?(platform)
|
|
43
|
+
return false unless platform.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
platform['os'] == 'linux' && platform['architecture'] == architecture &&
|
|
46
|
+
(platform['variant'].to_s.empty? || (architecture == 'arm64' && platform['variant'] == 'v8'))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def valid_digest?(value)
|
|
50
|
+
value.is_a?(String) && value.match?(Images::Configuration::DIGEST)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def metadata_command(arguments, image:, timeout: 30)
|
|
54
|
+
update_command(arguments, operation: 'remote image metadata', timeout: timeout, registry: image)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def local_image(image)
|
|
58
|
+
result = local_image_inspection(image)
|
|
59
|
+
return unless result.success?
|
|
60
|
+
|
|
61
|
+
data = parse_json(result.stdout)
|
|
62
|
+
malformed! unless data.is_a?(Array) && data.size == 1 && data.first.is_a?(Hash)
|
|
63
|
+
data.first
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def local_digests(data)
|
|
67
|
+
[data['Digest'], *Array(data['RepoDigests']).map { |value| value.to_s.split('@', 2)[1] }]
|
|
68
|
+
.select { |digest| valid_digest?(digest) }.uniq
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def same_remote_image?(image, local, remote)
|
|
72
|
+
return false unless local && local['Architecture'] == architecture && local['Os'] == 'linux'
|
|
73
|
+
|
|
74
|
+
digests = local_digests(local)
|
|
75
|
+
return true if digests.include?(remote)
|
|
76
|
+
|
|
77
|
+
# Docker's classic store can record an index digest. Resolve that immutable
|
|
78
|
+
# historical reference natively before comparing platform manifests.
|
|
79
|
+
digests.any? { |digest| remote_digest(Images::Reference.pinned(image, digest)) == remote }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
# Prove that an engine sees this client's canonical directory, including VM-backed engines.
|
|
6
|
+
class MountProbe
|
|
7
|
+
def initialize(runtime:)
|
|
8
|
+
@runtime = runtime
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def verify(resource, mounts)
|
|
12
|
+
mounts.each do |mount|
|
|
13
|
+
fields = mount.split(',').select { |field| field.include?('=') }.to_h { |field| field.split('=', 2) }
|
|
14
|
+
if File.file?(fields.fetch('src'))
|
|
15
|
+
verify_file(resource, fields.fetch('src'), fields.fetch('dst'))
|
|
16
|
+
else
|
|
17
|
+
verify_directory(resource, fields.fetch('src'), fields.fetch('dst'))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def verify_file(resource, source, destination)
|
|
25
|
+
code = 'exit(File.file?(ARGV[0]) && File.readable?(ARGV[0]) ? 0 : 1)'
|
|
26
|
+
result = @runtime.service_exec(resource, [ControlPlane::Health::RUBY, '-e', code, destination])
|
|
27
|
+
return if result.success?
|
|
28
|
+
|
|
29
|
+
failed_probe!(result, source, destination, 'Cannot verify the configured file mount (EYAML or server.mounts)')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def verify_directory(resource, source, destination)
|
|
33
|
+
code = 'require "digest"; print Digest::SHA256.hexdigest(Dir.children(ARGV.fetch(0)).sort.join("\0"))'
|
|
34
|
+
expected = Digest::SHA256.hexdigest(Dir.children(source).sort.join("\0"))
|
|
35
|
+
result = @runtime.service_exec(resource, [ControlPlane::Health::RUBY, '-e', code, destination])
|
|
36
|
+
return if result.success? && result.stdout == expected
|
|
37
|
+
|
|
38
|
+
reason = if result.success?
|
|
39
|
+
'Container directory listing differs from the local mount source'
|
|
40
|
+
else
|
|
41
|
+
'Cannot execute the container directory mount probe'
|
|
42
|
+
end
|
|
43
|
+
failed_probe!(result, source, destination, reason)
|
|
44
|
+
rescue SystemCallError
|
|
45
|
+
location = Execution::Diagnostics.safe_text(source.to_s)
|
|
46
|
+
raise Error, "Cannot read local mount directory to verify the mount: #{location}", cause: nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def failed_probe!(result, source, destination, reason)
|
|
50
|
+
paths = Execution::Diagnostics.safe_text("Source: #{source}\nDestination: #{destination}")
|
|
51
|
+
details = Execution::Diagnostics.command(result, operation: 'Verify server mount visibility',
|
|
52
|
+
tool: ControlPlane::Health::RUBY)
|
|
53
|
+
raise Error, "#{reason}\n#{paths}\n#{details}", cause: nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
class Podman < Container
|
|
6
|
+
def node_ssh_publication?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(context:, runner:)
|
|
11
|
+
super(name: 'podman', context: context, runner: runner)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
# A disposable manifest forces registry access even if the tag exists locally.
|
|
17
|
+
# Podman resolves both single manifests and indexes without downloading layers.
|
|
18
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- Keep manifest ownership, deadline and cleanup in one transaction.
|
|
19
|
+
def remote_descriptors(image)
|
|
20
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30
|
|
21
|
+
temporary = "localhost/empeira-metadata-#{SecureRandom.hex(16)}"
|
|
22
|
+
result = update_command(['manifest', 'create', temporary], operation: 'temporary metadata creation', timeout: 5)
|
|
23
|
+
identifier = result.stdout.strip
|
|
24
|
+
unless identifier.match?(/\A[0-9a-f]{64}\z/)
|
|
25
|
+
raise Providers::ExecutionError,
|
|
26
|
+
'Invalid temporary manifest identity'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
metadata_command(['manifest', 'add', '--all', identifier, "docker://#{image}"],
|
|
30
|
+
image: image, timeout: metadata_remaining(deadline))
|
|
31
|
+
result = metadata_command(['manifest', 'inspect', identifier], image: image,
|
|
32
|
+
timeout: metadata_remaining(deadline))
|
|
33
|
+
data = parse_json(result.stdout)
|
|
34
|
+
malformed! unless data.is_a?(Hash) && data['manifests'].is_a?(Array)
|
|
35
|
+
data['manifests']
|
|
36
|
+
ensure
|
|
37
|
+
if identifier&.match?(/\A[0-9a-f]{64}\z/)
|
|
38
|
+
update_command(['manifest', 'rm', identifier], operation: 'temporary metadata cleanup', timeout: 5)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def metadata_remaining(deadline)
|
|
43
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
44
|
+
raise Providers::ExecutionError, 'Remote metadata timeout' unless remaining.positive?
|
|
45
|
+
|
|
46
|
+
remaining
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def update_helper_user
|
|
50
|
+
['--userns', 'keep-id', *super]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def update_helper_network
|
|
54
|
+
'podman'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def info_arguments
|
|
58
|
+
['info', '--format', 'json']
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_info!(info)
|
|
62
|
+
malformed! unless info.is_a?(Hash) && info['host'].is_a?(Hash)
|
|
63
|
+
return if info.dig('host', 'networkBackend') == 'netavark' &&
|
|
64
|
+
bridge_available?(info['plugins'])
|
|
65
|
+
|
|
66
|
+
raise UnsupportedCapability, 'Podman requires the Netavark network backend with bridge support'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def bridge_available?(plugins)
|
|
70
|
+
plugins.is_a?(Hash) && plugins['network'].is_a?(Array) && plugins['network'].include?('bridge')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def network_entries
|
|
74
|
+
entries = json_command(['network', 'ls', '--format', 'json'], operation: 'network inventory')
|
|
75
|
+
malformed! unless entries.is_a?(Array) && entries.all?(Hash)
|
|
76
|
+
validate_entries(entries.map { |item| { 'id' => item['id'], 'name' => item['name'] } })
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def create_arguments(definition)
|
|
80
|
+
if definition.is_a?(Network::Egress)
|
|
81
|
+
return ['network', 'create', '--driver', 'bridge', *label_arguments(definition), definition.backend_name]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
['network', 'create', '--driver', 'bridge', '--internal', '--disable-dns',
|
|
85
|
+
'--opt', 'isolate=true', *peer_network_options(definition), *label_arguments(definition),
|
|
86
|
+
definition.backend_name]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def attachment_count(identifier)
|
|
90
|
+
entries = json_command(['ps', '--all', '--filter', "network=#{identifier}", '--format', 'json'],
|
|
91
|
+
operation: 'network membership')
|
|
92
|
+
malformed! unless entries.is_a?(Array) && entries.all?(Hash)
|
|
93
|
+
entries.size
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def peer_network_options(definition)
|
|
97
|
+
return [] unless definition.allocation
|
|
98
|
+
|
|
99
|
+
layout = Network::Peer::Layout.new(definition.allocation)
|
|
100
|
+
['--subnet', layout.subnet, '--ip-range', layout.container_pool,
|
|
101
|
+
'--interface-name', "ep#{context.workspace.id[0, 10]}"]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def isolated?(data)
|
|
105
|
+
data['internal'] && data['driver'] == 'bridge' && data['dns_enabled'] == false &&
|
|
106
|
+
data['options']['isolate'] == 'true'
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def normalize_network_binding(data)
|
|
110
|
+
data = data.merge('options' => data['options'] || {})
|
|
111
|
+
malformed! unless [true, false].include?(data['internal']) && data['options'].is_a?(Hash)
|
|
112
|
+
Network::Binding.new(id: data['id'], name: data['name'], labels: data['labels'] || {},
|
|
113
|
+
isolated: isolated?(data))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def normalize_network(data)
|
|
117
|
+
binding = normalize_network_binding(data)
|
|
118
|
+
Network::Resource.new(**binding.to_h, attachment_count: attachment_count(binding.id))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
module RecipeBuild
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def base_state(recipe, refresh:)
|
|
9
|
+
Images::Recipe.bases(recipe).sort.to_h.transform_values do |reference|
|
|
10
|
+
digest = refresh ? remote_digest(reference) : available_base_digest(reference)
|
|
11
|
+
{ 'reference' => reference, 'digest' => digest }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def available_base_digest(reference)
|
|
16
|
+
ensure_image(reference)
|
|
17
|
+
data = local_image(reference)
|
|
18
|
+
unless data && data['Architecture'] == architecture && data['Os'] == 'linux'
|
|
19
|
+
raise Providers::ExecutionError, "Build base has an incompatible platform: #{reference}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
digest = local_digests(data).first
|
|
23
|
+
raise Providers::ExecutionError, "Build base has no registry digest: #{reference}" unless digest
|
|
24
|
+
|
|
25
|
+
digest
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def bases_current?(labels, expected)
|
|
29
|
+
text = labels['io.empeira.base-inputs']
|
|
30
|
+
return false unless text
|
|
31
|
+
|
|
32
|
+
previous = recorded_bases(text, labels['io.empeira.base-fingerprint'])
|
|
33
|
+
return false unless previous.keys.sort == expected.keys.sort
|
|
34
|
+
|
|
35
|
+
normalized = previous.to_h do |key, base|
|
|
36
|
+
value = normalized_base(base, expected.fetch(key))
|
|
37
|
+
return false unless value
|
|
38
|
+
|
|
39
|
+
[key, value]
|
|
40
|
+
end
|
|
41
|
+
Infrastructure::Definition.fingerprint(normalized) == Infrastructure::Definition.fingerprint(expected)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def recorded_bases(text, fingerprint)
|
|
45
|
+
previous = JSON.parse(text)
|
|
46
|
+
unless previous.is_a?(Hash) && Infrastructure::Definition.fingerprint(previous) == fingerprint
|
|
47
|
+
raise Providers::OwnershipError, 'Invalid local build base metadata'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
previous
|
|
51
|
+
rescue JSON::ParserError
|
|
52
|
+
raise Providers::OwnershipError, 'Unreadable local build base metadata', cause: nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def normalized_base(base, current)
|
|
56
|
+
return unless base.is_a?(Hash) && base['reference'] == current['reference'] && valid_digest?(base['digest'])
|
|
57
|
+
|
|
58
|
+
digest = base['digest']
|
|
59
|
+
digest = remote_digest(Images::Reference.pinned(base['reference'], digest)) if digest != current['digest']
|
|
60
|
+
base.merge('digest' => digest)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_image(image, recipe, files, refresh: false, bases: nil)
|
|
64
|
+
@local_image_inspections&.delete(image)
|
|
65
|
+
bases ||= base_state(recipe, refresh: refresh)
|
|
66
|
+
Dir.mktmpdir('empeira-image-') do |directory|
|
|
67
|
+
write_build_context(directory, recipe, files)
|
|
68
|
+
labels = build_labels(recipe, files, bases)
|
|
69
|
+
arguments = ['build', *(refresh ? ['--pull'] : []), '--tag', image,
|
|
70
|
+
*labels.flat_map { |key, value| ['--label', "#{key}=#{value}"] },
|
|
71
|
+
*base_arguments(bases),
|
|
72
|
+
'--file', File.join(directory, 'Containerfile'), directory]
|
|
73
|
+
update_command(arguments, operation: 'managed image build', timeout: 600)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_labels(recipe, files, bases)
|
|
78
|
+
{ 'io.empeira.recipe' => Digest::SHA256.hexdigest(recipe),
|
|
79
|
+
'io.empeira.build-inputs' => Infrastructure::Definition.fingerprint(files),
|
|
80
|
+
'io.empeira.base-inputs' => JSON.generate(bases),
|
|
81
|
+
'io.empeira.base-fingerprint' => Infrastructure::Definition.fingerprint(bases) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def base_arguments(bases)
|
|
85
|
+
bases.flat_map do |key, base|
|
|
86
|
+
reference = Images::Reference.pinned(base.fetch('reference'), base.fetch('digest'))
|
|
87
|
+
['--build-arg', "#{key}=#{reference}"]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def write_build_context(directory, recipe, files)
|
|
92
|
+
files.each do |name, content|
|
|
93
|
+
unless name.match?(/\A[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*\z/) && content.is_a?(String)
|
|
94
|
+
raise ConfigurationError, 'Image context must contain named reviewed resource files'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
File.write(File.join(directory, name), content)
|
|
98
|
+
end
|
|
99
|
+
File.write(File.join(directory, 'Containerfile'), recipe)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
module RegistryDiagnostics
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def registry_failure_hint(image, diagnostic, timed_out:)
|
|
9
|
+
host = Images::Reference.host(image)
|
|
10
|
+
case Execution::Diagnostics.registry_failure(diagnostic, timed_out: timed_out)
|
|
11
|
+
when :auth
|
|
12
|
+
"Registry authentication or authorization failed for #{host}. " \
|
|
13
|
+
"Authenticate with the selected container runtime, check repository permissions and retry:\n " \
|
|
14
|
+
"#{name} login #{host}"
|
|
15
|
+
when :missing then "Registry image or tag is unavailable: #{image}. Check the repository and tag."
|
|
16
|
+
when :network then "Registry network access failed for #{host}. Check connectivity and DNS."
|
|
17
|
+
when :tls then "Registry TLS verification failed for #{host}. Check the selected runtime's CA trust."
|
|
18
|
+
else 'Check the selected runtime registry access and diagnostic below.'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
class ServiceArguments
|
|
6
|
+
SCALARS = { 'dns' => '--dns', 'user' => '--user', 'entrypoint' => '--entrypoint',
|
|
7
|
+
'env_file' => '--env-file', 'shm_size' => '--shm-size', 'mac_address' => '--mac-address',
|
|
8
|
+
'ip' => '--ip', 'runtime_ip' => '--ip' }.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(definition)
|
|
11
|
+
@definition = definition
|
|
12
|
+
@options = definition.options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# One declarative argument vector makes runtime options auditable.
|
|
16
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
17
|
+
def build
|
|
18
|
+
['create', '--pull=never', '--name', @definition.name,
|
|
19
|
+
'--hostname', @options.fetch('hostname', @definition.key),
|
|
20
|
+
'--network', @options.fetch('network'), '--memory', "#{@options.fetch('memory')}m",
|
|
21
|
+
'--cpus', @options.fetch('cpus', 1).to_s, '--no-healthcheck',
|
|
22
|
+
*init_options, *security_options, *pairs('--label', @definition.labels), *scalar_options,
|
|
23
|
+
*pairs('--env', @options.fetch('environment', {}).merge(@options.fetch('runtime_environment', {}))),
|
|
24
|
+
*pairs('--sysctl', @options.fetch('sysctls', {})),
|
|
25
|
+
*@options.fetch('mounts', []).flat_map { |mount| ['--mount', mount] },
|
|
26
|
+
*@options.fetch('ports', []).flat_map { |port| ['--publish', port] },
|
|
27
|
+
@options.fetch('image'), *@options.fetch('command', [])]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def security_options
|
|
33
|
+
values = { 'cap_drop' => '--cap-drop', 'cap_add' => '--cap-add', 'devices' => '--device',
|
|
34
|
+
'security_options' => '--security-opt' }
|
|
35
|
+
[*(@options['read_only'] ? ['--read-only'] : []),
|
|
36
|
+
*values.flat_map { |key, flag| @options.fetch(key, []).flat_map { |value| [flag, value] } }]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def init_options
|
|
40
|
+
return ['--init'] if @options['init'] == 'process'
|
|
41
|
+
return [] unless @options['init'] == 'systemd'
|
|
42
|
+
|
|
43
|
+
['--systemd=always', '--cgroupns=private', '--tmpfs', '/run', '--tmpfs', '/tmp']
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def scalar_options
|
|
47
|
+
SCALARS.flat_map { |key, option| @options[key] ? [option, @options[key]] : [] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def pairs(option, values)
|
|
51
|
+
values.flat_map { |key, value| [option, "#{key}=#{value}"] }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
module ServiceImages
|
|
6
|
+
def with_local_images
|
|
7
|
+
return yield if @local_image_inspections
|
|
8
|
+
|
|
9
|
+
@local_image_inspections = {}
|
|
10
|
+
begin
|
|
11
|
+
yield
|
|
12
|
+
ensure
|
|
13
|
+
@local_image_inspections = nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ensure_image(image, recipe: nil, files: {})
|
|
18
|
+
result = local_image_inspection(image)
|
|
19
|
+
return verify_recipe!(result, recipe) if result.success?
|
|
20
|
+
|
|
21
|
+
@local_image_inspections&.delete(image)
|
|
22
|
+
return build_image(image, recipe, files) if recipe
|
|
23
|
+
|
|
24
|
+
update_command(['pull', image], operation: "image pull #{image}", registry: image)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def refresh_image(image, recipe: nil, files: {}, &progress)
|
|
28
|
+
unless @image_metadata
|
|
29
|
+
return with_image_updates { refresh_image(image, recipe: recipe, files: files, &progress) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
observed = local_image(image)
|
|
33
|
+
return refresh_recipe(image, recipe, files, observed, &progress) if recipe
|
|
34
|
+
|
|
35
|
+
digest = remote_digest(image)
|
|
36
|
+
return :unchanged if same_remote_image?(image, observed, digest)
|
|
37
|
+
|
|
38
|
+
progress&.call(:updating)
|
|
39
|
+
@local_image_inspections&.delete(image)
|
|
40
|
+
update_command(['pull', image], operation: "image pull #{image}", registry: image)
|
|
41
|
+
:updated
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def image_id(image)
|
|
45
|
+
result = required_image_inspection(image, operation: 'image identity')
|
|
46
|
+
data = parse_json(result.stdout)
|
|
47
|
+
unless data.is_a?(Array) && data.size == 1 && data.first.is_a?(Hash) && data.first['Id'].is_a?(String)
|
|
48
|
+
malformed!
|
|
49
|
+
end
|
|
50
|
+
data.first.fetch('Id')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def image_architecture(image)
|
|
54
|
+
result = required_image_inspection(image, operation: 'node image architecture')
|
|
55
|
+
data = parse_json(result.stdout)
|
|
56
|
+
malformed! unless data.is_a?(Array) && data.size == 1 && data.first.is_a?(Hash)
|
|
57
|
+
architecture = data.first['Architecture']
|
|
58
|
+
malformed! unless %w[amd64 arm64].include?(architecture)
|
|
59
|
+
architecture
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def local_image_inspection(image)
|
|
65
|
+
return @local_image_inspections.fetch(image) if @local_image_inspections&.key?(image)
|
|
66
|
+
|
|
67
|
+
result = runner.run(name, arguments: ['image', 'inspect', image], timeout: 30)
|
|
68
|
+
@local_image_inspections[image] = result if @local_image_inspections
|
|
69
|
+
result
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def required_image_inspection(image, operation:)
|
|
73
|
+
result = local_image_inspection(image)
|
|
74
|
+
return result if result.success?
|
|
75
|
+
|
|
76
|
+
raise Providers::ExecutionError,
|
|
77
|
+
"#{name} #{operation} failed; run empeira status\n" \
|
|
78
|
+
"#{Execution::Diagnostics.native(result, operation: operation, tool: name)}", cause: nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# rubocop:disable-next Metrics/AbcSize -- Compare all three build inputs before deciding whether to rebuild.
|
|
82
|
+
def refresh_recipe(image, recipe, files, observed)
|
|
83
|
+
labels = observed.to_h.fetch('Config', {}).to_h.fetch('Labels', {}).to_h
|
|
84
|
+
if observed && labels['io.empeira.recipe'] != Digest::SHA256.hexdigest(recipe)
|
|
85
|
+
raise Providers::OwnershipError, 'Local service image does not match the recorded build recipe'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
bases = base_state(recipe, refresh: true)
|
|
89
|
+
if observed && labels['io.empeira.build-inputs'] == Infrastructure::Definition.fingerprint(files) &&
|
|
90
|
+
bases_current?(labels, bases)
|
|
91
|
+
return :unchanged
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
yield :updating if block_given?
|
|
95
|
+
build_image(image, recipe, files, refresh: true, bases: bases)
|
|
96
|
+
:updated
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def verify_recipe!(result, recipe)
|
|
100
|
+
return unless recipe
|
|
101
|
+
|
|
102
|
+
return if image_labels(result.stdout)['io.empeira.recipe'] == Digest::SHA256.hexdigest(recipe)
|
|
103
|
+
|
|
104
|
+
raise Providers::OwnershipError, 'Local service image does not match the recorded build recipe'
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def image_labels(stdout)
|
|
108
|
+
data = parse_json(stdout)
|
|
109
|
+
malformed! unless data.is_a?(Array) && data.size == 1 && data.first.is_a?(Hash)
|
|
110
|
+
config = data.first.fetch('Config', {})
|
|
111
|
+
malformed! unless config.is_a?(Hash)
|
|
112
|
+
labels = config.fetch('Labels', {})
|
|
113
|
+
malformed! unless labels.is_a?(Hash)
|
|
114
|
+
labels
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|