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,763 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module ControlPlane
|
|
5
|
+
# Called under the infrastructure store's mutation lock.
|
|
6
|
+
# Keep one explicit transaction coordinator for the ordered service lifecycle.
|
|
7
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
8
|
+
class Controller
|
|
9
|
+
def initialize(context:, runtime:, store:, runner: Execution::Runner.new, plan: nil, progress: Progress.new)
|
|
10
|
+
@progress = progress
|
|
11
|
+
@runner = runner
|
|
12
|
+
@context = context
|
|
13
|
+
@runtime = runtime
|
|
14
|
+
@store = store
|
|
15
|
+
@plan = plan || Plan.new(context: context)
|
|
16
|
+
@health = Health.new(runtime: runtime, plan: @plan)
|
|
17
|
+
logger = @runner.method(:debug) if @runner.respond_to?(:debug)
|
|
18
|
+
@direct_resolver = Network::DirectEgress::Resolver.new(logger: logger)
|
|
19
|
+
@observed = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- Dependency-ordered reconciliation shares one transaction.
|
|
23
|
+
def up
|
|
24
|
+
preflight unless @desired_images
|
|
25
|
+
load_state
|
|
26
|
+
check_network_architecture!
|
|
27
|
+
check_service_names!
|
|
28
|
+
@gateway_reconciling = true
|
|
29
|
+
capture_services
|
|
30
|
+
lockdown_existing_gateway
|
|
31
|
+
prepare_files
|
|
32
|
+
@progress.stage(20, 'Preparing persistent storage...')
|
|
33
|
+
prepare_storage
|
|
34
|
+
prepare_network_services
|
|
35
|
+
@plan.environment.prepare
|
|
36
|
+
@progress.stage(45, 'Starting PostgreSQL...')
|
|
37
|
+
prepare_postgres
|
|
38
|
+
@progress.stage(60, 'Starting configuration server...')
|
|
39
|
+
prepare_server
|
|
40
|
+
prepare_puppetdb
|
|
41
|
+
@plan.additional_services.each_key { |key| reconcile(key) }
|
|
42
|
+
activate_dns
|
|
43
|
+
prepare_browser
|
|
44
|
+
refresh_redirects
|
|
45
|
+
@progress.stage(95, 'Verifying control plane...')
|
|
46
|
+
finish_reconciliation
|
|
47
|
+
@changed
|
|
48
|
+
rescue StandardError
|
|
49
|
+
lockdown_existing_gateway if @gateway_reconciling
|
|
50
|
+
raise
|
|
51
|
+
ensure
|
|
52
|
+
@desired_images = nil
|
|
53
|
+
@service_snapshot = nil
|
|
54
|
+
@discovery_nodes = nil
|
|
55
|
+
@gateway_locked_id = nil
|
|
56
|
+
@gateway_reconciling = false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The browser lifecycle may change only the browser pair. Required workspace
|
|
60
|
+
# services and networks are inspected without being started or reconciled.
|
|
61
|
+
def browser
|
|
62
|
+
load_state
|
|
63
|
+
require_initialized_browser_workspace!
|
|
64
|
+
@plan.browser_enabled = true
|
|
65
|
+
check_service_names!
|
|
66
|
+
require_browser_access_network!
|
|
67
|
+
@dns = require_running_service!('dns')
|
|
68
|
+
definitions = Browser.new(@plan).definitions(dns: @dns)
|
|
69
|
+
check_images(definitions: definitions)
|
|
70
|
+
reconcile_browser('browser', definitions.fetch('browser'))
|
|
71
|
+
record_browser_address
|
|
72
|
+
reconcile_browser('browser-ui', definitions.fetch('browser-ui'))
|
|
73
|
+
browser_url
|
|
74
|
+
ensure
|
|
75
|
+
@desired_images = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def prepare_network_services
|
|
79
|
+
@progress.stage(30, 'Starting CoreDNS and gateways...')
|
|
80
|
+
ensure_egress
|
|
81
|
+
reconcile_bridge('apply')
|
|
82
|
+
reconcile_gateway
|
|
83
|
+
protect_existing_nodes
|
|
84
|
+
reconcile('dns')
|
|
85
|
+
@dns = @observed.fetch('dns').dig('networks', @plan.network, 'IPAddress')
|
|
86
|
+
raise Error, 'DNS service has no address in the isolated network' if @dns.nil? || @dns.empty?
|
|
87
|
+
|
|
88
|
+
@proxy_configuration_changed = @plan.files.bind_dns(@dns)
|
|
89
|
+
reconcile_proxy
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def reconcile_bridge(action)
|
|
93
|
+
definition = Network::Definition.new(workspace: @context.workspace, policy: Network::Policy.new)
|
|
94
|
+
definition.allocation = @plan.subnet
|
|
95
|
+
if action == 'apply'
|
|
96
|
+
@inventory['bridge_prepared'] = true
|
|
97
|
+
@inventory['bridge_image'] = @plan.gateway_artifact.fetch(:image)
|
|
98
|
+
save
|
|
99
|
+
end
|
|
100
|
+
network_id = @state.dig('resources', 'network', 'id')
|
|
101
|
+
@runtime.reconcile_workspace_bridge(definition: definition, expected_id: network_id,
|
|
102
|
+
image: @inventory.fetch('bridge_image'), action: action)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def remove_workspace_bridge
|
|
106
|
+
reconcile_bridge('remove') if @inventory['bridge_prepared']
|
|
107
|
+
@inventory.delete('bridge_prepared')
|
|
108
|
+
@inventory.delete('bridge_image')
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def protect_existing_nodes
|
|
112
|
+
return if @state.fetch('nodes', {}).empty?
|
|
113
|
+
|
|
114
|
+
definition = @plan.definitions.fetch('dns')
|
|
115
|
+
resource = service_resource(definition, expected_id: recorded('dns'))
|
|
116
|
+
return if resource && !image_changed?(resource, definition) &&
|
|
117
|
+
resource.dig('labels', 'io.empeira.definition') == definition.fingerprint
|
|
118
|
+
|
|
119
|
+
raise Error, 'DNS replacement would invalidate existing node resolver bindings; destroy nodes before empeira up'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def down
|
|
123
|
+
load_state
|
|
124
|
+
@progress.stage(25, 'Removing service containers...')
|
|
125
|
+
(@inventory.fetch('services').keys - %w[dns proxy gateway]).reverse_each { |key| remove(key) }
|
|
126
|
+
%w[proxy dns gateway].each { |key| remove(key) }
|
|
127
|
+
remove_workspace_bridge
|
|
128
|
+
remove_egress
|
|
129
|
+
@inventory['stopped'] = true
|
|
130
|
+
save if @state['control_plane']
|
|
131
|
+
@changed
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def destroy
|
|
135
|
+
down
|
|
136
|
+
@progress.stage(75, 'Removing persistent volumes and credentials...')
|
|
137
|
+
@inventory.fetch('volumes').each_key do |key|
|
|
138
|
+
@runtime.remove_volume(identity(key), expected_id: @inventory.fetch('volumes').fetch(key).fetch('id'))
|
|
139
|
+
@inventory.fetch('volumes').delete(key)
|
|
140
|
+
save
|
|
141
|
+
end
|
|
142
|
+
cleanup_files
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# rubocop:disable-next Metrics/AbcSize -- One bounded observation includes the central network health.
|
|
146
|
+
def status(state: @store.load)
|
|
147
|
+
load_state(state)
|
|
148
|
+
definitions = service_keys.to_h { |key| [key, identity(key)] }
|
|
149
|
+
expected = @inventory.fetch('services').transform_values { |record| record['id'] }
|
|
150
|
+
@observed = @runtime.inspect_services(definitions, expected_ids: expected)
|
|
151
|
+
report = @observed.to_h { |key, resource| [key.capitalize, readiness(key, resource)] }
|
|
152
|
+
report.merge(summary(report)).merge('Workspace network' =>
|
|
153
|
+
report.values.all? { |value| value == 'running' } ? 'healthy' : 'unhealthy')
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def browser_url
|
|
157
|
+
Browser.url(@observed.fetch('browser-ui'))
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def preflight
|
|
161
|
+
load_state
|
|
162
|
+
@plan.validate_server_mounts!
|
|
163
|
+
check_network_architecture!
|
|
164
|
+
check_service_names!
|
|
165
|
+
@plan.validate_dns_rewrites!
|
|
166
|
+
@plan.validate_network_redirects!
|
|
167
|
+
validate_redirect_routes!
|
|
168
|
+
@plan.module_request.warnings.each { |message| @progress.warning(message) }
|
|
169
|
+
@plan.module_request.verify_available!
|
|
170
|
+
lockdown_existing_gateway
|
|
171
|
+
check_images
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def validate_redirect_routes!
|
|
177
|
+
entries = @plan.config.dig('network', 'redirects')
|
|
178
|
+
unless entries.any? { |entry| Network::DirectEgress::BENCHMARK.include?(entry.fetch('from').fetch('ip')) }
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
routes = Platform::Routes.new(platform: @context.platform, runner: @runner).ipv4 + @runtime.network_subnets
|
|
183
|
+
Network::Redirects.validate_routes!(entries, routes: routes)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def cleanup_files
|
|
187
|
+
@plan.files.cleanup
|
|
188
|
+
Server::RelayCertificate.new(context: @context).cleanup
|
|
189
|
+
@plan.environment.cleanup
|
|
190
|
+
@plan.additional_configurations.cleanup
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def prepare_browser
|
|
194
|
+
return unless @plan.browser_enabled
|
|
195
|
+
|
|
196
|
+
reconcile('browser')
|
|
197
|
+
record_browser_address
|
|
198
|
+
reconcile('browser-ui')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def check_service_names!
|
|
202
|
+
names = @state.fetch('nodes', {}).keys.map { |name| name.delete_suffix('.empeira.internal') }
|
|
203
|
+
return unless names.intersect?(@plan.definitions.keys)
|
|
204
|
+
|
|
205
|
+
raise Error, 'Additional service name collides with an existing node; rename the service or destroy that node'
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def prepare_files
|
|
209
|
+
@plan.additional_configurations.prepare
|
|
210
|
+
@plan.module_request
|
|
211
|
+
prepare_dns_resolution
|
|
212
|
+
@reload_dns = dns_reload_needed?
|
|
213
|
+
@bootstrap_dns = bootstrap_dns?(**@dns_resolution)
|
|
214
|
+
@direct_egress = resolve_direct_egress
|
|
215
|
+
@plan.files.prepare(**@dns_resolution, bootstrap: @bootstrap_dns)
|
|
216
|
+
prepare_gateway_plan
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def prepare_dns_resolution
|
|
220
|
+
resolver = Platform::Resolvers.new(platform: @context.platform, runner: @runner)
|
|
221
|
+
upstreams = resolver.resolve(@plan.config.fetch('dns').fetch('upstream'))
|
|
222
|
+
@dns_resolution = { upstreams: upstreams, routes: resolver.routes }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def prepare_gateway_plan
|
|
226
|
+
@plan.validate_network_redirects!
|
|
227
|
+
@gateway_plan = Network::Gateway.plan(state: @state, resolved: @direct_egress,
|
|
228
|
+
additional: @plan.config.dig('dns', 'additional_resolver'),
|
|
229
|
+
redirects: resolved_redirects(existing_redirect_targets),
|
|
230
|
+
**@dns_resolution)
|
|
231
|
+
@plan.files.write('gateway.json', JSON.generate(@gateway_plan))
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def existing_redirect_targets
|
|
235
|
+
definitions = @plan.definitions.slice(*@plan.config.dig('network', 'redirects').map do |entry|
|
|
236
|
+
Network::Redirects.target_key(entry)
|
|
237
|
+
end)
|
|
238
|
+
ids = definitions.to_h { |key, _| [key, recorded(key)] }
|
|
239
|
+
definitions.to_h { |key, definition| [key, service_resource(definition, expected_id: ids[key])] }
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def resolved_redirects(resources)
|
|
243
|
+
Network::Redirects.new(entries: @plan.config.dig('network', 'redirects'), network: @plan.network,
|
|
244
|
+
subnet: @plan.subnet).resolve(definitions: @plan.definitions, resources: resources)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def refresh_redirects
|
|
248
|
+
redirects = resolved_redirects(@observed)
|
|
249
|
+
return if redirects == @gateway_plan.fetch('redirects')
|
|
250
|
+
|
|
251
|
+
@gateway_plan['redirects'] = redirects
|
|
252
|
+
@plan.files.write('gateway.json', JSON.generate(@gateway_plan))
|
|
253
|
+
apply_gateway_policy
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def resolve_direct_egress
|
|
257
|
+
config = @plan.config.dig('network', 'egress')
|
|
258
|
+
@direct_resolver.resolve(config)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def bootstrap_dns?(upstreams:, routes:)
|
|
262
|
+
return false unless @plan.config.dig('dns', 'additional_resolver')
|
|
263
|
+
return true unless @plan.files.corefile_current?(upstreams: upstreams, routes: routes)
|
|
264
|
+
|
|
265
|
+
definition = @plan.definitions.fetch('dns')
|
|
266
|
+
resource = service_resource(definition, expected_id: recorded('dns'))
|
|
267
|
+
resource.nil? || resource['state'] != 'running' || stale?('dns', resource, definition)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def dns_reload_needed?
|
|
271
|
+
return false if @plan.files.corefile_current?(**@dns_resolution)
|
|
272
|
+
|
|
273
|
+
definition = @plan.definitions.fetch('dns')
|
|
274
|
+
service_resource(definition, expected_id: recorded('dns'))&.fetch('state') == 'running'
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def activate_dns
|
|
278
|
+
return unless @bootstrap_dns || @reload_dns
|
|
279
|
+
|
|
280
|
+
@plan.files.activate_additional_resolver(**@dns_resolution) if @bootstrap_dns
|
|
281
|
+
@runtime.reload_service(@observed.fetch('dns'), signal: 'USR1')
|
|
282
|
+
@changed = true
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def service_keys
|
|
286
|
+
@plan.definitions.keys | @inventory.fetch('services').keys
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def summary(report)
|
|
290
|
+
{ 'Control plane' => report.values.all? { |value| value == 'running' } ? 'running' : 'degraded',
|
|
291
|
+
'Readiness' => 'not checked; empeira up verifies readiness',
|
|
292
|
+
'Persistent volumes' => @inventory.fetch('volumes').size.to_s }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def readiness(key, resource)
|
|
296
|
+
return 'missing' unless resource
|
|
297
|
+
|
|
298
|
+
verify_networks!(key, resource)
|
|
299
|
+
definition = @plan.definitions[key]
|
|
300
|
+
return 'stale' if definition && resource.dig('labels', 'io.empeira.definition') != definition.fingerprint
|
|
301
|
+
|
|
302
|
+
return gateway_readiness(resource) if key == 'gateway' && resource['state'] == 'running'
|
|
303
|
+
|
|
304
|
+
resource['state']
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def gateway_readiness(resource)
|
|
308
|
+
reconcile_bridge('check') if @inventory['bridge_prepared']
|
|
309
|
+
result = @runtime.service_exec(resource, [Network::Gateway::EXECUTABLE, 'check',
|
|
310
|
+
'/empeira-gateway/gateway.json'], timeout: 3)
|
|
311
|
+
result.success? ? 'running' : 'unhealthy; firewall/routing reconcile required'
|
|
312
|
+
rescue Providers::ExecutionError
|
|
313
|
+
'unhealthy; workspace bridge attachment reconcile required'
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def check_images(definitions: @plan.definitions)
|
|
317
|
+
@desired_images = {}
|
|
318
|
+
@runtime.with_local_images do
|
|
319
|
+
definitions.each_value do |definition|
|
|
320
|
+
image = definition.options.fetch('image')
|
|
321
|
+
@runtime.ensure_image(image, recipe: definition.options['recipe'],
|
|
322
|
+
files: definition.options.fetch('build_files', {}))
|
|
323
|
+
@desired_images[image] = @runtime.image_id(image)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def prepare_postgres
|
|
329
|
+
return unless @plan.database?
|
|
330
|
+
|
|
331
|
+
reconcile('postgres')
|
|
332
|
+
@health.wait('postgres', @observed)
|
|
333
|
+
result = @runtime.service_exec(@observed.fetch('postgres'),
|
|
334
|
+
['psql', '-U', 'postgres', '-d', 'puppetdb', '-v', 'ON_ERROR_STOP=1',
|
|
335
|
+
'-f', '/empeira/setup.sql'])
|
|
336
|
+
raise Error, 'PostgreSQL roles and pg_trgm initialization failed' unless result.success?
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def prepare_server
|
|
340
|
+
reconcile('server')
|
|
341
|
+
refresh_eyaml_keys if @plan.eyaml_staged?
|
|
342
|
+
Runtime::MountProbe.new(runtime: @runtime).verify(@observed.fetch('server'),
|
|
343
|
+
[@plan.repository_mounts, @plan.eyaml_mounts,
|
|
344
|
+
@plan.project_server_mounts].flatten)
|
|
345
|
+
@health.wait('server', @observed)
|
|
346
|
+
reconcile_server_environment
|
|
347
|
+
Server::RelayCertificate.new(context: @context).prepare(runtime: @runtime, server: @observed.fetch('server')) if
|
|
348
|
+
@plan.database?
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def reconcile_server_environment
|
|
352
|
+
environment = Server::EnvironmentConfiguration.new(runtime: @runtime, plan: @plan)
|
|
353
|
+
return unless environment.reconcile(@observed.fetch('server'))
|
|
354
|
+
|
|
355
|
+
@changed = true
|
|
356
|
+
@health.wait('server', @observed)
|
|
357
|
+
environment.verify!(@observed.fetch('server'))
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def refresh_eyaml_keys
|
|
361
|
+
result = @runtime.service_exec(@observed.fetch('server'),
|
|
362
|
+
['/bin/sh', '/empeira-server/start.sh', 'refresh-eyaml'])
|
|
363
|
+
raise Error, 'Puppet Server EYAML key staging failed; inspect server logs' unless result.success?
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def prepare_puppetdb
|
|
367
|
+
return unless @plan.database?
|
|
368
|
+
|
|
369
|
+
@progress.stage(85, 'Starting PuppetDB...')
|
|
370
|
+
reconcile('puppetdb-backend')
|
|
371
|
+
@health.wait('puppetdb-backend', @observed)
|
|
372
|
+
reconcile('puppetdb')
|
|
373
|
+
@health.wait('puppetdb', @observed)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def finish_reconciliation
|
|
377
|
+
remove_disabled
|
|
378
|
+
@health.wait('dns', @observed)
|
|
379
|
+
@health.wait('proxy', @observed) if @plan.proxy?
|
|
380
|
+
update_hosts(reload_proxy: true) if @plan.proxy?
|
|
381
|
+
@inventory['stopped'] = false
|
|
382
|
+
save
|
|
383
|
+
@plan.additional_configurations.prune
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def load_state(state = @store.load)
|
|
387
|
+
@state = state || {}
|
|
388
|
+
@inventory = @state['control_plane'] || { 'services' => {}, 'volumes' => {}, 'egress' => nil }
|
|
389
|
+
@inventory.delete('environment_cache')
|
|
390
|
+
@plan.subnet = @state.dig('peer_network', 'subnet')
|
|
391
|
+
@plan.browser_enabled = @inventory.fetch('services').key?('browser')
|
|
392
|
+
@changed = false
|
|
393
|
+
@proxy_bindings_changed = false
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def save
|
|
397
|
+
@state['control_plane'] = @inventory
|
|
398
|
+
@store.write(@state)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def check_network_architecture!
|
|
402
|
+
services = @inventory.fetch('services')
|
|
403
|
+
return if services.empty? || services.key?('gateway')
|
|
404
|
+
|
|
405
|
+
raise Error,
|
|
406
|
+
'Workspace network architecture changed; destroy this alpha workspace with its previous version first'
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def capture_services
|
|
410
|
+
definitions = service_keys.to_h { |key| [key, identity(key)] }
|
|
411
|
+
expected = @inventory.fetch('services').transform_values { |record| record['id'] }
|
|
412
|
+
@service_snapshot = @runtime.inspect_services(definitions, expected_ids: expected)
|
|
413
|
+
@observed = @service_snapshot.slice(*@plan.definitions.keys).compact
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def service_resource(definition, expected_id:)
|
|
417
|
+
unless @service_snapshot&.key?(definition.key)
|
|
418
|
+
return @runtime.inspect_service(definition,
|
|
419
|
+
expected_id: expected_id)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
resource = @service_snapshot[definition.key]
|
|
423
|
+
definition.verify!(resource, expected_id: expected_id)
|
|
424
|
+
resource
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def prepare_storage
|
|
428
|
+
storage = Storage.new(runtime: @runtime, plan: @plan, inventory: @inventory, persist: method(:save))
|
|
429
|
+
@changed = storage.prepare || @changed
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def reconcile(key)
|
|
433
|
+
definition = @plan.definitions(dns: @dns).fetch(key)
|
|
434
|
+
resource = service_resource(definition, expected_id: recorded(key))
|
|
435
|
+
if resource && stale?(key, resource, definition)
|
|
436
|
+
remove(key)
|
|
437
|
+
resource = nil
|
|
438
|
+
end
|
|
439
|
+
resource ||= create_service(definition)
|
|
440
|
+
start_service(resource)
|
|
441
|
+
observe_service(key, definition, resource.fetch('id'))
|
|
442
|
+
configure_route(key)
|
|
443
|
+
update_hosts
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def reconcile_browser(key, definition)
|
|
447
|
+
resource = service_resource(definition, expected_id: recorded(key))
|
|
448
|
+
if resource && stale?(key, resource, definition)
|
|
449
|
+
remove(key)
|
|
450
|
+
resource = nil
|
|
451
|
+
end
|
|
452
|
+
resource ||= create_service(definition)
|
|
453
|
+
start_service(resource)
|
|
454
|
+
observe_service(key, definition, resource.fetch('id'))
|
|
455
|
+
configure_route(key)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def record_browser_address
|
|
459
|
+
address = @observed.fetch('browser').dig('networks', @plan.network, 'IPAddress')
|
|
460
|
+
raise Error, 'Browser service has no address in the isolated network' if address.nil? || address.empty?
|
|
461
|
+
|
|
462
|
+
IPAddr.new(address)
|
|
463
|
+
@plan.files.write('browser-address', "#{address}\n")
|
|
464
|
+
rescue IPAddr::InvalidAddressError
|
|
465
|
+
raise Error, 'Browser service has an invalid address in the isolated network'
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def require_initialized_browser_workspace!
|
|
469
|
+
return if @state['control_plane']
|
|
470
|
+
|
|
471
|
+
raise Error, "Workspace control plane is not initialized. Start the workspace with 'empeira up' first."
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def require_browser_access_network!
|
|
475
|
+
return if observed_egress
|
|
476
|
+
|
|
477
|
+
raise Error, "Required browser access network is not running. Start the workspace with 'empeira up' first."
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def require_running_service!(key)
|
|
481
|
+
definition = @plan.definitions.fetch(key)
|
|
482
|
+
resource = service_resource(definition, expected_id: recorded(key))
|
|
483
|
+
require_service_running!(key, resource)
|
|
484
|
+
if resource.dig('labels', 'io.empeira.definition') != definition.fingerprint
|
|
485
|
+
raise Error, "Required service #{key} is stale. Run 'empeira up' first."
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
verify_networks!(key, resource)
|
|
489
|
+
@observed[key] = resource
|
|
490
|
+
address = resource.dig('networks', @plan.network, 'IPAddress')
|
|
491
|
+
require_service_address!(key, address)
|
|
492
|
+
|
|
493
|
+
address
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def require_service_running!(key, resource)
|
|
497
|
+
return if resource && resource.fetch('state') == 'running'
|
|
498
|
+
|
|
499
|
+
raise Error, "Required service #{key} is not running. Start the workspace with 'empeira up' first."
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def require_service_address!(key, address)
|
|
503
|
+
return unless address.nil? || address.empty?
|
|
504
|
+
|
|
505
|
+
raise Error, "Required service #{key} has no workspace address. Run 'empeira up' first."
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def observe_service(key, definition, id)
|
|
509
|
+
@observed[key] = service_resource(definition, expected_id: id)
|
|
510
|
+
require_started!(key, @observed.fetch(key))
|
|
511
|
+
attach_browser_ui_if_needed(key, definition, id)
|
|
512
|
+
verify_networks!(key, @observed.fetch(key))
|
|
513
|
+
@service_snapshot[key] = @observed.fetch(key) if @service_snapshot
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def attach_browser_ui_if_needed(key, definition, id)
|
|
517
|
+
return unless key == 'browser-ui' && !@observed.fetch(key).fetch('networks').key?(@plan.network)
|
|
518
|
+
|
|
519
|
+
@runtime.attach_egress(Network::Definition.new(workspace: @context.workspace, policy: Network::Policy.new),
|
|
520
|
+
@observed.fetch(key))
|
|
521
|
+
@service_snapshot&.delete(key)
|
|
522
|
+
@observed[key] = service_resource(definition, expected_id: id)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def require_started!(key, resource)
|
|
526
|
+
return if resource.fetch('state') == 'running'
|
|
527
|
+
|
|
528
|
+
raise Error, "Managed service #{key} is #{resource.fetch('state')} after start; " \
|
|
529
|
+
"inspect #{@context.container_engine} logs #{resource.fetch('id')}"
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def update_hosts(reload_proxy: false)
|
|
533
|
+
# A running container can still be initializing Squid. Defer HUP until its listener is ready.
|
|
534
|
+
changed = Discovery.new(plan: @plan, runtime: @runtime, state: @state).refresh(
|
|
535
|
+
services: @observed, node_resources: discovery_nodes, reload_proxy: reload_proxy,
|
|
536
|
+
force_proxy_reload: @proxy_configuration_changed || @proxy_bindings_changed
|
|
537
|
+
)
|
|
538
|
+
@proxy_bindings_changed = changed || @proxy_bindings_changed
|
|
539
|
+
@changed = true if @plan.proxy? && (changed || @proxy_configuration_changed)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def discovery_nodes
|
|
543
|
+
return @discovery_nodes if @discovery_nodes
|
|
544
|
+
|
|
545
|
+
records = @state.fetch('nodes', {}).select { |_, record| record['provider'] == 'container' }
|
|
546
|
+
definitions = records.transform_values do |record|
|
|
547
|
+
Node::Definition.new(hostname: record.fetch('hostname'), workspace: @context.workspace)
|
|
548
|
+
end
|
|
549
|
+
ids = records.transform_values { |record| record['id'] }
|
|
550
|
+
@discovery_nodes = definitions.empty? ? {} : @runtime.inspect_services(definitions, expected_ids: ids)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def start_service(resource)
|
|
554
|
+
return if resource.fetch('state') == 'running'
|
|
555
|
+
|
|
556
|
+
@runtime.start_service(resource)
|
|
557
|
+
@service_snapshot&.delete(resource.dig('labels', 'io.empeira.purpose'))
|
|
558
|
+
@changed = true
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def stale?(key, resource, definition)
|
|
562
|
+
resource.dig('labels', 'io.empeira.definition') != definition.fingerprint ||
|
|
563
|
+
image_changed?(resource, definition) || address_changed?(resource, definition) ||
|
|
564
|
+
(!%w[dns gateway].include?(key) && resource['dns'] != [@dns])
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def address_changed?(resource, definition)
|
|
568
|
+
expected = definition.options['runtime_ip']
|
|
569
|
+
expected && resource.dig('networks', @plan.network, 'IPAddress') != expected
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def image_changed?(resource, definition)
|
|
573
|
+
resource['image_id'] != @desired_images&.fetch(definition.options.fetch('image'), nil)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def create_service(definition)
|
|
577
|
+
@service_snapshot&.delete(definition.key)
|
|
578
|
+
@inventory.fetch('services')[definition.key] = { 'id' => nil }
|
|
579
|
+
save
|
|
580
|
+
resource = @runtime.create_service(definition)
|
|
581
|
+
@inventory.fetch('services')[definition.key] = { 'id' => resource.fetch('id') }
|
|
582
|
+
save
|
|
583
|
+
@changed = true
|
|
584
|
+
resource
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def verify_networks!(key, resource)
|
|
588
|
+
allowed = [@plan.network]
|
|
589
|
+
allowed << egress.backend_name if %w[gateway browser-ui].include?(key)
|
|
590
|
+
unless resource.fetch('networks').key?(@plan.network) &&
|
|
591
|
+
resource.fetch('networks').keys.all? { |name| allowed.include?(name) } &&
|
|
592
|
+
valid_ports?(key, resource)
|
|
593
|
+
raise Providers::OwnershipError, "#{key} has unexpected network attachments or published ports"
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def valid_ports?(key, resource)
|
|
598
|
+
return Browser.valid_ports?(resource) if key == 'browser-ui'
|
|
599
|
+
|
|
600
|
+
resource.fetch('ports').empty? && resource.fetch('published_ports', {}).values.all?(&:nil?)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def reconcile_proxy
|
|
604
|
+
return unless @plan.proxy?
|
|
605
|
+
|
|
606
|
+
ensure_egress
|
|
607
|
+
reconcile('proxy')
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def configure_route(key)
|
|
611
|
+
return if %w[gateway browser-ui].include?(key)
|
|
612
|
+
|
|
613
|
+
Network::Gateway.new(context: @context, runtime: @runtime, state: @state).route(@observed.fetch(key))
|
|
614
|
+
rescue Providers::ExecutionError
|
|
615
|
+
resource = @runtime.inspect_service(identity(key), expected_id: recorded(key))
|
|
616
|
+
require_started!(key, resource) if resource
|
|
617
|
+
raise
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def lockdown_existing_gateway
|
|
621
|
+
resource = @runtime.inspect_service(identity('gateway'), expected_id: recorded('gateway'))
|
|
622
|
+
return unless resource && resource['state'] == 'running'
|
|
623
|
+
|
|
624
|
+
lockdown_gateway(resource)
|
|
625
|
+
rescue Error
|
|
626
|
+
@runtime.stop_service(resource) if resource
|
|
627
|
+
raise
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# rubocop:disable-next Metrics/AbcSize -- Uplink attachment is ordered behind verified lockdown.
|
|
631
|
+
def reconcile_gateway
|
|
632
|
+
resource = service_resource(identity('gateway'), expected_id: recorded('gateway'))
|
|
633
|
+
if resource && resource['state'] != 'running' && resource.fetch('networks').key?(egress.backend_name)
|
|
634
|
+
@runtime.detach_egress(egress, resource)
|
|
635
|
+
end
|
|
636
|
+
reconcile('gateway')
|
|
637
|
+
lockdown_gateway(@observed.fetch('gateway'))
|
|
638
|
+
require_redirect_capability
|
|
639
|
+
attach_gateway('gateway')
|
|
640
|
+
apply_gateway_policy
|
|
641
|
+
rescue StandardError
|
|
642
|
+
resource = @observed['gateway']
|
|
643
|
+
@runtime.stop_service(resource) if resource
|
|
644
|
+
raise
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def lockdown_gateway(resource)
|
|
648
|
+
return if @gateway_locked_id == resource.fetch('id')
|
|
649
|
+
|
|
650
|
+
gateway_command(resource, 'lockdown')
|
|
651
|
+
@gateway_locked_id = resource.fetch('id')
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# rubocop:disable-next Metrics/AbcSize -- Negotiate legacy TCP and extended DNS capability without fallback.
|
|
655
|
+
def require_redirect_capability
|
|
656
|
+
return if @plan.config.dig('network', 'redirects').empty?
|
|
657
|
+
|
|
658
|
+
result = @runtime.service_exec(@observed.fetch('gateway'),
|
|
659
|
+
[Network::Gateway::EXECUTABLE, 'redirects-capability'])
|
|
660
|
+
protocols = @plan.config.dig('network', 'redirects').map do |entry|
|
|
661
|
+
entry.fetch('from').fetch('protocol', 'tcp')
|
|
662
|
+
end
|
|
663
|
+
supported = protocols.include?('udp') ? ['tcp-udp-redirects-v2'] : %w[tcp-redirects-v1 tcp-udp-redirects-v2]
|
|
664
|
+
return if result.success? && supported.include?(result.stdout.strip)
|
|
665
|
+
|
|
666
|
+
raise Error, 'Gateway does not support transparent IPv4 TCP/UDP redirects; configure a compatible ' \
|
|
667
|
+
'images.direct_egress image'
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def apply_gateway_policy
|
|
671
|
+
# An uncertain apply must force fresh lockdown during failure recovery.
|
|
672
|
+
@gateway_locked_id = nil
|
|
673
|
+
gateway_command(@observed.fetch('gateway'), 'apply', '/empeira-gateway/gateway.json')
|
|
674
|
+
record_gateway_policy
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def record_gateway_policy
|
|
678
|
+
fingerprint = Infrastructure::Definition.fingerprint(@gateway_plan)
|
|
679
|
+
@changed = true if @inventory['gateway_policy'] != fingerprint
|
|
680
|
+
@inventory['gateway_policy'] = fingerprint
|
|
681
|
+
save
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def gateway_command(resource, *arguments)
|
|
685
|
+
result = @runtime.service_exec(resource, [Network::Gateway::EXECUTABLE, *arguments])
|
|
686
|
+
return if result.success?
|
|
687
|
+
|
|
688
|
+
raise Error, 'Gateway firewall reconcile failed; workspace egress remains blocked'
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def attach_gateway(key)
|
|
692
|
+
resource = @observed.fetch(key)
|
|
693
|
+
return if resource.fetch('networks').key?(egress.backend_name)
|
|
694
|
+
|
|
695
|
+
@runtime.attach_egress(egress, resource)
|
|
696
|
+
@observed[key] = @runtime.inspect_service(identity(key), expected_id: recorded(key))
|
|
697
|
+
@service_snapshot[key] = @observed.fetch(key) if @service_snapshot
|
|
698
|
+
@changed = true
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def ensure_egress
|
|
702
|
+
resource = observed_egress
|
|
703
|
+
unless resource
|
|
704
|
+
save
|
|
705
|
+
resource = @runtime.create_network(definition: egress).resource
|
|
706
|
+
@changed = true
|
|
707
|
+
end
|
|
708
|
+
@inventory['egress'] = resource.id
|
|
709
|
+
save
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def observed_egress
|
|
713
|
+
expected = @inventory['egress']
|
|
714
|
+
resource = @runtime.inspect_network(identifier: egress.backend_name)
|
|
715
|
+
egress.verify_ownership!(resource, expected_id: expected)
|
|
716
|
+
if resource.nil? && expected && @runtime.inspect_network(identifier: expected)
|
|
717
|
+
raise Providers::OwnershipError, 'Recorded egress network exists under a different name'
|
|
718
|
+
end
|
|
719
|
+
return unless resource
|
|
720
|
+
|
|
721
|
+
egress.verify_definition!(resource)
|
|
722
|
+
egress.verify_isolation!(resource)
|
|
723
|
+
resource
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def remove_disabled
|
|
727
|
+
(@inventory.fetch('services').keys - @plan.definitions.keys).each { |key| remove(key) }
|
|
728
|
+
update_hosts
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def remove_egress
|
|
732
|
+
result = @runtime.remove_network(definition: egress, expected_id: @inventory['egress'])
|
|
733
|
+
@changed ||= result.changed
|
|
734
|
+
@inventory['egress'] = nil
|
|
735
|
+
save if @state['control_plane']
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def remove(key)
|
|
739
|
+
return unless @inventory.fetch('services').key?(key)
|
|
740
|
+
|
|
741
|
+
@changed = @runtime.remove_service(identity(key), expected_id: recorded(key)) || @changed
|
|
742
|
+
@inventory.fetch('services').delete(key)
|
|
743
|
+
@inventory.delete('gateway_policy') if key == 'gateway'
|
|
744
|
+
@inventory.delete('proxy_reload') if key == 'proxy'
|
|
745
|
+
@observed.delete(key)
|
|
746
|
+
@service_snapshot&.delete(key)
|
|
747
|
+
save
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def recorded(key)
|
|
751
|
+
@inventory.fetch('services').dig(key, 'id')
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
def identity(key)
|
|
755
|
+
Services::Definition.new(key: key, workspace: @context.workspace)
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def egress
|
|
759
|
+
Network::Egress.new(workspace: @context.workspace, policy: Network::Policy.new)
|
|
760
|
+
end
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
end
|