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,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Modules
|
|
5
|
+
class GitSafety
|
|
6
|
+
def initialize(project:, runner:, progress: Progress.new)
|
|
7
|
+
@project = project
|
|
8
|
+
@runner = runner
|
|
9
|
+
@progress = progress
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def verify(path, recommend: false)
|
|
13
|
+
relative = relative_path(path)
|
|
14
|
+
return unless relative
|
|
15
|
+
raise Error, 'The managed module path cannot be the control-repository root' if relative == '.'
|
|
16
|
+
|
|
17
|
+
tracked = git('ls-files', '--cached', '-z', '--', ":(literal)#{relative}")
|
|
18
|
+
raise Error, 'Cannot verify Git-tracked contents of the managed module path' unless tracked.success?
|
|
19
|
+
unless tracked.stdout.empty?
|
|
20
|
+
raise Error,
|
|
21
|
+
'The managed module path contains Git-tracked files; refusing to modify it'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
recommend_ignore(relative) if recommend
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.canonical_path(path)
|
|
28
|
+
return path.realpath if path.exist? || path.symlink?
|
|
29
|
+
|
|
30
|
+
canonical_path(path.parent).join(path.basename)
|
|
31
|
+
rescue SystemCallError
|
|
32
|
+
raise Error, 'Cannot resolve the managed module path for Git safety checks', cause: nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def relative_path(path)
|
|
38
|
+
canonical = self.class.canonical_path(path.expand_path)
|
|
39
|
+
return unless canonical == @project || canonical.to_s.start_with?("#{@project}/")
|
|
40
|
+
|
|
41
|
+
canonical.relative_path_from(@project).to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def recommend_ignore(relative)
|
|
45
|
+
ignored = git('check-ignore', '--quiet', '--', "#{relative}/")
|
|
46
|
+
return if ignored.success? || @recommended
|
|
47
|
+
unless ignored.exit_status == 1 && !ignored.timed_out
|
|
48
|
+
raise Error, 'Cannot determine whether Git ignores the managed module path'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@progress.warning("Recommendation: #{relative}/ is not ignored by Git. Add /#{relative}/ to .gitignore.")
|
|
52
|
+
@recommended = true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def git(*arguments)
|
|
56
|
+
@runner.run('git', arguments: arguments, directory: @project, timeout: 15)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Modules
|
|
7
|
+
# Source acquisition deliberately uses the invoking user's Git/OpenSSH context.
|
|
8
|
+
# Persistent bare repositories are mounted read-only; SSH credentials never enter the helper.
|
|
9
|
+
class HostGit
|
|
10
|
+
def initialize(runner:, project:, progress: Progress.new, passive: false)
|
|
11
|
+
@runner = runner
|
|
12
|
+
@project = project
|
|
13
|
+
@progress = progress
|
|
14
|
+
@passive = passive
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# rubocop:disable-next Metrics/AbcSize -- Acquire each host source and notify its module before network access.
|
|
18
|
+
def prepare(entries, directory:)
|
|
19
|
+
validate!(entries)
|
|
20
|
+
entries.to_h do |entry|
|
|
21
|
+
block_given? ? yield(entry.fetch('name')) : @progress.heartbeat(entry.fetch('name'))
|
|
22
|
+
mirror = source_path(directory, entry.fetch('remote'))
|
|
23
|
+
|
|
24
|
+
result = clone(acquisition_arguments(entry.fetch('remote'), mirror))
|
|
25
|
+
fail_acquisition(entry.fetch('name'), result) unless result.success?
|
|
26
|
+
[entry.fetch('name'), mirror.to_s]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.valid_remote?(remote)
|
|
31
|
+
return false unless remote.is_a?(String) && !remote.start_with?('-') && !remote.match?(/[\x00-\x1f]/)
|
|
32
|
+
return remote.match?(/\A(?:[\w.-]+@)?[\w.-]+:[^:].*\z/) unless remote.include?('://')
|
|
33
|
+
|
|
34
|
+
valid_uri?(URI.parse(remote))
|
|
35
|
+
rescue URI::InvalidURIError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.valid_uri?(uri)
|
|
40
|
+
%w[ssh http https].include?(uri.scheme) && uri.host && !uri.host.start_with?('-') &&
|
|
41
|
+
(uri.scheme == 'ssh' ? uri.password.nil? : uri.userinfo.nil?)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def source_path(directory, remote)
|
|
47
|
+
path = Pathname(directory).join("#{Digest::SHA256.hexdigest(remote)}.git")
|
|
48
|
+
raise Error, 'Git source cache must not be a symlink' if path.symlink?
|
|
49
|
+
|
|
50
|
+
path
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def acquisition_arguments(remote, mirror)
|
|
54
|
+
return ['clone', '--mirror', '--template=', '--', remote, mirror.to_s] unless mirror.exist?
|
|
55
|
+
|
|
56
|
+
['-C', mirror.to_s, 'fetch', '--prune', '--force', '--', remote, '+refs/*:refs/*']
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def fail_acquisition(name, result)
|
|
60
|
+
diagnostic = Execution::Diagnostics.native(result, operation: "Acquire Git source for #{name}", tool: 'git/ssh')
|
|
61
|
+
raise Error, "Module: #{name}\nError: Git source acquisition failed" \
|
|
62
|
+
"#{' (timed out)' if result.timed_out}\n\ngit/ssh output:\n#{diagnostic}", cause: nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def clone(arguments)
|
|
66
|
+
if @passive
|
|
67
|
+
arguments = ['-c', "core.hooksPath=#{File::NULL}", '-c', 'core.fsmonitor=false',
|
|
68
|
+
'-c', 'submodule.recurse=false', '-c', 'protocol.ext.allow=never', *arguments]
|
|
69
|
+
end
|
|
70
|
+
if $stdin.tty? && $stderr.tty?
|
|
71
|
+
@runner.buffered('git', arguments: arguments, directory: @project)
|
|
72
|
+
else
|
|
73
|
+
@runner.run('git', arguments: arguments, directory: @project, timeout: 300)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate!(entries)
|
|
78
|
+
unless entries.is_a?(Array) && entries.all? { |entry| valid_entry?(entry) } &&
|
|
79
|
+
entries.map { |entry| entry['name'] }.uniq.size == entries.size
|
|
80
|
+
raise Error, 'Puppetfile installer returned an invalid Git source plan'
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def valid_entry?(entry)
|
|
85
|
+
entry.is_a?(Hash) && entry.keys.sort == %w[name remote] &&
|
|
86
|
+
entry['name'].is_a?(String) && entry['name'].match?(Configuration::HieraMountSchema::NAME) &&
|
|
87
|
+
self.class.valid_remote?(entry['remote'])
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Modules
|
|
5
|
+
class Image
|
|
6
|
+
def initialize(config, registry: nil)
|
|
7
|
+
@config = config
|
|
8
|
+
@registry = registry
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def files
|
|
12
|
+
%w[Gemfile Gemfile.lock worker.rb].to_h do |name|
|
|
13
|
+
[name, File.read(File.join(Images::Configuration::ROOT, 'modules', name))]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def recipe
|
|
18
|
+
source = Images::Configuration.recipe(@config, registry: @registry)
|
|
19
|
+
"#{source}\n# Managed build inputs: #{Infrastructure::Definition.fingerprint(files)}\n"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reference
|
|
23
|
+
Images::Configuration.local_image(recipe, purpose: 'r10k')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Modules
|
|
5
|
+
class Installer
|
|
6
|
+
def initialize(runtime:, image:, git:, progress: Progress.new)
|
|
7
|
+
@git = git
|
|
8
|
+
@runtime = runtime
|
|
9
|
+
@image = image
|
|
10
|
+
@progress = progress
|
|
11
|
+
@events = Events.new(progress: progress)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# rubocop:disable-next Metrics/AbcSize -- Plan, host acquisition, sync and cleanup share one disposable helper.
|
|
15
|
+
def synchronize(request, destination, state:)
|
|
16
|
+
request.data.fetch('overrides').each { |name| @progress.heartbeat("#{name} (local override)") }
|
|
17
|
+
Dir.mktmpdir('empeira-puppetfile-') do |input|
|
|
18
|
+
write_input(request, input, state)
|
|
19
|
+
@sources = state.sources
|
|
20
|
+
@runtime.with_update_helper(image: @image, input: input, output: destination,
|
|
21
|
+
sources: state.sources, cache: state.cache) do |resource|
|
|
22
|
+
@resource = resource
|
|
23
|
+
execute('plan')
|
|
24
|
+
acquire_git
|
|
25
|
+
execute('sync')
|
|
26
|
+
@events.finish!
|
|
27
|
+
@names | request.data.fetch('overrides')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def execute(mode)
|
|
35
|
+
command = ['ruby', '/opt/empeira-modules/worker.rb', '/work', mode]
|
|
36
|
+
result = @runtime.service_exec(@resource, command, timeout: 600, on_stdout: @events.method(:observe))
|
|
37
|
+
return if result.success?
|
|
38
|
+
|
|
39
|
+
diagnostic = Execution::Diagnostics.native(result, operation: "Puppetfile #{mode}", tool: 'r10k')
|
|
40
|
+
raise Error, "Puppetfile synchronization failed#{' (timed out)' if result.timed_out}:\n" \
|
|
41
|
+
"#{"Module: #{@events.name}\n" if @events.name}#{diagnostic}", cause: nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def acquire_git
|
|
45
|
+
Dir.mktmpdir('empeira-git-sources-') do |directory|
|
|
46
|
+
plan = File.join(directory, 'git-sources.json')
|
|
47
|
+
@runtime.copy_from(@resource, '/work/git-sources.json', plan)
|
|
48
|
+
plan_data = JSON.parse(File.read(plan))
|
|
49
|
+
@names = plan_data.fetch('names')
|
|
50
|
+
acquired = @git.prepare(plan_data.fetch('git'), directory: @sources) { |name| @events.start(name) }
|
|
51
|
+
path = File.join(directory, 'acquired.json')
|
|
52
|
+
File.write(path, JSON.generate(acquired))
|
|
53
|
+
@runtime.copy_to(@resource, path, '/work/acquired.json') unless acquired.empty?
|
|
54
|
+
end
|
|
55
|
+
rescue JSON::ParserError, SystemCallError
|
|
56
|
+
raise Error, 'Cannot transfer the Puppetfile Git source plan or cache locations', cause: nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_input(request, directory, state)
|
|
60
|
+
data = request.data.merge('sources' => state.sources.to_s, 'cache' => state.cache.to_s)
|
|
61
|
+
{ 'Puppetfile' => request.puppetfile, 'request.json' => JSON.generate(data) }.each do |name, content|
|
|
62
|
+
File.write(File.join(directory, name), content, mode: 'wx', perm: 0o600)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Modules
|
|
5
|
+
class Request
|
|
6
|
+
attr_reader :puppetfile, :data
|
|
7
|
+
|
|
8
|
+
def initialize(context:)
|
|
9
|
+
@context = context
|
|
10
|
+
@project = context.project.path
|
|
11
|
+
@config = context.configuration
|
|
12
|
+
@hiera = Configuration::HieraMounts.new(config: @config.fetch('hiera'), project: @project,
|
|
13
|
+
environment: @config.dig('server', 'environment'))
|
|
14
|
+
@overrides = @hiera.entries.select { |entry| entry['type'] == 'module' && entry['status'] == 'available' }
|
|
15
|
+
.map { |entry| entry.fetch('name') }.sort
|
|
16
|
+
load_file
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def warnings
|
|
20
|
+
@hiera.warnings
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def present?
|
|
24
|
+
!puppetfile.nil?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def verify_available!
|
|
28
|
+
return unless present?
|
|
29
|
+
|
|
30
|
+
root = Storage.new(context: @context).root
|
|
31
|
+
return if State.new(context: @context).available?(root, overrides: data.fetch('overrides'))
|
|
32
|
+
|
|
33
|
+
raise Error, "Puppetfile modules are not available.\nRun: empeira update modules"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def load_file
|
|
39
|
+
path = @project.join('Puppetfile')
|
|
40
|
+
return unless path.exist? || path.symlink?
|
|
41
|
+
raise ConfigurationError, 'Puppetfile must be a readable regular file inside the control repository' unless
|
|
42
|
+
path.file? && path.realpath.to_s.start_with?("#{@project}/")
|
|
43
|
+
|
|
44
|
+
verify_modulepath!
|
|
45
|
+
validate_environment_mounts!
|
|
46
|
+
@puppetfile = path.binread
|
|
47
|
+
@data = { 'overrides' => @overrides }
|
|
48
|
+
rescue SystemCallError
|
|
49
|
+
raise ConfigurationError, 'Cannot read Puppetfile or control-repository modules', cause: nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def verify_modulepath!
|
|
53
|
+
environment = @config.dig('server', 'environment')
|
|
54
|
+
Configuration::HieraModulepath.new(project: @project, environment: environment).verify!
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_environment_mounts!
|
|
58
|
+
@hiera.entries.each_with_index do |entry, index|
|
|
59
|
+
next unless entry['status'] == 'available' && entry['type'] == 'environment'
|
|
60
|
+
next unless Configuration::HieraMountSchema.overlap?(entry.fetch('target'), 'modules')
|
|
61
|
+
|
|
62
|
+
raise ConfigurationError,
|
|
63
|
+
"hiera.mounts.#{index}.target overlaps Puppetfile modules; use type: module for local overrides"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Modules
|
|
5
|
+
# Native tooling caches and a small optional list of installed names, never input/tree fingerprints.
|
|
6
|
+
class State
|
|
7
|
+
attr_reader :sources, :cache
|
|
8
|
+
|
|
9
|
+
def initialize(context:)
|
|
10
|
+
@workspace = context.workspace.id
|
|
11
|
+
@directory = context.locations.workspace(context.workspace)
|
|
12
|
+
@path = @directory.join('module-sync.json')
|
|
13
|
+
@sources = @directory.join('r10k', 'sources')
|
|
14
|
+
@cache = @directory.join('r10k', 'cache')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def available?(root, overrides: [])
|
|
18
|
+
return false unless root.directory?
|
|
19
|
+
|
|
20
|
+
names = installed_names(root)
|
|
21
|
+
!names.nil? && names.all? { |name| overrides.include?(name) || populated?(root.join(name)) }
|
|
22
|
+
rescue SystemCallError, JSON::ParserError
|
|
23
|
+
raise Error, 'Cannot read module synchronization metadata', cause: nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def prepare
|
|
27
|
+
[sources.parent, sources, cache].each do |path|
|
|
28
|
+
raise Error, 'r10k cache directories must not be symlinks' if path.symlink?
|
|
29
|
+
|
|
30
|
+
FileUtils.mkdir_p(path, mode: 0o700)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def invalidate
|
|
35
|
+
raise Error, 'Module synchronization metadata must not be a symlink' if @path.symlink?
|
|
36
|
+
|
|
37
|
+
@path.unlink if @path.exist?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def record(root, names)
|
|
41
|
+
data = { 'version' => 1, 'workspace' => @workspace, 'path' => root.to_s, 'names' => names }
|
|
42
|
+
Tempfile.create(['module-sync-', '.json'], @directory) do |file|
|
|
43
|
+
file.chmod(0o600)
|
|
44
|
+
file.write(JSON.generate(data))
|
|
45
|
+
file.flush
|
|
46
|
+
File.rename(file.path, @path)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def installed_names(root)
|
|
53
|
+
raise Error, 'Module synchronization metadata must not be a symlink' if @path.symlink?
|
|
54
|
+
|
|
55
|
+
data = read if @path.exist?
|
|
56
|
+
data && data['path'] == root.to_s ? data['names'] : existing_names(root)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def read
|
|
60
|
+
data = JSON.parse(@path.read)
|
|
61
|
+
raise Error, 'Invalid module synchronization metadata; inspect the workspace state' unless valid?(data)
|
|
62
|
+
|
|
63
|
+
data
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def valid?(data)
|
|
67
|
+
data.is_a?(Hash) && data['workspace'] == @workspace && data['version'] == 1 &&
|
|
68
|
+
data['path'].is_a?(String) && valid_names?(data['names'])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def valid_names?(names)
|
|
72
|
+
names.is_a?(Array) && names.all? { |name| name.is_a?(String) && name.match?(Configuration::HieraMountSchema::NAME) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def existing_names(root)
|
|
76
|
+
names = root.children.filter_map do |path|
|
|
77
|
+
path.basename.to_s if populated?(path) && path.basename.to_s.match?(Configuration::HieraMountSchema::NAME)
|
|
78
|
+
end
|
|
79
|
+
names unless names.empty?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def populated?(path)
|
|
83
|
+
path.directory? && !path.symlink? && !path.children.empty?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Modules
|
|
5
|
+
# The configured path is the module directory itself, never a deployment cache.
|
|
6
|
+
class Storage
|
|
7
|
+
attr_reader :root
|
|
8
|
+
|
|
9
|
+
def initialize(context:)
|
|
10
|
+
project = context.project.path
|
|
11
|
+
@root = GitSafety.canonical_path(Pathname(context.configuration.dig('modules', 'path')).expand_path(project))
|
|
12
|
+
validate_root!(project)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def prepare
|
|
16
|
+
raise Error, 'modules.path must be a directory' if root.exist? && !root.directory?
|
|
17
|
+
|
|
18
|
+
FileUtils.mkdir_p(root)
|
|
19
|
+
rescue SystemCallError
|
|
20
|
+
raise Error, 'Cannot create the configured module directory', cause: nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def validate_root!(project)
|
|
26
|
+
git = project.join('.git')
|
|
27
|
+
return unless root == project || project.to_s.start_with?("#{root}/") || root == root.parent ||
|
|
28
|
+
root == git || root.to_s.start_with?("#{git}/")
|
|
29
|
+
|
|
30
|
+
raise ConfigurationError,
|
|
31
|
+
'modules.path must not contain the control repository, a filesystem root or Git metadata'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Network
|
|
7
|
+
# A temporary authenticated Squid gateway, managed under the workspace transaction.
|
|
8
|
+
# No credentials enter inventory, image definitions, guest seeds or logs.
|
|
9
|
+
# rubocop:disable-next Metrics/ClassLength -- One ownership transaction coordinates temporary gateway recovery.
|
|
10
|
+
class BootstrapProxy
|
|
11
|
+
attr_reader :url
|
|
12
|
+
|
|
13
|
+
def initialize(context:, runtime:, store:)
|
|
14
|
+
@context = context
|
|
15
|
+
@runtime = runtime
|
|
16
|
+
@store = store
|
|
17
|
+
@plan = ControlPlane::Plan.new(context: context)
|
|
18
|
+
@files = @plan.files
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def preflight!(state)
|
|
22
|
+
@plan.subnet = state.fetch('peer_network').fetch('subnet')
|
|
23
|
+
@dns = service_address(state, 'dns')
|
|
24
|
+
service_address(state, 'gateway')
|
|
25
|
+
egress = Network::Egress.new(workspace: @context.workspace, policy: Network::Policy.new)
|
|
26
|
+
resource = @runtime.inspect_network(identifier: egress.backend_name)
|
|
27
|
+
egress.verify_ownership!(resource, expected_id: state.dig('control_plane', 'egress'))
|
|
28
|
+
raise Error, 'Control-plane egress is unavailable; run empeira up first' unless resource
|
|
29
|
+
|
|
30
|
+
egress.verify_isolation!(resource)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def start(state, requirements, source:)
|
|
34
|
+
preflight!(state)
|
|
35
|
+
cleanup(state)
|
|
36
|
+
reject_unrecorded!
|
|
37
|
+
token = SecureRandom.hex(32)
|
|
38
|
+
prepare_policy(token, requirements, source)
|
|
39
|
+
definition = @plan.bootstrap_proxy(dns: @dns)
|
|
40
|
+
@runtime.ensure_image(definition.options.fetch('image'), recipe: definition.options['recipe'])
|
|
41
|
+
resource = create(state, definition)
|
|
42
|
+
address = connect_and_verify(resource, definition, state)
|
|
43
|
+
@url = "http://bootstrap:#{token}@#{address}:3128"
|
|
44
|
+
wait_ready(state, address)
|
|
45
|
+
address
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cleanup(state)
|
|
49
|
+
return unless state.key?('bootstrap_proxy')
|
|
50
|
+
|
|
51
|
+
entry = state.fetch('bootstrap_proxy')
|
|
52
|
+
definition = Services::Definition.new(key: 'bootstrap-proxy', workspace: @context.workspace)
|
|
53
|
+
remove_owned(definition, entry.fetch('id'))
|
|
54
|
+
policy = @files.path('bootstrap-squid.conf')
|
|
55
|
+
File.unlink(policy) if File.exist?(policy) || File.symlink?(policy)
|
|
56
|
+
state.delete('bootstrap_proxy')
|
|
57
|
+
@store.write(state)
|
|
58
|
+
@url = nil
|
|
59
|
+
rescue SystemCallError
|
|
60
|
+
raise Error, 'Cannot remove bootstrap proxy credentials; cleanup is incomplete and Puppet was not run'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def connect_and_verify(resource, definition, state)
|
|
66
|
+
@runtime.start_service(resource)
|
|
67
|
+
observed = @runtime.inspect_service(definition, expected_id: resource.fetch('id'))
|
|
68
|
+
address = verify_endpoint(observed)
|
|
69
|
+
Gateway.new(context: @context, runtime: @runtime, state: state).route(observed)
|
|
70
|
+
address
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def reject_unrecorded!
|
|
74
|
+
definition = Services::Definition.new(key: 'bootstrap-proxy', workspace: @context.workspace)
|
|
75
|
+
return unless @runtime.inspect_service(definition)
|
|
76
|
+
|
|
77
|
+
raise Providers::OwnershipError, 'Unrecorded bootstrap proxy exists; refusing adoption'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def remove_owned(definition, id)
|
|
81
|
+
@runtime.remove_service(definition, expected_id: id)
|
|
82
|
+
rescue Providers::ExecutionError
|
|
83
|
+
resource = @runtime.inspect_service(definition, expected_id: id)
|
|
84
|
+
@runtime.stop_service(resource) if resource && resource['state'] == 'running'
|
|
85
|
+
raise
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def create(state, definition)
|
|
89
|
+
state['bootstrap_proxy'] = { 'id' => nil }
|
|
90
|
+
@store.write(state)
|
|
91
|
+
resource = @runtime.create_service(definition)
|
|
92
|
+
state['bootstrap_proxy']['id'] = resource.fetch('id')
|
|
93
|
+
@store.write(state)
|
|
94
|
+
resource
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def service_address(state, key)
|
|
98
|
+
expected = state.dig('control_plane', 'services', key, 'id')
|
|
99
|
+
raise Error, "#{key} is not recorded; run empeira up first" unless expected
|
|
100
|
+
|
|
101
|
+
resource = @runtime.inspect_service(@plan.definitions.fetch(key), expected_id: expected)
|
|
102
|
+
address = resource&.dig('networks', @plan.network, 'IPAddress')
|
|
103
|
+
raise Error, "#{key} is unavailable; run empeira up first" unless resource&.dig('state') == 'running' && address
|
|
104
|
+
|
|
105
|
+
IPAddr.new(address).to_s
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def prepare_policy(token, requirements, source)
|
|
109
|
+
authorization = ["bootstrap:#{token}"].pack('m0')
|
|
110
|
+
config = { 'global' => requirements.destinations }
|
|
111
|
+
policy = ProxyPolicy.new(config, authorization: authorization)
|
|
112
|
+
content = policy.configuration.gsub('@EMPEIRA_DNS@', @dns)
|
|
113
|
+
IPAddr.new(source)
|
|
114
|
+
content = "acl provisioning_vm src #{source}/32\nhttp_access deny !provisioning_vm\n#{content}"
|
|
115
|
+
@files.write('bootstrap-squid.conf', content)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def verify_endpoint(resource)
|
|
119
|
+
expected = [@plan.network]
|
|
120
|
+
unless resource && resource['state'] == 'running' && resource.fetch('networks').keys.sort == expected &&
|
|
121
|
+
unpublished?(resource)
|
|
122
|
+
raise Providers::OwnershipError, 'Bootstrap proxy has unexpected network attachments or published ports'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
IPAddr.new(resource.fetch('networks').fetch(@plan.network).fetch('IPAddress')).to_s
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def unpublished?(resource)
|
|
129
|
+
resource.fetch('ports').empty? && resource.fetch('published_ports').values.all?(&:nil?)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def readiness_probe
|
|
133
|
+
's=TCPSocket.new(ARGV[0],3128);s.write("GET http://bootstrap.invalid/ HTTP/1.0\r\n\r\n");' \
|
|
134
|
+
'line=s.gets.to_s;abort(line) unless line.match?(/HTTP\/1\.[01] 403 /)'
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def wait_ready(state, address)
|
|
138
|
+
server = @runtime.inspect_service(@plan.definitions.fetch('server'),
|
|
139
|
+
expected_id: state.dig('control_plane', 'services', 'server', 'id'))
|
|
140
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 30
|
|
141
|
+
loop do
|
|
142
|
+
result = @runtime.service_exec(server, [Node::Certificates::RUBY, '-rsocket', '-e',
|
|
143
|
+
readiness_probe, address], timeout: 3)
|
|
144
|
+
return if result.success?
|
|
145
|
+
|
|
146
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
147
|
+
detail = Execution::Diagnostics.native(result, operation: 'Bootstrap proxy readiness', tool: 'ruby',
|
|
148
|
+
url: 'http://bootstrap.invalid/')
|
|
149
|
+
raise Error, "Bootstrap proxy did not become reachable on the peer network\n#{detail}", cause: nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
sleep 0.2
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
class Definition
|
|
6
|
+
LABEL_PREFIX = 'io.empeira'
|
|
7
|
+
attr_reader :workspace, :policy
|
|
8
|
+
attr_accessor :allocation
|
|
9
|
+
|
|
10
|
+
def initialize(workspace:, policy:, revision: 2)
|
|
11
|
+
@workspace = workspace
|
|
12
|
+
@policy = policy
|
|
13
|
+
@revision = revision
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def identity
|
|
17
|
+
"#{workspace.id}:environment"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def backend_name
|
|
21
|
+
"empeira-#{workspace.id}-network"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def labels
|
|
25
|
+
ownership_labels.merge('io.empeira.definition' => Infrastructure::Definition.fingerprint(to_h))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ownership_labels
|
|
29
|
+
{ "#{LABEL_PREFIX}.managed-by" => 'empeira', "#{LABEL_PREFIX}.workspace" => workspace.id,
|
|
30
|
+
"#{LABEL_PREFIX}.purpose" => 'environment-network' }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def verify_ownership!(resource, expected_id: nil)
|
|
34
|
+
return unless resource
|
|
35
|
+
|
|
36
|
+
owned = resource.name == backend_name && ownership_labels.all? { |key, value| resource.labels[key] == value }
|
|
37
|
+
return if owned && (expected_id.nil? || resource.id == expected_id)
|
|
38
|
+
|
|
39
|
+
raise Providers::OwnershipError, "Network ownership or recorded ID conflicts for workspace #{workspace.id}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def verify_definition!(resource)
|
|
43
|
+
return if resource.labels['io.empeira.definition'] == labels.fetch('io.empeira.definition')
|
|
44
|
+
|
|
45
|
+
raise Error, 'Network definition is stale or unknown. Run empeira down before recreating it.'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def verify_isolation!(resource)
|
|
49
|
+
return if resource&.isolated == true
|
|
50
|
+
|
|
51
|
+
raise UnsupportedPolicy, "Cannot verify isolated network for workspace #{workspace.id}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_h
|
|
55
|
+
{ 'identity' => identity, 'backend_name' => backend_name,
|
|
56
|
+
'policy' => { 'isolated' => true, 'controlled_egress' => false },
|
|
57
|
+
'labels' => ownership_labels, 'revision' => @revision }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|