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,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Configuration
|
|
7
|
+
# rubocop:disable-next Metrics/ClassLength -- Fragment validation and matching share one configuration contract.
|
|
8
|
+
class VMInterfaces
|
|
9
|
+
PARENT = 'empeira-vlan'
|
|
10
|
+
RESERVED = %w[lo eth0 peer].push(PARENT).freeze
|
|
11
|
+
UNSUITABLE = %w[0.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16
|
|
12
|
+
192.0.0.0/24 192.88.99.0/24 224.0.0.0/3].map { |value| IPAddr.new(value) }.freeze
|
|
13
|
+
|
|
14
|
+
def self.validate!(rules, path = 'vm.interfaces')
|
|
15
|
+
raise ConfigurationError, "#{path} must be an array" unless rules.is_a?(Array)
|
|
16
|
+
|
|
17
|
+
rules.each_with_index { |rule, index| validate_rule!(rule, "#{path}[#{index}]") }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.validate_rule!(rule, path)
|
|
21
|
+
unless rule.is_a?(Hash) && rule.keys.sort_by(&:to_s) == %w[devices hosts]
|
|
22
|
+
raise ConfigurationError, "#{path} requires only hosts and devices"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
hosts = rule.fetch('hosts')
|
|
26
|
+
raise ConfigurationError, "#{path}.hosts must contain hostname globs using * and ?" unless valid_hosts?(hosts)
|
|
27
|
+
|
|
28
|
+
validate_devices!(rule.fetch('devices'), path, hosts)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.valid_hosts?(hosts)
|
|
32
|
+
hosts.is_a?(Array) && !hosts.empty? && hosts.all? { |host| valid_pattern?(host) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.validate_devices!(devices, path, hosts)
|
|
36
|
+
unless devices.is_a?(Hash) && !devices.empty?
|
|
37
|
+
raise ConfigurationError, "#{path}.devices must be a nonempty mapping"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
devices.each do |name, definition|
|
|
41
|
+
device!(name, definition, "#{path}.devices.#{name}")
|
|
42
|
+
end
|
|
43
|
+
unique!(devices, path)
|
|
44
|
+
rescue ConfigurationError => e
|
|
45
|
+
raise ConfigurationError, "#{e.message} (hosts: #{hosts.join(', ')})", cause: nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.valid_pattern?(value)
|
|
49
|
+
value.is_a?(String) && Network::HostPolicy.valid_glob?(value.downcase)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.device!(name, definition, path)
|
|
53
|
+
unless valid_name?(name)
|
|
54
|
+
raise ConfigurationError, "#{path}: interface must use 1-15 ASCII letters, digits, _, . or -, " \
|
|
55
|
+
'start with a letter or digit, and not use a reserved Empeira name'
|
|
56
|
+
end
|
|
57
|
+
unless valid_definition_keys?(definition)
|
|
58
|
+
raise ConfigurationError, "#{path} requires network and permits only optional vlan_id"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
network!(definition.fetch('network'), "#{path}.network")
|
|
62
|
+
return unless definition.key?('vlan_id')
|
|
63
|
+
|
|
64
|
+
vlan!(definition['vlan_id'], path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.valid_definition_keys?(definition)
|
|
68
|
+
definition.is_a?(Hash) && definition.key?('network') && (definition.keys - %w[network vlan_id]).empty?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.vlan!(value, path)
|
|
72
|
+
return if value.is_a?(Integer) && value.between?(1, 4094)
|
|
73
|
+
|
|
74
|
+
raise ConfigurationError, "#{path}.vlan_id must be an integer from 1 through 4094"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.valid_name?(name)
|
|
78
|
+
name.is_a?(String) && name.bytesize.between?(1, 15) &&
|
|
79
|
+
name.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]*\z/) && !RESERVED.include?(name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.network!(value, path)
|
|
83
|
+
unless value.is_a?(String) && value.match?(%r{\A(?:\d{1,3}\.){3}\d{1,3}/(?:[0-9]|[12][0-9]|3[0-2])\z})
|
|
84
|
+
raise ConfigurationError, "#{path} must be a canonical IPv4 host address with CIDR prefix (IPv6 unsupported)"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
address = IPAddr.new(value.split('/').first)
|
|
88
|
+
subnet = IPAddr.new(value)
|
|
89
|
+
valid = address.ipv4? && address.to_s == value.split('/').first && usable_host?(address, subnet)
|
|
90
|
+
raise ConfigurationError, "#{path} must be a usable unicast IPv4 host address and subnet" unless valid
|
|
91
|
+
rescue IPAddr::InvalidAddressError
|
|
92
|
+
raise ConfigurationError, "#{path} must be a valid IPv4 host address with CIDR prefix", cause: nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.usable_host?(address, subnet)
|
|
96
|
+
UNSUITABLE.none? { |range| overlap?(range, subnet) } &&
|
|
97
|
+
(subnet.prefix >= 31 || ![subnet.to_range.first, subnet.to_range.last].include?(address))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.overlap?(left, right)
|
|
101
|
+
left.include?(right.to_range.first) || right.include?(left.to_range.first)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# One parent cannot carry two devices with the same VLAN ID.
|
|
105
|
+
def self.unique!(devices, path)
|
|
106
|
+
%w[network vlan_id].each { |key| unique_value!(devices, key, path) }
|
|
107
|
+
devices.to_a.combination(2) do |(left_name, left), (right_name, right)|
|
|
108
|
+
next unless overlap?(IPAddr.new(left.fetch('network')), IPAddr.new(right.fetch('network')))
|
|
109
|
+
|
|
110
|
+
raise ConfigurationError, "#{path}: interfaces #{left_name} and #{right_name} have overlapping networks"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.unique_value!(devices, key, path)
|
|
115
|
+
groups = devices.group_by { |_, definition| definition[key].to_s.split('/').first }
|
|
116
|
+
groups.each do |value, entries|
|
|
117
|
+
next if value.nil? || entries.size == 1
|
|
118
|
+
|
|
119
|
+
raise ConfigurationError, "#{path}: devices #{entries.map(&:first).join(', ')} have duplicate #{key} #{value}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.resolve(rules, hostname)
|
|
124
|
+
devices = {}
|
|
125
|
+
paths = {}
|
|
126
|
+
rules.each_with_index do |rule, index|
|
|
127
|
+
next unless rule.fetch('hosts').any? { |pattern| Network::HostPolicy.matches?(pattern, hostname) }
|
|
128
|
+
|
|
129
|
+
rule.fetch('devices').each do |name, definition|
|
|
130
|
+
path = "vm.interfaces[#{index}].devices.#{name}"
|
|
131
|
+
compatible!(devices, name, definition, hostname, path, paths[name])
|
|
132
|
+
devices[name] = definition
|
|
133
|
+
paths[name] ||= path
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
unique!(devices, "vm.interfaces (host #{hostname})")
|
|
137
|
+
devices
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.compatible!(devices, name, definition, hostname, path, previous)
|
|
141
|
+
return unless devices.key?(name) && devices[name] != definition
|
|
142
|
+
|
|
143
|
+
raise ConfigurationError, "Host #{hostname}, interface #{name}: #{path} conflicts with #{previous}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module ControlPlane
|
|
5
|
+
# Generated data stays outside service-wide directory mounts and control code.
|
|
6
|
+
class AdditionalConfigurations
|
|
7
|
+
attr_reader :directory
|
|
8
|
+
|
|
9
|
+
def initialize(context:)
|
|
10
|
+
@context = context
|
|
11
|
+
@workspace = context.locations.workspace(context.workspace)
|
|
12
|
+
@directory = @workspace.join('additional-configurations')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def path(entry)
|
|
16
|
+
directory.join("#{Digest::SHA256.hexdigest(entry.fetch('name'))}.yaml")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def options(entry, location:, mounts: [])
|
|
20
|
+
return {} unless entry.key?('configuration')
|
|
21
|
+
|
|
22
|
+
value = entry.fetch('configuration')
|
|
23
|
+
targets = mounts.map do |mount|
|
|
24
|
+
mount.split(',').find do |part|
|
|
25
|
+
part.start_with?('dst=')
|
|
26
|
+
end.to_s.delete_prefix('dst=')
|
|
27
|
+
end
|
|
28
|
+
Configuration::ServiceConfiguration.validate_targets!(value.fetch('target'), targets, location)
|
|
29
|
+
content = Configuration::ServiceConfiguration.serialize(value.fetch('content'), location)
|
|
30
|
+
{ configuration: content,
|
|
31
|
+
mounts: [*mounts, "type=bind,src=#{path(entry)},dst=#{value.fetch('target')},readonly"] }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def prepare
|
|
35
|
+
return if entries.empty?
|
|
36
|
+
|
|
37
|
+
verify_directory!
|
|
38
|
+
FileUtils.mkdir_p(directory, mode: 0o700)
|
|
39
|
+
entries.each { |entry| write(entry) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def prune
|
|
43
|
+
return unless directory.exist? || directory.symlink?
|
|
44
|
+
|
|
45
|
+
verify_directory!
|
|
46
|
+
|
|
47
|
+
expected = entries.map { |entry| path(entry) }
|
|
48
|
+
directory.children.each { |file| File.unlink(file) unless expected.include?(file) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def cleanup
|
|
52
|
+
return unless directory.exist? || directory.symlink?
|
|
53
|
+
|
|
54
|
+
verify_directory!
|
|
55
|
+
FileUtils.remove_entry_secure(directory) if directory.exist?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def entries
|
|
61
|
+
@context.configuration.dig('containers', 'additional').select { |entry| entry.key?('configuration') }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def verify_directory!
|
|
65
|
+
state = @context.locations.state
|
|
66
|
+
[state, @workspace.parent, @workspace, directory].each do |parent|
|
|
67
|
+
raise Error, 'Additional configuration directory must not be a symlink' if parent.symlink?
|
|
68
|
+
end
|
|
69
|
+
verify_external!
|
|
70
|
+
raise Error, 'Additional configuration mount source is unsafe' if directory.to_s.match?(/[,\r\n]/)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def verify_external!
|
|
74
|
+
ancestor = directory
|
|
75
|
+
ancestor = ancestor.parent until ancestor.exist?
|
|
76
|
+
project = @context.project.path.to_s
|
|
77
|
+
return unless ancestor.realpath.to_s == project || ancestor.realpath.to_s.start_with?("#{project}/")
|
|
78
|
+
|
|
79
|
+
raise Error, 'Additional configuration files must be outside the control repository'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def write(entry)
|
|
83
|
+
destination = path(entry)
|
|
84
|
+
if destination.symlink? || (destination.exist? && !destination.file?)
|
|
85
|
+
raise Error, 'Additional configuration file must be a regular file, not a symlink'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
content = Configuration::ServiceConfiguration.serialize(entry.dig('configuration', 'content'), 'configuration')
|
|
89
|
+
return if destination.file? && destination.binread == content
|
|
90
|
+
|
|
91
|
+
atomic_write(destination, content)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def atomic_write(destination, content)
|
|
95
|
+
Tempfile.create('configuration-', directory) do |file|
|
|
96
|
+
# The private workspace protects host access; arbitrary container UIDs need read access.
|
|
97
|
+
file.chmod(0o644)
|
|
98
|
+
file.write(content)
|
|
99
|
+
file.flush
|
|
100
|
+
file.fsync
|
|
101
|
+
File.rename(file.path, destination)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module ControlPlane
|
|
5
|
+
# A fixed TCP UI relay publishes HTTPS without giving Chromium an egress attachment.
|
|
6
|
+
class Browser
|
|
7
|
+
def initialize(plan)
|
|
8
|
+
@plan = plan
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def definitions(dns: nil)
|
|
12
|
+
{ 'browser' => browser(dns), 'browser-ui' => gateway(dns) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def browser(dns)
|
|
16
|
+
Services::Definition.new(key: 'browser', workspace: @plan.context.workspace,
|
|
17
|
+
hostname: @plan.naming.hostname('browser'), network: @plan.network, dns: dns,
|
|
18
|
+
image: Images::Configuration.reference(@plan.config.dig('browser', 'image'),
|
|
19
|
+
registry: @plan.config.dig('images',
|
|
20
|
+
'registry')),
|
|
21
|
+
memory: 2048, cpus: 2, shm_size: '1g',
|
|
22
|
+
mounts: ['type=tmpfs,dst=/config'],
|
|
23
|
+
environment: { 'CHROME_CLI' => @plan.config.dig('browser', 'start_url'),
|
|
24
|
+
'http_proxy' => '', 'https_proxy' => '', 'all_proxy' => '',
|
|
25
|
+
'HTTP_PROXY' => '', 'HTTPS_PROXY' => '', 'ALL_PROXY' => '' })
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# rubocop:disable-next Metrics/AbcSize -- Fixed relay options stay auditable together.
|
|
29
|
+
def gateway(dns)
|
|
30
|
+
relay = Pathname(__dir__).join('../../../resources/browser/relay.rb').realpath
|
|
31
|
+
access = Network::Egress.new(workspace: @plan.context.workspace, policy: Network::Policy.new)
|
|
32
|
+
Services::Definition.new(key: 'browser-ui', workspace: @plan.context.workspace,
|
|
33
|
+
hostname: @plan.naming.hostname('browser-ui'), network: access.backend_name, dns: dns,
|
|
34
|
+
**relay_image, memory: 96, cpus: 1,
|
|
35
|
+
entrypoint: 'ruby', command: ['/empeira-browser/relay.rb'],
|
|
36
|
+
configuration: File.read(relay),
|
|
37
|
+
sysctls: { 'net.ipv4.ip_forward' => '0' },
|
|
38
|
+
mounts: [@plan.bind(relay, '/empeira-browser/relay.rb'),
|
|
39
|
+
@plan.bind(@plan.files.path('browser-address'),
|
|
40
|
+
'/empeira-browser/address')],
|
|
41
|
+
ports: ['127.0.0.1::3001/tcp'])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def relay_image
|
|
45
|
+
Images::Configuration.artifact(
|
|
46
|
+
@plan.config.dig('images', 'relay'), purpose: 'relay', registry: @plan.config.dig('images', 'registry')
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# rubocop:disable-next Metrics/AbcSize -- Compare both configured and observed publications.
|
|
51
|
+
def self.valid_ports?(resource)
|
|
52
|
+
configured = resource.fetch('ports')
|
|
53
|
+
published = resource.fetch('published_ports', {}).compact
|
|
54
|
+
return false unless configured.keys == ['3001/tcp']
|
|
55
|
+
return false unless published.keys == ['3001/tcp'] || resource['state'] != 'running'
|
|
56
|
+
|
|
57
|
+
requested = configured['3001/tcp']
|
|
58
|
+
return false unless Node::SSHEndpoint.loopback_binding?(requested)
|
|
59
|
+
return true unless resource['state'] == 'running'
|
|
60
|
+
|
|
61
|
+
actual = published['3001/tcp']
|
|
62
|
+
Node::SSHEndpoint.binding?(actual, nil) &&
|
|
63
|
+
['', '0', actual.first['HostPort']].include?(requested.first['HostPort'])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.url(resource)
|
|
67
|
+
raise Providers::OwnershipError, 'Browser UI must publish only HTTPS on loopback' unless valid_ports?(resource)
|
|
68
|
+
|
|
69
|
+
"https://127.0.0.1:#{resource.fetch('published_ports').fetch('3001/tcp').first.fetch('HostPort')}/"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|