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,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'pathname'
|
|
7
|
+
require 'tmpdir'
|
|
8
|
+
require 'tempfile'
|
|
9
|
+
|
|
10
|
+
module Empeira
|
|
11
|
+
module Agent
|
|
12
|
+
class CacheCorruption < Error; end
|
|
13
|
+
|
|
14
|
+
class Cache
|
|
15
|
+
include CacheStorage
|
|
16
|
+
|
|
17
|
+
def initialize(root:, temporary: Dir.tmpdir)
|
|
18
|
+
@root = Pathname(root)
|
|
19
|
+
@temporary = temporary
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def with_artifact(request, enabled:, acquire:, pin: nil, &use)
|
|
23
|
+
return temporary(request, pin, acquire, &use) unless enabled
|
|
24
|
+
|
|
25
|
+
prepare
|
|
26
|
+
key = fingerprint(request)
|
|
27
|
+
lock(key) do
|
|
28
|
+
artifact = load(key, request, pin) || acquire_cached(key, request, pin, acquire)
|
|
29
|
+
use.call(artifact)
|
|
30
|
+
end
|
|
31
|
+
rescue SystemCallError, IOError
|
|
32
|
+
raise Error, 'Cannot read or write the private agent cache; check ownership and permissions', cause: nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.fingerprint(value)
|
|
36
|
+
Digest::SHA256.hexdigest(JSON.generate(canonical(value)))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.canonical(value)
|
|
40
|
+
return value.sort.to_h.transform_values { |child| canonical(child) } if value.is_a?(Hash)
|
|
41
|
+
return value.map { |child| canonical(child) } if value.is_a?(Array)
|
|
42
|
+
|
|
43
|
+
value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def fingerprint(value)
|
|
49
|
+
self.class.fingerprint(value)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def temporary(request, pin, acquire)
|
|
53
|
+
Dir.mktmpdir('empeira-agent-', @temporary) do |directory|
|
|
54
|
+
artifact = acquire.call(directory)
|
|
55
|
+
validate!(artifact, request, pin)
|
|
56
|
+
yield artifact
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def acquire_cached(key, request, pin, acquire)
|
|
61
|
+
Dir.mktmpdir('.download-', @root.join('entries')) do |directory|
|
|
62
|
+
artifact = acquire.call(directory)
|
|
63
|
+
validate!(artifact, request, pin)
|
|
64
|
+
publish(key, artifact, request)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def load(key, request, pin)
|
|
69
|
+
reference = @root.join('refs', "#{key}.json")
|
|
70
|
+
return unless reference.exist? || reference.symlink?
|
|
71
|
+
|
|
72
|
+
owned!(reference)
|
|
73
|
+
artifact = read_artifact(reference, request)
|
|
74
|
+
validate!(artifact, request, pin)
|
|
75
|
+
artifact
|
|
76
|
+
rescue JSON::ParserError, KeyError, Errno::ENOENT, CacheCorruption
|
|
77
|
+
reference.delete if reference.exist?
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def read_artifact(reference, request)
|
|
82
|
+
entry = read_entry(reference)
|
|
83
|
+
directory = @root.join('entries', entry)
|
|
84
|
+
owned!(directory, directory: true)
|
|
85
|
+
metadata = directory.join('metadata.json')
|
|
86
|
+
owned!(metadata)
|
|
87
|
+
path = directory.join("package.#{request.fetch('format')}")
|
|
88
|
+
owned!(path)
|
|
89
|
+
Artifact.new(path: path, metadata: JSON.parse(metadata.read))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def read_entry(reference)
|
|
93
|
+
data = JSON.parse(reference.read)
|
|
94
|
+
raise CacheCorruption, 'Invalid agent cache reference' unless data.is_a?(Hash)
|
|
95
|
+
|
|
96
|
+
entry = data.fetch('entry')
|
|
97
|
+
unless entry.is_a?(String) && entry.match?(/\A[a-f0-9]{64}\z/)
|
|
98
|
+
raise CacheCorruption,
|
|
99
|
+
'Invalid agent cache reference'
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
entry
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def validate!(artifact, request, pin)
|
|
106
|
+
metadata = artifact.metadata
|
|
107
|
+
valid = artifact.valid_identity?(request) && valid_file?(artifact, request.fetch('format'))
|
|
108
|
+
valid &&= metadata['sha256'] == pin if pin
|
|
109
|
+
raise CacheCorruption, 'Agent cache artifact or package identity is damaged' unless valid
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def valid_file?(artifact, format)
|
|
113
|
+
path = artifact.path
|
|
114
|
+
return false unless path.file? && path.size.positive? && path.size == artifact.metadata['size']
|
|
115
|
+
return false unless Digest::SHA256.file(path).hexdigest == artifact.metadata['sha256']
|
|
116
|
+
|
|
117
|
+
magic = File.binread(path, 8)
|
|
118
|
+
format == 'deb' ? magic == "!<arch>\n" : magic.start_with?("\xed\xab\xee\xdb".b)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Agent
|
|
5
|
+
# Filesystem ownership, stable locks and atomic publication for the package cache.
|
|
6
|
+
module CacheStorage
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def prepare
|
|
10
|
+
FileUtils.mkdir_p(@root.dirname, mode: 0o700)
|
|
11
|
+
[@root, @root.join('entries'), @root.join('refs'), @root.join('locks')].each do |path|
|
|
12
|
+
begin
|
|
13
|
+
Dir.mkdir(path, 0o700)
|
|
14
|
+
rescue Errno::EEXIST
|
|
15
|
+
# Concurrent creation is allowed only after ownership verification.
|
|
16
|
+
end
|
|
17
|
+
owned!(path, directory: true)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def owned!(path, directory: false)
|
|
22
|
+
stat = path.lstat
|
|
23
|
+
valid = stat.uid == Process.uid && !stat.symlink? && stat.mode.nobits?(0o077)
|
|
24
|
+
valid &&= directory ? stat.directory? : stat.file? && stat.nlink == 1
|
|
25
|
+
raise Error, 'Agent cache contains an unsafe or foreign entry; refusing access' unless valid
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def lock(key)
|
|
29
|
+
path = @root.join('locks', key)
|
|
30
|
+
File.open(path, File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |file|
|
|
31
|
+
owned!(path)
|
|
32
|
+
file.flock(File::LOCK_EX)
|
|
33
|
+
yield
|
|
34
|
+
ensure
|
|
35
|
+
file.flock(File::LOCK_UN)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def publish(key, artifact, request)
|
|
40
|
+
entry = fingerprint(request.merge('native_version' => artifact.metadata.fetch('version')))
|
|
41
|
+
metadata = artifact.metadata.merge('request' => request)
|
|
42
|
+
final = commit_entry(entry, artifact, metadata, request)
|
|
43
|
+
write_reference(key, entry)
|
|
44
|
+
Artifact.new(path: final.join("package.#{request.fetch('format')}"), metadata: metadata)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def commit_entry(entry, artifact, metadata, request)
|
|
48
|
+
staging = stage_entry(artifact, metadata, request)
|
|
49
|
+
final = @root.join('entries', entry)
|
|
50
|
+
remove_owned_entry(final) if final.exist?
|
|
51
|
+
File.rename(staging, final)
|
|
52
|
+
final
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def stage_entry(artifact, metadata, request)
|
|
56
|
+
staging = Pathname(File.dirname(artifact.path)).join('entry')
|
|
57
|
+
Dir.mkdir(staging, 0o700)
|
|
58
|
+
File.rename(artifact.path, staging.join("package.#{request.fetch('format')}"))
|
|
59
|
+
File.write(staging.join('metadata.json'), JSON.generate(metadata), perm: 0o600)
|
|
60
|
+
staging.children.each { |path| File.open(path, 'r+', &:fsync) }
|
|
61
|
+
staging
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def remove_owned_entry(directory)
|
|
65
|
+
owned!(directory, directory: true)
|
|
66
|
+
directory.children.each { |child| owned!(child) }
|
|
67
|
+
FileUtils.remove_entry(directory)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def write_reference(key, entry)
|
|
71
|
+
reference = @root.join('refs', "#{key}.json")
|
|
72
|
+
Tempfile.create(['.ref-', '.json'], reference.dirname) do |file|
|
|
73
|
+
file.chmod(0o600)
|
|
74
|
+
file.write(JSON.generate('entry' => entry))
|
|
75
|
+
file.flush
|
|
76
|
+
file.fsync
|
|
77
|
+
File.rename(file.path, reference)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'timeout'
|
|
7
|
+
|
|
8
|
+
module Empeira
|
|
9
|
+
module Agent
|
|
10
|
+
# Host HTTPS transport with bounded diagnostics and acquisition-scoped authentication.
|
|
11
|
+
class Download
|
|
12
|
+
HTTP_OPTIONS = { use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
|
13
|
+
open_timeout: 10, read_timeout: 30, write_timeout: 30 }.freeze
|
|
14
|
+
FAILURE_HINTS = { '403' => 'access denied', '407' => 'Host proxy authentication failed',
|
|
15
|
+
'404' => 'artifact unavailable' }.freeze
|
|
16
|
+
def initialize(authentication:)
|
|
17
|
+
@authentication = authentication
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fetch(url, path, sha256: nil, operation: 'Download agent package')
|
|
21
|
+
@authentication.attempt do
|
|
22
|
+
File.open(path, 'wb', 0o600) do |file|
|
|
23
|
+
request(url, operation) do |response|
|
|
24
|
+
write_response(file, response)
|
|
25
|
+
end
|
|
26
|
+
file.flush
|
|
27
|
+
file.fsync
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
raise Error, 'Agent artifact download is empty' if File.empty?(path)
|
|
31
|
+
raise Error, 'Agent artifact checksum mismatch' if sha256 && Digest::SHA256.file(path).hexdigest != sha256
|
|
32
|
+
rescue StandardError, Interrupt
|
|
33
|
+
FileUtils.rm_f(path)
|
|
34
|
+
raise
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def write_response(file, response)
|
|
40
|
+
response.read_body { |part| file.write(part) }
|
|
41
|
+
length = response['Content-Length']
|
|
42
|
+
raise Error, 'Agent artifact download is incomplete' if length && file.size != Integer(length)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def request(url, operation)
|
|
46
|
+
received = nil
|
|
47
|
+
uri = URI(url)
|
|
48
|
+
Configuration::AgentSchema.https_url!(url, 'agent artifact URL')
|
|
49
|
+
Timeout.timeout(180) do
|
|
50
|
+
Net::HTTP.start(uri.host, uri.port, **HTTP_OPTIONS) do |http|
|
|
51
|
+
http.request(http_request(uri)) do |response|
|
|
52
|
+
received = response
|
|
53
|
+
yield validate_response!(response, url, operation)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
rescue SocketError, OpenSSL::SSL::SSLError, IOError, SystemCallError, Timeout::Error, Net::ProtocolError => e
|
|
58
|
+
detail = Execution::Diagnostics.transport(operation: operation, url: url, error: e,
|
|
59
|
+
response: received, secrets: @authentication.credentials || [])
|
|
60
|
+
raise Error, "Agent HTTPS download failed\n#{detail}", cause: nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def http_request(uri)
|
|
64
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
65
|
+
request['Accept-Encoding'] = 'identity'
|
|
66
|
+
credentials = @authentication.credentials(uri.to_s)
|
|
67
|
+
request.basic_auth(*credentials) if credentials
|
|
68
|
+
request
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_response!(response, url, operation)
|
|
72
|
+
return response if response.code == '200'
|
|
73
|
+
|
|
74
|
+
detail = Execution::Diagnostics.http_response(response, operation: operation, url: url,
|
|
75
|
+
secrets: @authentication.credentials || [])
|
|
76
|
+
raise AuthenticationRequired.new(detail, url: url) if response.code == '401'
|
|
77
|
+
|
|
78
|
+
fallback = response.code.start_with?('3') ? 'redirects are not followed' : 'HTTP error response'
|
|
79
|
+
hint = FAILURE_HINTS.fetch(response.code, fallback)
|
|
80
|
+
raise Error, "Agent HTTPS request failed: #{hint}\n#{detail}", cause: nil
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Agent
|
|
5
|
+
# Shared helper execution, scoped authentication, artifact delivery and release ownership.
|
|
6
|
+
module NativeCommands
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def download_artifact(artifact, filename)
|
|
10
|
+
path = File.join(@directory, filename)
|
|
11
|
+
operation = filename == 'signing-key' ? 'Download agent signing key' : 'Download agent release package'
|
|
12
|
+
@download.fetch(artifact.fetch('url'), path, sha256: artifact['sha256'], operation: operation)
|
|
13
|
+
path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def install_auth
|
|
17
|
+
credentials = @authentication.credentials
|
|
18
|
+
return unless credentials
|
|
19
|
+
|
|
20
|
+
uri = URI(@source.fetch('url'))
|
|
21
|
+
endpoint = "https://#{uri.host}#{":#{uri.port}" unless uri.port == 443}"
|
|
22
|
+
# APT's netrc parser treats quotes and backslashes as literal credential bytes.
|
|
23
|
+
content = "machine #{endpoint}\nlogin #{credentials.first}\npassword #{credentials.last}\n"
|
|
24
|
+
copy_text(content, self.class::AUTH_PATH, mode: '0600')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def native!(arguments, operation)
|
|
28
|
+
@authentication.attempt do
|
|
29
|
+
install_auth
|
|
30
|
+
execute!(arguments, operation)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def execute!(arguments, operation)
|
|
35
|
+
result = @execute.call(arguments)
|
|
36
|
+
return result if result.success?
|
|
37
|
+
|
|
38
|
+
text = @authentication.redact("#{result.stderr}\n#{result.stdout}")
|
|
39
|
+
diagnostic = Execution::Diagnostics.native(result.with(stderr: text, stdout: ''),
|
|
40
|
+
operation: "Agent #{operation}", tool: arguments.first)
|
|
41
|
+
case text
|
|
42
|
+
when /407\s+Proxy Authentication Required|(?:HTTP|Status code|HTTP code)\s*:?\s*407\b/i
|
|
43
|
+
raise Error, "Host helper proxy authentication failed\n#{diagnostic}", cause: nil
|
|
44
|
+
when /401\s+Unauthorized|(?:HTTP|Status code|HTTP code)\s*:?\s*401\b/i
|
|
45
|
+
raise Agent::AuthenticationRequired, diagnostic, cause: nil
|
|
46
|
+
when /403\s+Forbidden|(?:HTTP|Status code|HTTP code)\s*:?\s*403\b/i
|
|
47
|
+
raise Error, "Agent repository access denied\n#{diagnostic}", cause: nil
|
|
48
|
+
end
|
|
49
|
+
raise Error, "Agent #{operation} failed\n#{diagnostic}", cause: nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def copy_text(content, destination, mode:)
|
|
53
|
+
Node::PackageProxy.copy_text(@copy, content, destination, mode: mode)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def require_absent_release!(name)
|
|
57
|
+
Configuration::AgentSchema.package_token!(name, 'Agent release package name')
|
|
58
|
+
return unless installed_release_packages.include?(name)
|
|
59
|
+
|
|
60
|
+
raise Error, 'Agent release package already exists in the helper; refusing to replace or remove it'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def installed_release_packages
|
|
64
|
+
execute!(release_inventory_arguments, 'release-package ownership verification').stdout.lines.map(&:strip)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def release_inventory_arguments
|
|
68
|
+
['dpkg-query', '-W', '-f=${Package}\n']
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def release_removal_arguments
|
|
72
|
+
['dpkg', '--purge', '--', @release_package]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def remove_release_package
|
|
76
|
+
return unless installed_release_packages.include?(@release_package)
|
|
77
|
+
|
|
78
|
+
execute!(release_removal_arguments, 'release package cleanup')
|
|
79
|
+
raise Error, 'Cannot verify agent helper release package cleanup' if
|
|
80
|
+
installed_release_packages.include?(@release_package)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Agent
|
|
5
|
+
# Apply the selected-source policy to native source files supplied by a release package.
|
|
6
|
+
module ReleaseSources
|
|
7
|
+
DNF_SCOPED_OPTIONS = %w[gpgcheck sslverify skip_if_unavailable includepkgs username password
|
|
8
|
+
mirrorlist metalink].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def apt(content, format:, source:)
|
|
13
|
+
urls = content.scan(%r{[a-z][a-z0-9+.-]*://[^\s]+}i)
|
|
14
|
+
raise Error, 'Agent release package must provide HTTPS APT sources' if urls.empty?
|
|
15
|
+
|
|
16
|
+
urls.each { |url| Configuration::AgentSchema.https_url!(url, 'agent release source URL') }
|
|
17
|
+
trusted = source['verify_signatures'] == false
|
|
18
|
+
format == '.sources' ? policy_deb822(content, trusted) : policy_list(content, trusted)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def policy_deb822(content, trusted)
|
|
22
|
+
content.split(/\n\s*\n/).map do |stanza|
|
|
23
|
+
safe = stanza.gsub(/^(?:Trusted|Allow-Insecure|Allow-Weak|Allow-Downgrade-To-Insecure):.*\n?/i, '').rstrip
|
|
24
|
+
"#{safe}\n#{"Trusted: yes\n" if trusted}"
|
|
25
|
+
end.join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def policy_list(content, trusted)
|
|
29
|
+
content.gsub(/^(\s*deb(?:-src)?\s+)(?:\[([^\]]*)\]\s+)?/) do
|
|
30
|
+
prefix = Regexp.last_match(1)
|
|
31
|
+
options = Regexp.last_match(2).to_s.gsub(/\b(?:trusted|allow-[a-z-]+)=\S+/, '').strip
|
|
32
|
+
options = [options, ('trusted=yes' if trusted)].compact.reject(&:empty?).join(' ')
|
|
33
|
+
"#{prefix}#{"[#{options}] " unless options.empty?}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def dnf(content, target:, source:, package:)
|
|
38
|
+
sections = content.split(/(?=^\[)/)
|
|
39
|
+
sections.map do |section|
|
|
40
|
+
section.start_with?('[') ? dnf_section(section, target, source, package) : section
|
|
41
|
+
end.join
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def scoped_dnf(section, source)
|
|
45
|
+
section = section.gsub(/^\s*(?:#{DNF_SCOPED_OPTIONS.join('|')})\s*=.*\n?/, '')
|
|
46
|
+
if source['verify_signatures'] == false
|
|
47
|
+
section = section.gsub(/^\s*repo_gpgcheck\s*=.*\n?/, '')
|
|
48
|
+
section = "#{section.rstrip}\nrepo_gpgcheck=0\n"
|
|
49
|
+
end
|
|
50
|
+
section
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def dnf_section(section, target, source, package)
|
|
54
|
+
urls = section.scan(/^\s*baseurl\s*=\s*(.+)$/).flatten.flat_map(&:split)
|
|
55
|
+
raise Error, 'Agent release package must provide explicit HTTPS DNF base URLs' if urls.empty?
|
|
56
|
+
|
|
57
|
+
urls.each { |url| Configuration::AgentSchema.https_url!(target.expand(url), 'agent release source URL') }
|
|
58
|
+
section = scoped_dnf(section, source)
|
|
59
|
+
"#{section.rstrip}\ngpgcheck=#{source['verify_signatures'] == false ? 0 : 1}\n" \
|
|
60
|
+
"sslverify=1\nskip_if_unavailable=0\nincludepkgs=#{package}\n"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Agent
|
|
5
|
+
class Target < Data.define(:os, :release, :architecture)
|
|
6
|
+
SUITES = { '22.04' => 'jammy', '24.04' => 'noble' }.freeze
|
|
7
|
+
ARCHITECTURES = { 'amd64' => 'x86_64', 'arm64' => 'aarch64' }.freeze
|
|
8
|
+
|
|
9
|
+
def family
|
|
10
|
+
Node::PackageBootstrap::FAMILIES.fetch(os)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def format
|
|
14
|
+
family == 'debian' ? 'deb' : 'rpm'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def native_architecture
|
|
18
|
+
family == 'debian' ? architecture : ARCHITECTURES.fetch(architecture)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def key
|
|
22
|
+
family == 'debian' ? "#{os}#{release}" : "el#{release}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def expand(value)
|
|
26
|
+
values = { 'suite' => SUITES[release], 'releasever' => release, 'basearch' => native_architecture }
|
|
27
|
+
value.gsub(/\$(?:\{([^}]+)\}|([a-zA-Z_][a-zA-Z_0-9]*))/) do
|
|
28
|
+
values.fetch(Regexp.last_match(1) || Regexp.last_match(2)) do
|
|
29
|
+
raise ConfigurationError, 'Unknown agent repository placeholder'
|
|
30
|
+
end || raise(ConfigurationError, 'Agent repository suite is unavailable for this target')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def source(install)
|
|
35
|
+
return package_source(install) if install.fetch('method') == 'package'
|
|
36
|
+
|
|
37
|
+
manager = family == 'debian' ? 'apt' : 'dnf'
|
|
38
|
+
sources = install.fetch(manager)
|
|
39
|
+
return install.fetch('repositories').fetch(key) unless configured?(sources)
|
|
40
|
+
|
|
41
|
+
resolved = Configuration::Merge.call(sources.fetch('default', {}), sources.fetch(key, {}),
|
|
42
|
+
path: ['agent', 'install', manager, key])
|
|
43
|
+
expand_source(resolved)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def package_source(install)
|
|
49
|
+
install.fetch('packages').fetch(key).fetch(architecture)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def configured?(sources)
|
|
53
|
+
sources.key?('default') || sources.key?(key)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def expand_source(source)
|
|
57
|
+
resolved = source.transform_values { |value| value.is_a?(String) ? expand(value) : value }
|
|
58
|
+
if family == 'debian'
|
|
59
|
+
resolved['suite'] ||= SUITES.fetch(release)
|
|
60
|
+
resolved['component'] ||= 'main'
|
|
61
|
+
end
|
|
62
|
+
resolved
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Agent
|
|
5
|
+
# Exact identity matching only; ordering remains the native package manager's responsibility.
|
|
6
|
+
module Versions
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def match?(native, requested, suffix: nil)
|
|
10
|
+
return native == "#{requested}#{suffix}" if suffix
|
|
11
|
+
return true if native == requested
|
|
12
|
+
return false if requested.include?('-') || requested.include?(':')
|
|
13
|
+
|
|
14
|
+
native.split(':').last.split('-', 2).first == requested
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def select(candidates, requested, suffix: nil)
|
|
18
|
+
matches = candidates.select { |candidate| match?(candidate.fetch('version'), requested, suffix: suffix) }.uniq
|
|
19
|
+
raise Error, 'Requested agent version is unavailable in the selected source' if matches.empty?
|
|
20
|
+
if matches.size > 1
|
|
21
|
+
raise Error,
|
|
22
|
+
'Requested agent version is ambiguous; specify the full native version or suffix'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
matches.first
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
class Application
|
|
5
|
+
Context = Data.define(:project, :workspace, :configuration, :platform, :locations) do
|
|
6
|
+
def container_engine
|
|
7
|
+
configuration.fetch('runtime').fetch('container_engine')
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :context, :runner
|
|
12
|
+
|
|
13
|
+
# Dependencies are explicit at this single composition boundary.
|
|
14
|
+
# rubocop:disable Metrics/ParameterLists
|
|
15
|
+
def initialize(project_path: Dir.pwd, overrides: {}, platform: Platform::Facts.new, runner: nil,
|
|
16
|
+
factories: {}, locations: nil, build_info: BuildInfo.load, progress: Progress.new,
|
|
17
|
+
recovery: false, recovery_path: nil)
|
|
18
|
+
@runner = runner || Execution::Runner.new(platform: platform)
|
|
19
|
+
locations ||= Platform::Locations.new(facts: platform)
|
|
20
|
+
if recovery
|
|
21
|
+
@context = recovery_context(project_path, recovery_path, platform, locations)
|
|
22
|
+
else
|
|
23
|
+
raise ConfigurationError, 'Explicit recovery project paths are only available for destroy' if recovery_path
|
|
24
|
+
|
|
25
|
+
project = ProjectRoot.resolve(project_path, runner: @runner)
|
|
26
|
+
@context = build_context(project:, locations:, platform:, overrides:)
|
|
27
|
+
end
|
|
28
|
+
@progress = progress
|
|
29
|
+
@build_info = build_info
|
|
30
|
+
@factories = { providers: Node.registry, runtimes: Runtime.registry,
|
|
31
|
+
engines: VM.registry }.merge(factories).freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# rubocop:enable Metrics/ParameterLists
|
|
35
|
+
|
|
36
|
+
def infrastructure
|
|
37
|
+
Infrastructure::Service.new(context: context, runner: runner, runtimes: @factories.fetch(:runtimes),
|
|
38
|
+
build_info: @build_info, progress: @progress)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def updates
|
|
42
|
+
Updates::Service.new(context: context, runner: runner, runtimes: @factories.fetch(:runtimes),
|
|
43
|
+
build_info: @build_info, progress: @progress)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def addons
|
|
47
|
+
Addons::Service.new(context: context, runner: runner)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def nodes
|
|
51
|
+
Node::Service.new(context: context, runner: runner, **@factories, build_info: @build_info, progress: @progress,
|
|
52
|
+
ssh_preferences: @ssh_preferences)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def run_node(hostname:, provider:)
|
|
56
|
+
request = Node::RunRequest.from_config(hostname: hostname, provider: provider, config: context.configuration)
|
|
57
|
+
nodes.run(request)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# rubocop:disable-next Metrics/AbcSize -- Resolve the saved owner independently of live project configuration.
|
|
63
|
+
def recovery_context(project_path, recovery_path, platform, locations)
|
|
64
|
+
project = ProjectRoot.resolve(project_path, runner: @runner, recovery: true) unless recovery_path
|
|
65
|
+
workspace = recovery_path ? Workspace.recover(recovery_path) : Workspace.new(path: project)
|
|
66
|
+
configuration = Configuration::Document.new.read(Configuration::Loader::DEFAULTS)
|
|
67
|
+
Configuration::Schema.new.validate_effective!(configuration)
|
|
68
|
+
context = Context.new(project: project, workspace: workspace, configuration: configuration,
|
|
69
|
+
platform: platform, locations: locations)
|
|
70
|
+
state = Infrastructure::Store.new(context: context, recovery: true).load
|
|
71
|
+
configuration.fetch('runtime')['container_engine'] = state.fetch('runtime') if state
|
|
72
|
+
@ssh_preferences = {}.freeze
|
|
73
|
+
Context.new(project: project, workspace: workspace, configuration: Immutable.deep_freeze(configuration),
|
|
74
|
+
platform: platform, locations: locations)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_context(project:, locations:, platform:, overrides:)
|
|
78
|
+
loader = Configuration::Loader.new(project_path: project, locations: locations)
|
|
79
|
+
configuration = loader.load(overrides: overrides)
|
|
80
|
+
@ssh_preferences = loader.ssh_preferences
|
|
81
|
+
workspace = Workspace.new(path: project)
|
|
82
|
+
Context.new(project: project, workspace: workspace, configuration: Immutable.deep_freeze(configuration),
|
|
83
|
+
platform: platform, locations: locations)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'version_requirement'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
class BuildInfo
|
|
8
|
+
METADATA_PATH = File.expand_path('build.json', __dir__).freeze
|
|
9
|
+
attr_reader :version, :revision, :build_time
|
|
10
|
+
|
|
11
|
+
def self.load(path: METADATA_PATH)
|
|
12
|
+
data = File.file?(path) ? JSON.parse(File.read(path)) : {}
|
|
13
|
+
new(**data.transform_keys(&:to_sym))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(version: 'development', revision: nil, build_time: nil)
|
|
17
|
+
@version = version.delete_prefix('v').freeze
|
|
18
|
+
@revision = revision&.dup&.freeze
|
|
19
|
+
@build_time = build_time&.dup&.freeze
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def gem_version
|
|
24
|
+
version == 'development' ? '0.0.0.development' : version
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def requirement_status(requirement)
|
|
28
|
+
parsed = VersionRequirement.parse(requirement)
|
|
29
|
+
return 'not required' unless parsed
|
|
30
|
+
return 'unverifiable (development build)' if version == 'development'
|
|
31
|
+
|
|
32
|
+
parsed.satisfied_by?(Gem::Version.new(version)) ? 'satisfied' : 'incompatible'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def require_compatible!(requirement)
|
|
36
|
+
return if ['not required', 'satisfied'].include?(requirement_status(requirement))
|
|
37
|
+
|
|
38
|
+
raise Error, "Empeira version requirement is not satisfied.\nRequired: #{requirement}\nInstalled: #{version}\n" \
|
|
39
|
+
"Update Empeira before modifying this project.\nRun: empeira self-update"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_h
|
|
43
|
+
{ version: version, revision: revision, build_time: build_time }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|