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,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
# rubocop:disable-next Metrics/ModuleLength -- Shared native ownership and snapshot validation boundary.
|
|
6
|
+
module ServiceInspection
|
|
7
|
+
def inspect_services(definitions, expected_ids: {})
|
|
8
|
+
selected = select_services(definitions, expected_ids)
|
|
9
|
+
ids = selected.values.compact.map { |entry| entry.fetch('id') }
|
|
10
|
+
resources = inspect_containers(ids)
|
|
11
|
+
definitions.to_h do |key, definition|
|
|
12
|
+
resource = resources[selected[key]&.fetch('id')]
|
|
13
|
+
definition.verify!(resource, expected_id: expected_ids[key])
|
|
14
|
+
[key, resource]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def inspect_volumes(definitions, expected_ids: {})
|
|
19
|
+
resources = inspect_volumes_by_name(select_volume_names(definitions))
|
|
20
|
+
definitions.to_h do |key, definition|
|
|
21
|
+
resource = resources[definition.name]
|
|
22
|
+
definition.verify!(resource, expected_id: expected_ids[key])
|
|
23
|
+
[key, resource]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def select_volume_names(definitions)
|
|
30
|
+
names = definitions.values.map(&:name)
|
|
31
|
+
inventory('volume').filter_map { |entry| entry.fetch('name') if names.include?(entry.fetch('name')) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def inspect_volumes_by_name(names)
|
|
35
|
+
return {} if names.empty?
|
|
36
|
+
|
|
37
|
+
data = parse_json(service_command(['volume', 'inspect', *names], operation: 'volume snapshot').stdout)
|
|
38
|
+
validate_snapshot!(data, names.size)
|
|
39
|
+
resources = data.map { |entry| normalize_volume(entry) }
|
|
40
|
+
verify_volume_names!(resources, names)
|
|
41
|
+
resources.to_h { |entry| [entry.fetch('name'), entry] }
|
|
42
|
+
rescue KeyError, TypeError, NoMethodError
|
|
43
|
+
malformed!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def verify_volume_names!(resources, names)
|
|
47
|
+
malformed! unless resources.map { |entry| entry.fetch('name') }.sort == names.sort
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def select_services(definitions, expected_ids)
|
|
51
|
+
entries = inventory('container')
|
|
52
|
+
definitions.to_h do |key, definition|
|
|
53
|
+
entry = entries.find { |item| item.fetch('name') == definition.name }
|
|
54
|
+
verify_recorded_name!('container', entries, entry, expected_ids[key])
|
|
55
|
+
[key, entry]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def inspect_containers(ids)
|
|
60
|
+
return {} if ids.empty?
|
|
61
|
+
|
|
62
|
+
data = parse_json(service_command(['container', 'inspect', *ids], operation: 'service snapshot').stdout)
|
|
63
|
+
validate_snapshot!(data, ids.size)
|
|
64
|
+
resources = data.map { |entry| normalize_service('container', entry) }
|
|
65
|
+
verify_snapshot_ids!(resources, ids)
|
|
66
|
+
resources.to_h { |entry| [entry.fetch('id'), entry] }
|
|
67
|
+
rescue KeyError, TypeError, NoMethodError
|
|
68
|
+
malformed!
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_snapshot!(data, size)
|
|
72
|
+
malformed! unless data.is_a?(Array) && data.size == size && data.all?(Hash)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def verify_snapshot_ids!(resources, ids)
|
|
76
|
+
malformed! unless resources.map { |entry| entry['id'] }.sort == ids.sort
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def inspect_owned(kind, definition, expected_id:)
|
|
80
|
+
return inspect_named(kind, definition, expected_id: expected_id) unless kind == 'container' && expected_id
|
|
81
|
+
|
|
82
|
+
result = runner.run(name, arguments: ['container', 'inspect', expected_id], timeout: 30)
|
|
83
|
+
return inspect_named(kind, definition, expected_id: expected_id) unless result.success?
|
|
84
|
+
|
|
85
|
+
data = parse_json(result.stdout)
|
|
86
|
+
validate_snapshot!(data, 1)
|
|
87
|
+
resource = normalize_service(kind, data.first)
|
|
88
|
+
definition.verify!(resource, expected_id: expected_id)
|
|
89
|
+
resource
|
|
90
|
+
rescue KeyError, TypeError, NoMethodError
|
|
91
|
+
malformed!
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def inspect_named(kind, definition, expected_id:)
|
|
95
|
+
entries = inventory(kind)
|
|
96
|
+
entry = entries.find { |item| item.fetch('name') == definition.name }
|
|
97
|
+
verify_recorded_name!(kind, entries, entry, expected_id)
|
|
98
|
+
return unless entry
|
|
99
|
+
|
|
100
|
+
resource = normalize_service(kind, inspect_entry_data(kind, entry.fetch('id')))
|
|
101
|
+
definition.verify!(resource, expected_id: expected_id)
|
|
102
|
+
resource
|
|
103
|
+
rescue JSON::ParserError, KeyError, TypeError, NoMethodError
|
|
104
|
+
raise Providers::ExecutionError, 'Malformed service inventory', cause: nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def verify_recorded_name!(kind, entries, entry, expected_id)
|
|
108
|
+
return unless kind == 'container' && !entry && expected_id
|
|
109
|
+
return unless entries.any? { |item| item.fetch('id') == expected_id }
|
|
110
|
+
|
|
111
|
+
raise Providers::OwnershipError, "Recorded #{kind} exists under a different name"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def inspect_entry_data(kind, id)
|
|
115
|
+
data = JSON.parse(service_command([kind, 'inspect', id], operation: "#{kind} inspection").stdout)
|
|
116
|
+
unless data.is_a?(Array) && data.size == 1 && data.first.is_a?(Hash)
|
|
117
|
+
raise Providers::ExecutionError, 'Malformed service inspection'
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
data.first
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def inventory(kind)
|
|
124
|
+
args = kind == 'container' ? ['ps', '--all', '--no-trunc'] : %w[volume ls]
|
|
125
|
+
# Both CLIs support Go templates independently of their JSON inspection casing.
|
|
126
|
+
if kind == 'container'
|
|
127
|
+
output = service_command([*args, '--format', '{{.ID}} {{.Names}}'],
|
|
128
|
+
operation: 'service inventory')
|
|
129
|
+
end
|
|
130
|
+
output ||= service_command([*args, '--format', '{{.Name}}'], operation: 'volume inventory')
|
|
131
|
+
output.stdout.lines.reject { |line| line.strip.empty? }.map do |line|
|
|
132
|
+
id, item_name = line.strip.split(' ', 2)
|
|
133
|
+
{ 'id' => id, 'name' => item_name || id }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def normalize_volume(data)
|
|
138
|
+
{ 'id' => data.fetch('Labels', {}).fetch('io.empeira.volume-id', data.fetch('Name')),
|
|
139
|
+
'name' => data.fetch('Name'), 'labels' => data['Labels'] || {} }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def normalize_service(kind, data)
|
|
143
|
+
return normalize_volume(data) if kind == 'volume'
|
|
144
|
+
|
|
145
|
+
{ 'id' => data.fetch('Id'), 'name' => data.fetch('Name').delete_prefix('/'),
|
|
146
|
+
'labels' => data.fetch('Config')['Labels'] || {}, 'state' => data.fetch('State').fetch('Status'),
|
|
147
|
+
'networks' => data.fetch('NetworkSettings').fetch('Networks'),
|
|
148
|
+
'host_config' => data['HostConfig'], 'image_id' => data['Image'],
|
|
149
|
+
'mounts' => data.fetch('Mounts'), 'dns' => data.dig('HostConfig', 'Dns') || [],
|
|
150
|
+
**port_observations(data) }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def port_observations(data)
|
|
154
|
+
{ 'ports' => data.dig('HostConfig', 'PortBindings') || {},
|
|
155
|
+
'published_ports' => data.dig('NetworkSettings', 'Ports') || {} }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Runtime
|
|
7
|
+
# Shared service contract for Docker and Podman. No shell or Compose orchestration.
|
|
8
|
+
module Services
|
|
9
|
+
include ServiceInspection
|
|
10
|
+
include ServiceImages
|
|
11
|
+
include ImageMetadata
|
|
12
|
+
include RecipeBuild
|
|
13
|
+
include RegistryDiagnostics
|
|
14
|
+
|
|
15
|
+
def inspect_service(definition, expected_id: nil)
|
|
16
|
+
inspect_owned('container', definition, expected_id: expected_id)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inspect_volume(definition, expected_id: nil)
|
|
20
|
+
inspect_owned('volume', definition, expected_id: expected_id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_volume(definition)
|
|
24
|
+
service_command(['volume', 'create', *label_arguments(definition),
|
|
25
|
+
'--label', "io.empeira.volume-id=#{SecureRandom.uuid}",
|
|
26
|
+
definition.name], operation: 'volume creation')
|
|
27
|
+
inspect_volume(definition)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def remove_volume(definition, expected_id:)
|
|
31
|
+
resource = inspect_volume(definition, expected_id: expected_id)
|
|
32
|
+
return unless resource
|
|
33
|
+
|
|
34
|
+
service_command(['volume', 'rm', definition.name], operation: 'persistent volume removal')
|
|
35
|
+
raise Providers::ExecutionError, 'Volume removal could not be verified' if inspect_volume(definition)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_service(definition)
|
|
39
|
+
service_command(create_service_arguments(definition), operation: "#{definition.key} creation")
|
|
40
|
+
inspect_service(definition)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def start_service(resource)
|
|
44
|
+
service_command(['start', resource.fetch('id')], operation: 'service start')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# rubocop:disable-next Naming/PredicateMethod -- This mutation returns whether it changed infrastructure.
|
|
48
|
+
def remove_service(definition, expected_id: nil)
|
|
49
|
+
resource = inspect_service(definition, expected_id: expected_id)
|
|
50
|
+
return false unless resource
|
|
51
|
+
|
|
52
|
+
service_command(['stop', '--time', '20', resource.fetch('id')], operation: 'service stop', timeout: 40)
|
|
53
|
+
volumes = ['--volumes']
|
|
54
|
+
service_command(['rm', *volumes, resource.fetch('id')], operation: 'service removal')
|
|
55
|
+
raise Providers::ExecutionError, 'Service removal could not be verified' if inspect_service(definition)
|
|
56
|
+
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def reload_service(resource, signal: 'HUP')
|
|
61
|
+
service_command(['kill', '--signal', signal, resource.fetch('id')], operation: 'service reload')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def stop_service(resource)
|
|
65
|
+
service_command(['stop', '--time', '20', resource.fetch('id')], operation: 'node stop', timeout: 40)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ssh_proxy_command(resource, port: nil)
|
|
69
|
+
helper = Pathname(__dir__).join('../../../resources/nodes/ssh/proxy.rb').realpath
|
|
70
|
+
[RbConfig.ruby, helper.to_s, name, resource.fetch('id'), *(port ? [port.to_s] : [])]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def stream_service(resource, arguments, interactive: false, logs: false, user: nil)
|
|
74
|
+
command = if logs
|
|
75
|
+
['logs', '--follow', resource.fetch('id')]
|
|
76
|
+
else
|
|
77
|
+
['exec', *(user ? ['--user', user] : []), *(interactive ? ['--interactive'] : []),
|
|
78
|
+
*(interactive && $stdin.tty? && $stdout.tty? ? ['--tty'] : []), resource.fetch('id'), *arguments]
|
|
79
|
+
end
|
|
80
|
+
runner.stream(name, arguments: command)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def service_exec(resource, arguments, timeout: 15, on_stdout: nil)
|
|
84
|
+
options = on_stdout ? { on_stdout: on_stdout } : {}
|
|
85
|
+
runner.run(name, arguments: ['exec', resource.fetch('id'), *arguments], timeout: timeout, **options)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def copy_from(resource, source, destination)
|
|
89
|
+
service_command(['cp', "#{resource.fetch('id')}:#{source}", destination.to_s], operation: 'certificate export')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def copy_to(resource, source, destination)
|
|
93
|
+
service_command(['cp', source.to_s, "#{resource.fetch('id')}:#{destination}"],
|
|
94
|
+
operation: 'service configuration')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def attach_egress(definition, resource)
|
|
98
|
+
service_command(['network', 'connect', definition.backend_name, resource.fetch('id')],
|
|
99
|
+
operation: 'proxy egress attachment')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def service_command(arguments, operation:, timeout: 30)
|
|
105
|
+
result = runner.run(name, arguments: arguments, timeout: timeout)
|
|
106
|
+
return result if result.success?
|
|
107
|
+
|
|
108
|
+
raise Providers::ExecutionError,
|
|
109
|
+
"#{name} #{operation} failed; run empeira status\n" \
|
|
110
|
+
"#{Execution::Diagnostics.native(result, operation: operation, tool: name)}", cause: nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def create_service_arguments(definition)
|
|
114
|
+
ServiceArguments.new(definition).build
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Container.include(Services)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
# Disposable update tooling: no service plan, workspace networking or persistent inventory.
|
|
6
|
+
module UpdateHelper
|
|
7
|
+
PROXY_VARIABLES = %w[HTTP_PROXY HTTPS_PROXY NO_PROXY http_proxy https_proxy no_proxy].freeze
|
|
8
|
+
|
|
9
|
+
def with_update_helper(image:, input:, output:, sources:, cache:)
|
|
10
|
+
# Reuse the ownership verifier, but assign a unique identity for this invocation only.
|
|
11
|
+
identity = Empeira::Services::Definition.new(key: "update-#{SecureRandom.hex(16)}",
|
|
12
|
+
workspace: context.workspace)
|
|
13
|
+
update_command(helper_arguments(identity, image, input, output, sources, cache), operation: 'helper creation')
|
|
14
|
+
resource = inspect_service(identity)
|
|
15
|
+
raise Providers::ExecutionError, 'Update helper creation could not be verified' unless resource
|
|
16
|
+
|
|
17
|
+
update_command(['start', resource.fetch('id')], operation: 'helper start')
|
|
18
|
+
verify_helper_network!(identity, resource.fetch('id'))
|
|
19
|
+
yield resource
|
|
20
|
+
ensure
|
|
21
|
+
remove_update_helper(identity, resource&.fetch('id')) if identity
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update_command(arguments, operation:, timeout: 600, registry: nil)
|
|
25
|
+
result = runner.run(name, arguments: arguments, timeout: timeout)
|
|
26
|
+
return result if result.success?
|
|
27
|
+
|
|
28
|
+
diagnostic = Execution::Diagnostics.safe_text("#{result.stderr}\n#{result.stdout}")
|
|
29
|
+
guidance = if registry
|
|
30
|
+
registry_failure_hint(registry, diagnostic, timed_out: result.timed_out)
|
|
31
|
+
else
|
|
32
|
+
'Check your normal host/container network access.'
|
|
33
|
+
end
|
|
34
|
+
raise Providers::ExecutionError, "#{name} update #{operation} failed " \
|
|
35
|
+
"(#{result.timed_out ? 'timeout' : result.exit_status}). " \
|
|
36
|
+
"#{guidance}\n" \
|
|
37
|
+
"#{Execution::Diagnostics.native(result, operation: operation, tool: name)}",
|
|
38
|
+
cause: nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# rubocop:disable-next Metrics/AbcSize -- Keep all explicit module-helper mounts and restrictions together.
|
|
44
|
+
def helper_arguments(identity, image, input, output, sources, cache)
|
|
45
|
+
mounts = %w[Puppetfile request.json].flat_map do |file|
|
|
46
|
+
['--mount', helper_mount(File.join(input, file), "/work/#{file}", readonly: true)]
|
|
47
|
+
end
|
|
48
|
+
environment = { 'HOME' => '/work', 'GIT_TERMINAL_PROMPT' => '0', 'GIT_ALLOW_PROTOCOL' => 'file' }
|
|
49
|
+
['create', '--name', identity.name, '--network', update_helper_network, *update_helper_user,
|
|
50
|
+
'--cap-drop', 'ALL', '--security-opt', 'no-new-privileges', *label_arguments(identity),
|
|
51
|
+
*PROXY_VARIABLES.select { |key| ENV.key?(key) }.flat_map { |key| ['--env', key] },
|
|
52
|
+
*environment.flat_map { |key, value| ['--env', "#{key}=#{value}"] },
|
|
53
|
+
*mounts, '--mount', helper_mount(output, '/work/modules', readonly: false),
|
|
54
|
+
'--mount', helper_mount(sources, sources.to_s, readonly: true),
|
|
55
|
+
'--mount', helper_mount(cache, cache.to_s, readonly: false), image]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def update_helper_user
|
|
59
|
+
['--user', "#{Process.uid}:#{Process.gid}"]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def helper_mount(source, target, readonly:)
|
|
63
|
+
if source.to_s.match?(/[,\r\n]/)
|
|
64
|
+
raise ConfigurationError,
|
|
65
|
+
'Update mount paths must not contain commas or newlines'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
"type=bind,src=#{File.realpath(source)},dst=#{target}#{',readonly' if readonly}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def verify_helper_network!(identity, id)
|
|
72
|
+
resource = inspect_service(identity, expected_id: id)
|
|
73
|
+
raise Providers::ExecutionError, 'Update helper disappeared before synchronization' unless resource
|
|
74
|
+
|
|
75
|
+
networks = resource.fetch('networks').keys
|
|
76
|
+
return if networks == [update_helper_network] && resource.fetch('dns').empty? &&
|
|
77
|
+
resource.fetch('ports').empty? && resource.fetch('published_ports', {}).values.all?(&:nil?)
|
|
78
|
+
|
|
79
|
+
raise Providers::OwnershipError, 'Update helper has unexpected network, DNS or port configuration'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def update_helper_network
|
|
83
|
+
'bridge'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def remove_update_helper(identity, id)
|
|
87
|
+
resource = inspect_service(identity, expected_id: id)
|
|
88
|
+
return unless resource
|
|
89
|
+
|
|
90
|
+
update_command(['rm', '--force', '--volumes', resource.fetch('id')], operation: 'helper removal')
|
|
91
|
+
raise Providers::ExecutionError, 'Update helper removal could not be verified' if inspect_service(identity)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Container.include(UpdateHelper)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
module WorkspaceNetwork
|
|
6
|
+
# This privileged, short-lived operation runs in the engine's Linux host
|
|
7
|
+
# namespace, including Docker Desktop's VM. It does not share host files,
|
|
8
|
+
# sockets or PID namespaces, and cannot select arbitrary firewall rules.
|
|
9
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- Verify ownership before this narrow host-namespace transaction.
|
|
10
|
+
def reconcile_workspace_bridge(definition:, expected_id:, image:, action:)
|
|
11
|
+
return unless name == 'docker'
|
|
12
|
+
|
|
13
|
+
resource = inspect_network(identifier: definition.backend_name)
|
|
14
|
+
definition.verify_ownership!(resource, expected_id: expected_id)
|
|
15
|
+
raise Providers::OwnershipError, 'Workspace network is missing' unless resource
|
|
16
|
+
|
|
17
|
+
definition.verify_isolation!(resource)
|
|
18
|
+
details = network_details(resource.id)
|
|
19
|
+
bridge = "ep#{context.workspace.id[0, 10]}"
|
|
20
|
+
unless details.dig('Options', 'com.docker.network.bridge.name') == bridge &&
|
|
21
|
+
details.fetch('IPAM').fetch('Config').any? { |item| item['Subnet'] == definition.allocation }
|
|
22
|
+
raise Providers::OwnershipError, 'Workspace bridge or subnet conflicts with recorded allocation'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
helper = Empeira::Services::Definition.new(key: "bridge-#{SecureRandom.hex(16)}", workspace: context.workspace)
|
|
26
|
+
service_command(['run', '--rm', '--pull=never', '--name', helper.name, '--network', 'host',
|
|
27
|
+
'--cap-drop', 'ALL', '--cap-add', 'NET_ADMIN', '--cap-add', 'NET_RAW',
|
|
28
|
+
'--security-opt', 'no-new-privileges', '--read-only',
|
|
29
|
+
'--tmpfs', '/run:rw,noexec,nosuid,size=1m', *label_arguments(helper),
|
|
30
|
+
'--entrypoint', '/usr/local/libexec/empeira-bridge', image,
|
|
31
|
+
action, bridge, definition.allocation, resource.id],
|
|
32
|
+
operation: 'workspace bridge attachment', timeout: action == 'check' ? 3 : 30)
|
|
33
|
+
ensure
|
|
34
|
+
if helper && (remaining = inspect_service(helper))
|
|
35
|
+
service_command(['rm', '--force', remaining.fetch('id')], operation: 'bridge helper cleanup')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def detach_egress(definition, resource)
|
|
40
|
+
service_command(['network', 'disconnect', definition.backend_name, resource.fetch('id')],
|
|
41
|
+
operation: 'gateway uplink detachment')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Only the target's network namespace is shared. No host namespace, Docker
|
|
45
|
+
# socket, target filesystem or persistent NET_ADMIN capability is exposed.
|
|
46
|
+
# rubocop:disable-next Metrics/AbcSize -- Bound the privilege and cleanup of a single namespace operation.
|
|
47
|
+
def configure_workspace_route(resource, gateway:, image:)
|
|
48
|
+
labels = resource.fetch('labels')
|
|
49
|
+
unless labels['io.empeira.managed-by'] == 'empeira' && labels['io.empeira.workspace'] == context.workspace.id
|
|
50
|
+
raise Providers::OwnershipError, 'Cannot configure routing on a foreign container'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
helper = Empeira::Services::Definition.new(key: "route-#{SecureRandom.hex(16)}", workspace: context.workspace)
|
|
54
|
+
arguments = ['run', '--rm', '--pull=never', '--name', helper.name,
|
|
55
|
+
'--network', "container:#{resource.fetch('id')}", '--cap-drop', 'ALL', '--cap-add', 'NET_ADMIN',
|
|
56
|
+
'--security-opt', 'no-new-privileges', '--read-only',
|
|
57
|
+
'--tmpfs', '/run:rw,noexec,nosuid,size=1m', *label_arguments(helper),
|
|
58
|
+
'--entrypoint', Network::Gateway::EXECUTABLE, image, 'route', gateway]
|
|
59
|
+
service_command(arguments, operation: 'workspace default routing')
|
|
60
|
+
ensure
|
|
61
|
+
if helper && (remaining = inspect_service(helper))
|
|
62
|
+
service_command(['rm', '--force', remaining.fetch('id')], operation: 'route helper cleanup')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Container.include(WorkspaceNetwork)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Server
|
|
5
|
+
# Apply one native Puppet setting without replacing an image's configuration.
|
|
6
|
+
class EnvironmentConfiguration
|
|
7
|
+
PUPPET = '/opt/puppetlabs/bin/puppet'
|
|
8
|
+
READER = '/empeira-server/environment.rb'
|
|
9
|
+
|
|
10
|
+
def initialize(runtime:, plan:)
|
|
11
|
+
@runtime = runtime
|
|
12
|
+
@plan = plan
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# rubocop:disable-next Naming/PredicateMethod -- Reconciliation mutates the native server setting.
|
|
16
|
+
def reconcile(resource)
|
|
17
|
+
return false if current(resource) == '0'
|
|
18
|
+
|
|
19
|
+
execute(resource, [PUPPET, 'config', 'set', 'environment_timeout', '0', '--section', 'server',
|
|
20
|
+
'--confdir', confdir])
|
|
21
|
+
@runtime.stop_service(resource)
|
|
22
|
+
@runtime.start_service(resource)
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def verify!(resource)
|
|
27
|
+
return if current(resource) == '0'
|
|
28
|
+
|
|
29
|
+
raise Error, 'Puppetserver environment_timeout is not 0 after startup; the custom entrypoint or ' \
|
|
30
|
+
'server confdir overrides the managed setting; inspect the server runtime contract'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def current(resource)
|
|
36
|
+
execute(resource, [ControlPlane::Health::RUBY, READER, '--confdir', confdir]).stdout.strip
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def execute(resource, arguments)
|
|
40
|
+
result = @runtime.service_exec(resource, arguments)
|
|
41
|
+
return result if result.success?
|
|
42
|
+
|
|
43
|
+
details = Execution::Diagnostics.command(result, operation: 'Effective Puppetserver environment timeout',
|
|
44
|
+
tool: arguments.first)
|
|
45
|
+
raise Error, "Cannot reconcile Puppetserver environment_timeout\n#{details}", cause: nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def confdir
|
|
49
|
+
File.dirname(@plan.server_runtime.paths.fetch('puppetdb_config'))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Server
|
|
5
|
+
module HTTP
|
|
6
|
+
def self.arguments
|
|
7
|
+
['curl', '--silent', '--show-error', '--fail-with-body', '--max-time', '8', '--noproxy', '*']
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.tls_arguments
|
|
11
|
+
ssl = '/etc/puppetlabs/puppet/ssl'
|
|
12
|
+
[*arguments, '--cacert', "#{ssl}/certs/ca.pem", '--cert', "#{ssl}/certs/server.empeira.internal.pem",
|
|
13
|
+
'--key', "#{ssl}/private_keys/server.empeira.internal.pem"]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.response(result)
|
|
17
|
+
text = result.stdout
|
|
18
|
+
status = reason = type = nil
|
|
19
|
+
while (match = %r{\AHTTP/\S+ (\d{3})(?: ([^\r\n]*))?\r?\n((?:[^\r\n]+\r?\n)*)\r?\n}.match(text))
|
|
20
|
+
status = match[1]
|
|
21
|
+
reason = match[2]
|
|
22
|
+
type = match[3][/^Content-Type:\s*([^\r\n]+)/i, 1]
|
|
23
|
+
text = text[match[0].length..]
|
|
24
|
+
end
|
|
25
|
+
{ status: status, reason: reason, body: text, content_type: type }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.failure(result, url:, operation:)
|
|
29
|
+
data = response(result)
|
|
30
|
+
return Execution::Diagnostics.native(result, operation: operation, tool: 'curl', url: url) unless data[:status]
|
|
31
|
+
|
|
32
|
+
detail = Execution::Diagnostics.http(operation: operation, url: url, **data)
|
|
33
|
+
stderr = Execution::Diagnostics.bounded(Execution::Diagnostics.safe_text(result.stderr))
|
|
34
|
+
"#{detail}\n#{stderr}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Server
|
|
5
|
+
# OpenVoxDB process layout; custom images may override only these runtime details.
|
|
6
|
+
class PuppetDBRuntime
|
|
7
|
+
PATHS = %w[jar config bootstrap_config data].freeze
|
|
8
|
+
FIELDS = %w[entrypoint user java_arguments main_arguments paths].freeze
|
|
9
|
+
|
|
10
|
+
def self.validate!(value, path = 'puppetdb.runtime', effective: false)
|
|
11
|
+
raise ConfigurationError, "#{path} must be a mapping" unless value.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
value.each do |key, item|
|
|
14
|
+
validate_entry!(key, item, path)
|
|
15
|
+
end
|
|
16
|
+
return unless effective
|
|
17
|
+
|
|
18
|
+
missing = FIELDS - value.keys
|
|
19
|
+
raise ConfigurationError, "#{path}.#{missing.first} is required" unless missing.empty?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.validate_entry!(key, item, path)
|
|
23
|
+
raise ConfigurationError, "#{path}.#{key} is not supported" unless FIELDS.include?(key)
|
|
24
|
+
|
|
25
|
+
if key == 'paths'
|
|
26
|
+
validate_mapping!(item, PATHS, "#{path}.#{key}")
|
|
27
|
+
elsif !valid_item?(key, item)
|
|
28
|
+
raise ConfigurationError, "#{path}.#{key} is invalid"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.validate_mapping!(value, allowed, path)
|
|
33
|
+
raise ConfigurationError, "#{path} must be a mapping" unless value.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
value.each do |key, item|
|
|
36
|
+
raise ConfigurationError, "#{path}.#{key} is not supported" unless allowed.include?(key)
|
|
37
|
+
|
|
38
|
+
valid = Runtime.safe_path?(item)
|
|
39
|
+
raise ConfigurationError, "#{path}.#{key} is invalid" unless valid
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.valid_item?(key, item)
|
|
44
|
+
case key
|
|
45
|
+
when 'entrypoint' then Runtime.safe_path?(item)
|
|
46
|
+
when 'user' then item.is_a?(String) && item.match?(/\A[0-9]+\z/)
|
|
47
|
+
else Runtime.valid_arguments?(item)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.resolve(config)
|
|
52
|
+
definition = config.fetch('puppetdb').fetch('runtime')
|
|
53
|
+
validate!(definition, effective: true)
|
|
54
|
+
new(definition)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
attr_reader :entrypoint, :user, :paths
|
|
58
|
+
|
|
59
|
+
def initialize(definition)
|
|
60
|
+
@entrypoint = definition.fetch('entrypoint')
|
|
61
|
+
@user = definition.fetch('user')
|
|
62
|
+
@paths = definition.fetch('paths')
|
|
63
|
+
@java_arguments = definition.fetch('java_arguments')
|
|
64
|
+
@main_arguments = definition.fetch('main_arguments')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def command
|
|
68
|
+
[*@java_arguments, '-cp', paths.fetch('jar'), *@main_arguments,
|
|
69
|
+
'--config', paths.fetch('config'), '--bootstrap-config', paths.fetch('bootstrap_config'),
|
|
70
|
+
'--restart-file', "#{paths.fetch('data')}/restartcounter"]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|