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,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module ControlPlane
|
|
5
|
+
class Discovery
|
|
6
|
+
def initialize(plan:, runtime:, state:)
|
|
7
|
+
@plan = plan
|
|
8
|
+
@runtime = runtime
|
|
9
|
+
@state = state
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def refresh(services: nil, node_resources: nil, reload_proxy: true, force_proxy_reload: false)
|
|
13
|
+
nodes = @state.fetch('nodes', {})
|
|
14
|
+
definitions = node_definitions(nodes)
|
|
15
|
+
expected = nodes.select { |_, record| record['provider'] == 'container' }
|
|
16
|
+
.transform_values { |record| record['id'] }
|
|
17
|
+
resources = inspect(definitions, expected, services, node_resources)
|
|
18
|
+
resources.merge!(lease_resources(nodes))
|
|
19
|
+
@plan.files.hosts(resources, @plan.network)
|
|
20
|
+
refresh_proxy(resources, nodes, reload: reload_proxy, force: force_proxy_reload)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def node_definitions(nodes)
|
|
26
|
+
nodes.select { |_, record| record['provider'] == 'container' }.transform_values do |record|
|
|
27
|
+
Node::Definition.new(hostname: record.fetch('hostname'), workspace: @plan.context.workspace)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def refresh_proxy(resources, nodes, reload:, force:)
|
|
32
|
+
changed = @plan.files.proxy_clients(resources, @plan.network, nodes: nodes)
|
|
33
|
+
resource = resources['proxy']
|
|
34
|
+
reload_proxy(resource, changed: changed || force) if reload && resource&.fetch('state') == 'running'
|
|
35
|
+
changed
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def reload_proxy(resource, changed:)
|
|
39
|
+
checkpoint = { 'id' => resource.fetch('id'), 'fingerprint' => @plan.files.proxy_reload_digest }
|
|
40
|
+
plane = @state.fetch('control_plane')
|
|
41
|
+
return if !changed && plane['proxy_reload'] == checkpoint
|
|
42
|
+
|
|
43
|
+
@runtime.reload_service(resource)
|
|
44
|
+
plane['proxy_reload'] = checkpoint
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def lease_resources(nodes)
|
|
48
|
+
nodes.filter_map do |name, record|
|
|
49
|
+
address = record.dig('peer', 'ip') || record.dig('definition', 'ip')
|
|
50
|
+
[name, { 'networks' => { @plan.network => { 'IPAddress' => address } } }] if address
|
|
51
|
+
end.to_h
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def inspect_nodes(definitions, expected)
|
|
55
|
+
definitions.empty? ? {} : @runtime.inspect_services(definitions, expected_ids: expected)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inspect(definitions, expected, services, node_resources)
|
|
59
|
+
if services
|
|
60
|
+
nodes = node_resources || inspect_nodes(definitions, expected)
|
|
61
|
+
return services.merge(nodes.compact)
|
|
62
|
+
end
|
|
63
|
+
@plan.browser_enabled = @state.dig('control_plane', 'services')&.key?('browser')
|
|
64
|
+
expected.merge!(@state.fetch('control_plane').fetch('services').transform_values { |record| record['id'] })
|
|
65
|
+
@runtime.inspect_services(@plan.definitions.merge(definitions), expected_ids: expected).compact
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module ControlPlane
|
|
5
|
+
# Mount destinations exist in state, while control code stays live and read-only.
|
|
6
|
+
class Environment
|
|
7
|
+
CONTROL = '/empeira-control'
|
|
8
|
+
|
|
9
|
+
def initialize(context:, hiera:)
|
|
10
|
+
@project = context.project.path
|
|
11
|
+
@root = root(context)
|
|
12
|
+
@targets = mount_targets(hiera)
|
|
13
|
+
@modules = Modules::Storage.new(context: context).root if @project.join('Puppetfile').file?
|
|
14
|
+
@targets |= ['modules'] if @modules
|
|
15
|
+
@base = context.locations.workspace(context.workspace).join('environments')
|
|
16
|
+
@directory = @base.join(Infrastructure::Definition.fingerprint([@targets, @modules&.to_s]))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def mounts
|
|
20
|
+
return [bind(@project, @root)] if @targets.empty?
|
|
21
|
+
|
|
22
|
+
[bind(@project, CONTROL), bind(@directory, @root),
|
|
23
|
+
*(@modules ? [bind(@modules, "#{@root}/modules")] : [])]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def prepare
|
|
27
|
+
return if @targets.empty?
|
|
28
|
+
raise Error, 'Managed environment directory must not be a symlink' if @directory.symlink?
|
|
29
|
+
|
|
30
|
+
FileUtils.mkdir_p(@directory, mode: 0o755)
|
|
31
|
+
project_directory('', @directory)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def cleanup
|
|
35
|
+
raise Error, 'Managed environment directory must not be a symlink' if @base.symlink?
|
|
36
|
+
|
|
37
|
+
FileUtils.remove_entry_secure(@base) if @base.exist?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def mount_targets(hiera)
|
|
43
|
+
hiera.entries.select { |entry| entry['status'] == 'available' }.map do |entry|
|
|
44
|
+
entry['destination'].delete_prefix("#{@root}/")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def root(context)
|
|
49
|
+
"/etc/puppetlabs/code/environments/#{context.configuration.dig('server', 'environment')}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def bind(source, destination)
|
|
53
|
+
"type=bind,src=#{source},dst=#{destination},readonly"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def project_directory(relative, destination)
|
|
57
|
+
child_names(relative).each do |name|
|
|
58
|
+
path = relative.empty? ? name : "#{relative}/#{name}"
|
|
59
|
+
create_entry(path, destination.join(name))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def child_names(relative)
|
|
64
|
+
source = @project.join(relative)
|
|
65
|
+
children = source.directory? ? source.children.map { |child| child.basename.to_s } : []
|
|
66
|
+
children = [] if relative == 'modules' && @modules
|
|
67
|
+
prefixes = target_prefixes(relative)
|
|
68
|
+
children | prefixes
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def target_prefixes(relative)
|
|
72
|
+
@targets.filter_map do |target|
|
|
73
|
+
next unless relative.empty? || target.start_with?("#{relative}/")
|
|
74
|
+
|
|
75
|
+
target.delete_prefix(relative.empty? ? '' : "#{relative}/").split('/').first
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def create_link(relative, destination)
|
|
80
|
+
File.symlink("#{CONTROL}/#{relative}", destination) unless destination.symlink? || destination.exist?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def create_entry(relative, destination)
|
|
84
|
+
if @targets.any? { |target| target == relative || target.start_with?("#{relative}/") }
|
|
85
|
+
create_target(relative, destination)
|
|
86
|
+
else
|
|
87
|
+
create_link(relative, destination)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def create_target(relative, destination)
|
|
92
|
+
raise Error, 'Managed environment destination is unsafe' if destination.symlink?
|
|
93
|
+
|
|
94
|
+
FileUtils.mkdir_p(destination, mode: 0o755)
|
|
95
|
+
return if @targets.include?(relative) && !(relative == 'modules' && @modules)
|
|
96
|
+
|
|
97
|
+
project_directory(relative, destination)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module ControlPlane
|
|
7
|
+
# Keep the small generated SQL and TLS configuration templates together.
|
|
8
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
9
|
+
class Files
|
|
10
|
+
attr_reader :directory
|
|
11
|
+
|
|
12
|
+
def initialize(context:)
|
|
13
|
+
@context = context
|
|
14
|
+
@directory = context.locations.workspace(context.workspace).join('services')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def path(name)
|
|
18
|
+
directory.join(name).to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def prepare(upstreams: nil, routes: {}, bootstrap: false)
|
|
22
|
+
FileUtils.mkdir_p(directory, mode: 0o755)
|
|
23
|
+
configuration(bootstrap: bootstrap).each do |name, content|
|
|
24
|
+
next if name == 'squid.conf'
|
|
25
|
+
|
|
26
|
+
content = if name == 'Corefile'
|
|
27
|
+
corefile_content(upstreams:, routes:, bootstrap:)
|
|
28
|
+
else
|
|
29
|
+
content.gsub('@HOST_UPSTREAM@', Array(upstreams).join(' '))
|
|
30
|
+
end
|
|
31
|
+
write(name, content)
|
|
32
|
+
end
|
|
33
|
+
prepare_discovery_files
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def corefile_current?(upstreams:, routes:)
|
|
37
|
+
File.file?(path('Corefile')) && File.binread(path('Corefile')) == corefile_content(upstreams:, routes:)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def activate_additional_resolver(upstreams:, routes:)
|
|
41
|
+
write('Corefile', corefile_content(upstreams:, routes:))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def configuration(bootstrap: false)
|
|
45
|
+
{
|
|
46
|
+
'Corefile' => corefile(bootstrap: bootstrap),
|
|
47
|
+
'squid.conf' => Network::ProxyPolicy.new(@context.configuration.fetch('proxy')).configuration,
|
|
48
|
+
'jetty.ini' => jetty, 'database.conf' => database, 'database-setup.sql' => database_setup,
|
|
49
|
+
'puppetdb.conf' => "puppetdb: {}\n",
|
|
50
|
+
'server-puppetdb.conf' => <<~CONFIG
|
|
51
|
+
[main]
|
|
52
|
+
server_urls = https://puppetdb.empeira.internal:8081
|
|
53
|
+
verify_client_certificate = false
|
|
54
|
+
CONFIG
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def prepare_discovery_files
|
|
59
|
+
write('proxy-rules.conf', '') unless File.exist?(path('proxy-rules.conf'))
|
|
60
|
+
write('proxy-clients', "127.0.0.1/32\n") unless File.exist?(path('proxy-clients'))
|
|
61
|
+
write('hosts', '') unless File.exist?(path('hosts'))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bind_dns(address)
|
|
65
|
+
IPAddr.new(address)
|
|
66
|
+
write('squid.conf', configuration.fetch('squid.conf').gsub('@EMPEIRA_DNS@', address))
|
|
67
|
+
rescue IPAddr::InvalidAddressError
|
|
68
|
+
raise Error, 'Cannot bind proxy DNS to an invalid CoreDNS address'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def cleanup
|
|
72
|
+
FileUtils.rm_f(secret_path)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def credentials(existing:)
|
|
76
|
+
return if File.file?(secret_path)
|
|
77
|
+
|
|
78
|
+
if existing
|
|
79
|
+
raise Error,
|
|
80
|
+
'Database credentials are missing for retained storage; restore the workspace credentials'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
content = %w[POSTGRES_PASSWORD EMPEIRA_DB_PASSWORD EMPEIRA_READ_PASSWORD].map do |key|
|
|
84
|
+
"#{key}=#{SecureRandom.hex(32)}\n"
|
|
85
|
+
end.join
|
|
86
|
+
File.write(secret_path, content, mode: 'wx', perm: 0o600)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def secret_path
|
|
90
|
+
@context.locations.workspace(@context.workspace).join('database.env').to_s
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# rubocop:disable-next Naming/PredicateMethod -- This atomic write returns whether content changed.
|
|
94
|
+
def write(name, content, mode: 0o644)
|
|
95
|
+
return false if File.file?(path(name)) && File.binread(path(name)) == content
|
|
96
|
+
|
|
97
|
+
Tempfile.create('configuration-', directory) do |file|
|
|
98
|
+
file.chmod(mode)
|
|
99
|
+
file.write(content)
|
|
100
|
+
file.flush
|
|
101
|
+
File.rename(file.path, path(name))
|
|
102
|
+
end
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def hosts(resources, network)
|
|
107
|
+
naming = Network::Naming.new
|
|
108
|
+
entries = resources.sort.filter_map do |key, resource|
|
|
109
|
+
address = resource.dig('networks', network, 'IPAddress')
|
|
110
|
+
"#{address} #{naming.aliases(key).join(' ')}" if address && !address.empty?
|
|
111
|
+
end
|
|
112
|
+
write('hosts', "#{entries.join("\n")}\n")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def proxy_clients(resources, network, nodes: {})
|
|
116
|
+
bindings = Network::ProxyBindings.new(context: @context, resources: resources, network: network, nodes: nodes)
|
|
117
|
+
# The workspace parent is private; Squid's mapped service user must read the bind mount.
|
|
118
|
+
rules_changed = write('proxy-rules.conf', bindings.configuration)
|
|
119
|
+
allowed = proxy_allowed(nodes)
|
|
120
|
+
addresses = resources.slice(*allowed).values.filter_map do |resource|
|
|
121
|
+
resource&.dig('networks', network, 'IPAddress')
|
|
122
|
+
end
|
|
123
|
+
clients_changed = write('proxy-clients', "#{(['127.0.0.1/32'] + addresses.reject(&:empty?).sort).join("\n")}\n")
|
|
124
|
+
rules_changed || clients_changed
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def proxy_reload_digest
|
|
128
|
+
content = %w[squid.conf proxy-rules.conf proxy-clients].to_h do |name|
|
|
129
|
+
[name, File.binread(path(name))]
|
|
130
|
+
end
|
|
131
|
+
Infrastructure::Definition.fingerprint(content)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def proxy_allowed(nodes)
|
|
135
|
+
['server', *nodes.keys]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def corefile_content(upstreams:, routes:, bootstrap: false)
|
|
141
|
+
corefile(bootstrap: bootstrap).gsub('@HOST_UPSTREAM@', Array(upstreams).join(' ')) +
|
|
142
|
+
resolver_routes(routes, bootstrap: bootstrap) + rewrite_zones(upstreams: upstreams, routes: routes,
|
|
143
|
+
bootstrap: bootstrap)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Reloadable DNS policy is deliberately outside the container definition fingerprint.
|
|
147
|
+
# Exact rewrites use authoritative discovery; unmatched descendants keep normal forwarding.
|
|
148
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
149
|
+
def rewrite_zones(upstreams:, routes:, bootstrap:)
|
|
150
|
+
Configuration::DNSRewrites.entries(@context.configuration).map do |entry|
|
|
151
|
+
source, target = entry.values_at('from', 'to')
|
|
152
|
+
route = routes.keys.select { |zone| source == zone || source.end_with?(".#{zone}") }.max_by(&:size)
|
|
153
|
+
servers = route ? routes.fetch(route) : upstreams
|
|
154
|
+
<<~CONFIG
|
|
155
|
+
#{source}:53 {
|
|
156
|
+
rewrite stop name exact #{source} #{target}
|
|
157
|
+
template IN AAAA . {
|
|
158
|
+
rcode NOERROR
|
|
159
|
+
}
|
|
160
|
+
hosts /empeira/hosts empeira.internal {
|
|
161
|
+
ttl 1
|
|
162
|
+
reload 1s
|
|
163
|
+
}
|
|
164
|
+
#{forwarders(Array(servers).join(' '), bootstrap: bootstrap, authoritative: true)}
|
|
165
|
+
loop
|
|
166
|
+
reload
|
|
167
|
+
}
|
|
168
|
+
CONFIG
|
|
169
|
+
end.join
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def resolver_routes(routes, bootstrap: false)
|
|
173
|
+
sources = Configuration::DNSRewrites.entries(@context.configuration).map { |entry| entry.fetch('from') }
|
|
174
|
+
routes.except(*sources).map do |zone, addresses|
|
|
175
|
+
forwarding = forwarders(addresses.join(' '), bootstrap: bootstrap)
|
|
176
|
+
"#{zone}:53 {\n template IN AAAA {\n rcode NOERROR\n }\n #{forwarding}\n loop\n reload\n}\n"
|
|
177
|
+
end.join
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def forwarders(servers, bootstrap: false, authoritative: false)
|
|
181
|
+
additional = @context.configuration.dig('dns', 'additional_resolver') unless bootstrap
|
|
182
|
+
exclusion = authoritative ? " except empeira.internal\n" : ''
|
|
183
|
+
fallback = authoritative ? "forward . #{servers} {\n#{exclusion} }" : "forward . #{servers}"
|
|
184
|
+
return fallback unless additional
|
|
185
|
+
|
|
186
|
+
"forward . #{additional} {\n#{exclusion} next NXDOMAIN\n next_on_nodata\n }\n #{fallback}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# One readable CoreDNS document defines both authoritative and forwarding zones.
|
|
190
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
191
|
+
def corefile(bootstrap: false)
|
|
192
|
+
upstream = @context.configuration.dig('dns', 'upstream')
|
|
193
|
+
servers = upstream['mode'] == 'host' ? '@HOST_UPSTREAM@' : upstream.fetch('servers').join(' ')
|
|
194
|
+
<<~CONFIG
|
|
195
|
+
empeira.internal:53 {
|
|
196
|
+
template IN AAAA {
|
|
197
|
+
rcode NOERROR
|
|
198
|
+
}
|
|
199
|
+
hosts /empeira/hosts {
|
|
200
|
+
ttl 1
|
|
201
|
+
reload 1s
|
|
202
|
+
}
|
|
203
|
+
reload
|
|
204
|
+
}
|
|
205
|
+
.:53 {
|
|
206
|
+
template IN AAAA {
|
|
207
|
+
rcode NOERROR
|
|
208
|
+
}
|
|
209
|
+
hosts /empeira/hosts {
|
|
210
|
+
ttl 1
|
|
211
|
+
reload 1s
|
|
212
|
+
fallthrough
|
|
213
|
+
}
|
|
214
|
+
#{forwarders(servers, bootstrap: bootstrap)}
|
|
215
|
+
loop
|
|
216
|
+
reload
|
|
217
|
+
health :8080
|
|
218
|
+
}
|
|
219
|
+
CONFIG
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def database_setup
|
|
223
|
+
<<~'SQL'
|
|
224
|
+
\getenv writer_password EMPEIRA_DB_PASSWORD
|
|
225
|
+
\getenv reader_password EMPEIRA_READ_PASSWORD
|
|
226
|
+
SELECT format('CREATE ROLE puppetdb LOGIN PASSWORD %L', :'writer_password')
|
|
227
|
+
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'puppetdb') \gexec
|
|
228
|
+
SELECT format('CREATE ROLE puppetdb_read LOGIN PASSWORD %L', :'reader_password')
|
|
229
|
+
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'puppetdb_read') \gexec
|
|
230
|
+
ALTER DATABASE puppetdb OWNER TO puppetdb;
|
|
231
|
+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
|
232
|
+
GRANT ALL ON SCHEMA public TO puppetdb;
|
|
233
|
+
GRANT USAGE ON SCHEMA public TO puppetdb_read;
|
|
234
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO puppetdb_read;
|
|
235
|
+
ALTER DEFAULT PRIVILEGES FOR ROLE puppetdb IN SCHEMA public GRANT SELECT ON TABLES TO puppetdb_read;
|
|
236
|
+
SQL
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def jetty
|
|
240
|
+
<<~CONFIG
|
|
241
|
+
[jetty]
|
|
242
|
+
host = 0.0.0.0
|
|
243
|
+
port = 8080
|
|
244
|
+
CONFIG
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def database
|
|
248
|
+
<<~CONFIG
|
|
249
|
+
database: {
|
|
250
|
+
subname: "//postgres.empeira.internal:5432/puppetdb"
|
|
251
|
+
username: puppetdb
|
|
252
|
+
password: ${EMPEIRA_DB_PASSWORD}
|
|
253
|
+
}
|
|
254
|
+
read-database: {
|
|
255
|
+
subname: "//postgres.empeira.internal:5432/puppetdb"
|
|
256
|
+
username: puppetdb_read
|
|
257
|
+
password: ${EMPEIRA_READ_PASSWORD}
|
|
258
|
+
}
|
|
259
|
+
CONFIG
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module ControlPlane
|
|
5
|
+
class Health
|
|
6
|
+
RUBY = '/opt/puppetlabs/puppet/bin/ruby'
|
|
7
|
+
|
|
8
|
+
def initialize(runtime:, plan:)
|
|
9
|
+
@runtime = runtime
|
|
10
|
+
@plan = plan
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def wait(key, resources)
|
|
14
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @plan.config.dig('server', 'timeout')
|
|
15
|
+
loop do
|
|
16
|
+
return if ready?(key, resources)
|
|
17
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
18
|
+
raise Error,
|
|
19
|
+
"#{key} did not become ready within server.timeout; inspect empeira status and owned service logs\n" \
|
|
20
|
+
"#{@failure}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sleep 2
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity -- Each service has its own concrete readiness probe.
|
|
28
|
+
def ready?(key, resources)
|
|
29
|
+
resource = resources[key]
|
|
30
|
+
return false unless resources.dig(key, 'state') == 'running'
|
|
31
|
+
|
|
32
|
+
case key
|
|
33
|
+
when 'server' then check(resource, [*tls_curl, 'https://server.empeira.internal:8140/status/v1/simple'],
|
|
34
|
+
'running')
|
|
35
|
+
when 'postgres' then check(resource, ['psql', '-U', 'postgres', '-d', 'puppetdb', '-Atc', 'SELECT 1'], '1')
|
|
36
|
+
when 'puppetdb-backend'
|
|
37
|
+
check(resources['server'], [*http_curl, 'http://puppetdb-backend.empeira.internal:8080/pdb/query/v4/nodes'])
|
|
38
|
+
when 'puppetdb' then puppetdb_ready?(resources['server'])
|
|
39
|
+
when 'dns' then dns_ready?(resources)
|
|
40
|
+
when 'proxy' then proxy_ready?(resources)
|
|
41
|
+
else false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def http_curl
|
|
46
|
+
Server::HTTP.arguments
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def tls_curl
|
|
50
|
+
Server::HTTP.tls_arguments
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def puppetdb_ready?(server)
|
|
56
|
+
path = '/pdb/query/v4/nodes'
|
|
57
|
+
check(server, [*http_curl, "http://puppetdb.empeira.internal:8080#{path}"]) &&
|
|
58
|
+
check(server, [*http_curl, '--cacert', '/etc/puppetlabs/puppet/ssl/certs/ca.pem',
|
|
59
|
+
"https://puppetdb.empeira.internal:8081#{path}"])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def check(resource, arguments, expected = nil)
|
|
63
|
+
return false unless resource
|
|
64
|
+
return check_http?(resource, arguments, expected) if http_probe?(arguments)
|
|
65
|
+
|
|
66
|
+
result = @runtime.service_exec(resource, arguments)
|
|
67
|
+
return true if result.success? && (expected.nil? || result.stdout.strip == expected)
|
|
68
|
+
|
|
69
|
+
@failure = Execution::Diagnostics.native(result, operation: 'Probe control-plane readiness',
|
|
70
|
+
tool: arguments.first)
|
|
71
|
+
false
|
|
72
|
+
rescue Empeira::ExecutionError
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def http_probe?(arguments)
|
|
77
|
+
arguments.first == 'curl' && !arguments.include?('--write-out')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def check_http?(resource, arguments, expected)
|
|
81
|
+
result = @runtime.service_exec(resource, [*arguments[0...-1], '--include', arguments.last])
|
|
82
|
+
body = Server::HTTP.response(result)[:body]
|
|
83
|
+
return true if result.success? && (expected.nil? || body.strip == expected)
|
|
84
|
+
|
|
85
|
+
@failure = Server::HTTP.failure(result, url: arguments.last, operation: 'Probe control-plane readiness')
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def proxy_ready?(resources)
|
|
90
|
+
arguments = %w[curl --silent --max-time 5 --noproxy localhost --proxy
|
|
91
|
+
http://proxy.empeira.internal:3128 --output /dev/null --write-out]
|
|
92
|
+
# Squid's denial must be observable, even when no destination is allowed.
|
|
93
|
+
arguments.push('%{http_code}', 'http://empeira-denied.invalid/') # rubocop:disable Style/FormatStringToken
|
|
94
|
+
check(resources['server'], arguments, '403')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def dns_ready?(resources)
|
|
98
|
+
names = resources.keys.map { |key| @plan.naming.hostname(key) }
|
|
99
|
+
code = 'require "resolv"; ARGV.each { |name| Resolv.getaddress(name) }'
|
|
100
|
+
check(resources['server'], [RUBY, '-e', code, *names])
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|