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,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
class Progress
|
|
5
|
+
Event = Data.define(:percent, :message, :state)
|
|
6
|
+
|
|
7
|
+
def initialize(listener: nil)
|
|
8
|
+
@listener = listener
|
|
9
|
+
@percent = 0
|
|
10
|
+
@message = ''
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(message)
|
|
14
|
+
@active = true
|
|
15
|
+
@percent = 0
|
|
16
|
+
emit(message, :active)
|
|
17
|
+
result = yield
|
|
18
|
+
@percent = 100
|
|
19
|
+
emit('Completed.', :complete)
|
|
20
|
+
result
|
|
21
|
+
rescue StandardError, Interrupt
|
|
22
|
+
emit("Failed: #{@message}", :failed)
|
|
23
|
+
raise
|
|
24
|
+
ensure
|
|
25
|
+
@active = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def stage(percent, message)
|
|
29
|
+
return unless @active
|
|
30
|
+
|
|
31
|
+
raise ArgumentError, 'Progress stages must increase and remain below 100' unless percent.between?(@percent, 99)
|
|
32
|
+
|
|
33
|
+
@percent = percent
|
|
34
|
+
emit(message, :active)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def heartbeat(message = @message)
|
|
38
|
+
emit(message, :active) if @active && !@streaming
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def count(completed, total, message)
|
|
42
|
+
return unless @active
|
|
43
|
+
raise ArgumentError, 'Invalid progress counts' unless completed.between?(0, total)
|
|
44
|
+
|
|
45
|
+
@percent = completed == total ? 100 : [(100.0 * completed / total).round, 99].min
|
|
46
|
+
emit(message, :active)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def warning(message)
|
|
50
|
+
@listener&.call(Event.new(percent: @percent, message: message, state: :warning))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def streaming
|
|
54
|
+
@streaming = true
|
|
55
|
+
emit(@message, :suspended) if @active
|
|
56
|
+
result = yield
|
|
57
|
+
emit(@message, :resumed) if @active
|
|
58
|
+
result
|
|
59
|
+
ensure
|
|
60
|
+
@streaming = false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def emit(message, state)
|
|
66
|
+
@message = message
|
|
67
|
+
@listener&.call(Event.new(percent: @percent, message: message, state: state))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
class ProjectRoot
|
|
8
|
+
attr_reader :path
|
|
9
|
+
|
|
10
|
+
def self.resolve(value, runner: nil, recovery: false)
|
|
11
|
+
value.is_a?(self) ? value : new(value, runner: runner || Execution::Runner.new, recovery: recovery)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(path, runner: Execution::Runner.new, recovery: false)
|
|
15
|
+
directory = Pathname(path).realpath
|
|
16
|
+
raise ConfigurationError, 'project path must be an existing directory' unless directory.directory?
|
|
17
|
+
|
|
18
|
+
@path = git_root(directory, runner).freeze
|
|
19
|
+
require_marker! unless recovery
|
|
20
|
+
freeze
|
|
21
|
+
rescue ExecutionError
|
|
22
|
+
raise ConfigurationError, 'An Empeira Git project is required; ensure Git is installed and accessible.',
|
|
23
|
+
cause: nil
|
|
24
|
+
rescue SystemCallError
|
|
25
|
+
raise ConfigurationError, 'project path must be an existing directory accessible to the current user', cause: nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def git_root(directory, runner)
|
|
31
|
+
result = runner.run('git', arguments: %w[rev-parse --show-toplevel], directory: directory, timeout: 10)
|
|
32
|
+
unless result.success? && !result.stdout.strip.empty?
|
|
33
|
+
raise ConfigurationError, 'An Empeira Git project is required. Enter a Git repository with .empeira.yaml.'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Pathname(result.stdout.chomp).realpath
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def require_marker!
|
|
40
|
+
marker = @path.join('.empeira.yaml')
|
|
41
|
+
return if marker.file?
|
|
42
|
+
|
|
43
|
+
raise ConfigurationError, "Empeira project #{@path} is missing marker #{marker}. " \
|
|
44
|
+
"Opt in with: touch #{Shellwords.escape(marker.to_s)}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Lint/UnusedMethodArgument -- Preserve explicit adapter keyword contracts.
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Providers
|
|
7
|
+
# Adapters implement the protected operations. Public semantics stay shared.
|
|
8
|
+
class Lifecycle
|
|
9
|
+
attr_reader :name, :context, :runner
|
|
10
|
+
|
|
11
|
+
def initialize(name:, context:, runner:)
|
|
12
|
+
@name = name.dup.freeze
|
|
13
|
+
@context = context
|
|
14
|
+
@runner = runner
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def available?
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def inspect_resource(name:)
|
|
22
|
+
resource = lookup(name: name)
|
|
23
|
+
return if resource.nil?
|
|
24
|
+
|
|
25
|
+
verify_resource!(resource, name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run(name:, request:)
|
|
29
|
+
raise AlreadyExists, 'Resource already exists' if inspect_resource(name: name)
|
|
30
|
+
|
|
31
|
+
resource = verify_resource!(create(name: name, request: request), name)
|
|
32
|
+
raise ExecutionError, 'Provider did not start the resource' unless resource.state == :running
|
|
33
|
+
|
|
34
|
+
Result.new(resource: resource, changed: true)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def start(name:)
|
|
38
|
+
change_state(name, :running)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def stop(name:)
|
|
42
|
+
change_state(name, :stopped)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def destroy(name:)
|
|
46
|
+
resource = inspect_resource(name: name)
|
|
47
|
+
return Result.new(resource: nil, changed: false) unless resource
|
|
48
|
+
|
|
49
|
+
remove(resource: resource)
|
|
50
|
+
Result.new(resource: nil, changed: true)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
protected
|
|
54
|
+
|
|
55
|
+
def lookup(name:)
|
|
56
|
+
raise UnavailableFeature, 'This command is not implemented yet.'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def create(name:, request:)
|
|
60
|
+
raise UnavailableFeature, 'This command is not implemented yet.'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def transition(resource:, state:)
|
|
64
|
+
raise UnavailableFeature, 'This command is not implemented yet.'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def remove(resource:)
|
|
68
|
+
raise UnavailableFeature, 'This command is not implemented yet.'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def verify_resource!(resource, expected_name)
|
|
74
|
+
unless resource.is_a?(Resource) && resource.name == expected_name
|
|
75
|
+
raise ExecutionError, 'Provider returned an invalid resource'
|
|
76
|
+
end
|
|
77
|
+
raise OwnershipError, 'Resource belongs to another workspace' unless resource.owner == context.workspace.id
|
|
78
|
+
|
|
79
|
+
resource
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def change_state(name, state)
|
|
83
|
+
resource = inspect_resource(name: name)
|
|
84
|
+
raise NotFound, 'Resource does not exist' unless resource
|
|
85
|
+
return Result.new(resource: resource, changed: false) if resource.state == state
|
|
86
|
+
|
|
87
|
+
updated = verify_resource!(transition(resource: resource, state: state), name)
|
|
88
|
+
raise ExecutionError, 'Provider did not reach the requested state' unless updated.state == state
|
|
89
|
+
|
|
90
|
+
Result.new(resource: updated, changed: true)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Providers
|
|
5
|
+
class Registry
|
|
6
|
+
def initialize(factories)
|
|
7
|
+
@factories = factories.transform_keys(&:to_s).transform_values(&:freeze).freeze
|
|
8
|
+
@names = @factories.keys.freeze
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :names
|
|
12
|
+
|
|
13
|
+
def build(name, **dependencies)
|
|
14
|
+
factory = @factories.fetch(name.to_s) do
|
|
15
|
+
raise Error, "Unknown provider. Choose one of: #{names.join(', ')}"
|
|
16
|
+
end
|
|
17
|
+
factory.call(**dependencies)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Providers
|
|
5
|
+
class NotFound < Error; end
|
|
6
|
+
class AlreadyExists < Error; end
|
|
7
|
+
class OwnershipError < Error; end
|
|
8
|
+
class ExecutionError < Error; end
|
|
9
|
+
|
|
10
|
+
Resource = Data.define(:name, :owner, :state) do
|
|
11
|
+
def initialize(name:, owner:, state:)
|
|
12
|
+
unless [name, owner].all? { |value| value.is_a?(String) && !value.empty? }
|
|
13
|
+
raise Error, 'Resource name and owner must be non-empty strings'
|
|
14
|
+
end
|
|
15
|
+
raise Error, 'Resource state must be running or stopped' unless %i[running stopped].include?(state)
|
|
16
|
+
|
|
17
|
+
super(name: name.dup.freeze, owner: owner.dup.freeze, state: state)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Result = Data.define(:resource, :changed) do
|
|
22
|
+
def initialize(resource:, changed:)
|
|
23
|
+
unless resource.nil? || resource.is_a?(Resource) || resource.is_a?(Network::Resource)
|
|
24
|
+
raise Error, 'Result resource must be a resource or null'
|
|
25
|
+
end
|
|
26
|
+
raise Error, 'Result changed must be boolean' unless [true, false].include?(changed)
|
|
27
|
+
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Lint/UnusedBlockArgument -- Factories share explicit composition dependencies.
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Runtime
|
|
7
|
+
CATALOG = Providers::Registry.new(
|
|
8
|
+
'podman' => ->(**dependencies) { Podman.new(**dependencies) },
|
|
9
|
+
'docker' => ->(**dependencies) { Docker.new(**dependencies) }
|
|
10
|
+
).freeze
|
|
11
|
+
|
|
12
|
+
def self.registry
|
|
13
|
+
CATALOG
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module VM
|
|
18
|
+
CATALOG = Providers::Registry.new(
|
|
19
|
+
'qemu' => ->(**dependencies) { Qemu.new(**dependencies) }
|
|
20
|
+
).freeze
|
|
21
|
+
|
|
22
|
+
def self.registry
|
|
23
|
+
CATALOG
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module Node
|
|
28
|
+
CATALOG = Providers::Registry.new(
|
|
29
|
+
'container' => lambda { |context:, runner:, runtimes:, engines:, build_info:, progress:|
|
|
30
|
+
runtime = runtimes.build(context.container_engine, context: context, runner: runner)
|
|
31
|
+
Container.new(context: context, runner: runner, backend: runtime, build_info: build_info, progress: progress)
|
|
32
|
+
},
|
|
33
|
+
'vm' => lambda { |context:, runner:, runtimes:, engines:, build_info:, progress:|
|
|
34
|
+
engine = engines.build('qemu', context: context, runner: runner)
|
|
35
|
+
runtime = runtimes.build(context.container_engine, context: context, runner: runner)
|
|
36
|
+
VM.new(context: context, runner: runner, backend: engine, runtime: runtime,
|
|
37
|
+
build_info: build_info, progress: progress)
|
|
38
|
+
}
|
|
39
|
+
).freeze
|
|
40
|
+
|
|
41
|
+
def self.registry
|
|
42
|
+
CATALOG
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module Images
|
|
47
|
+
CATALOG = Providers::Registry.new('upstream' => -> { Source.new }, 'custom' => -> { Source.new }).freeze
|
|
48
|
+
|
|
49
|
+
def self.registry
|
|
50
|
+
CATALOG
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# rubocop:enable Lint/UnusedBlockArgument
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
# Reuses helper ownership and observed default-network verification, without host bind mounts.
|
|
6
|
+
module AgentHelper
|
|
7
|
+
def with_agent_helper(image:)
|
|
8
|
+
identity = Empeira::Services::Definition.new(key: "agent-#{SecureRandom.hex(16)}", workspace: context.workspace)
|
|
9
|
+
update_command(agent_helper_arguments(identity, image), operation: 'agent helper creation')
|
|
10
|
+
resource = inspect_service(identity)
|
|
11
|
+
raise Providers::ExecutionError, 'Agent helper creation could not be verified' unless resource
|
|
12
|
+
|
|
13
|
+
update_command(['start', resource.fetch('id')], operation: 'agent helper start')
|
|
14
|
+
verify_helper_network!(identity, resource.fetch('id'))
|
|
15
|
+
yield resource
|
|
16
|
+
ensure
|
|
17
|
+
remove_update_helper(identity, resource&.fetch('id')) if identity
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def agent_helper_arguments(identity, image)
|
|
23
|
+
['create', '--name', identity.name, '--network', update_helper_network,
|
|
24
|
+
'--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
|
|
25
|
+
*%w[CHOWN DAC_OVERRIDE FOWNER SETUID SETGID].flat_map { |capability| ['--cap-add', capability] },
|
|
26
|
+
*label_arguments(identity),
|
|
27
|
+
*UpdateHelper::PROXY_VARIABLES.select { |key| ENV.key?(key) }.flat_map { |key| ['--env', key] },
|
|
28
|
+
'--entrypoint', 'sleep', image, 'infinity']
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Container.include(AgentHelper)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Runtime
|
|
7
|
+
class Unavailable < Providers::ExecutionError; end
|
|
8
|
+
class UnsupportedCapability < Providers::ExecutionError; end
|
|
9
|
+
|
|
10
|
+
# Keep the common network lifecycle and its failure semantics in one adapter boundary.
|
|
11
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
12
|
+
class Container < Interface
|
|
13
|
+
def available?
|
|
14
|
+
check_available!
|
|
15
|
+
true
|
|
16
|
+
rescue Unavailable, UnsupportedCapability
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def check_available!
|
|
21
|
+
@engine_info = json_command(info_arguments, operation: 'engine detection')
|
|
22
|
+
validate_info!(@engine_info)
|
|
23
|
+
true
|
|
24
|
+
rescue Providers::ExecutionError => e
|
|
25
|
+
raise if e.is_a?(UnsupportedCapability)
|
|
26
|
+
|
|
27
|
+
raise Unavailable, "#{name} is unavailable. Install its CLI and start/reach its engine. #{e.message}",
|
|
28
|
+
cause: nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def architecture
|
|
32
|
+
check_available! unless @engine_info
|
|
33
|
+
cpu = @engine_info['Architecture'] || @engine_info.dig('host', 'arch')
|
|
34
|
+
Platform::Facts.new(host_os: 'linux', host_cpu: cpu.to_s).architecture.to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def require_systemd!
|
|
38
|
+
check_available! unless @engine_info
|
|
39
|
+
return if name == 'podman' && @engine_info.dig('host', 'cgroupVersion') == 'v2'
|
|
40
|
+
|
|
41
|
+
raise UnsupportedCapability, 'systemd nodes require Podman with cgroup v2; select node_defaults.init=process'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def node_ssh_publication?
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def capabilities
|
|
49
|
+
{ isolated: true, controlled_egress: false }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def inspect_network(identifier:)
|
|
53
|
+
entry = network_entries.find { |item| [item.fetch('id'), item.fetch('name')].include?(identifier) }
|
|
54
|
+
return unless entry
|
|
55
|
+
|
|
56
|
+
inspect_entry(entry)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# rubocop:disable-next Naming/PredicateMethod -- Verification raises on failure.
|
|
60
|
+
def verify_isolated_network(definition, expected_id:)
|
|
61
|
+
data = json_command(['network', 'inspect', definition.backend_name],
|
|
62
|
+
operation: 'node network verification')
|
|
63
|
+
malformed! unless data.is_a?(Array) && data.size == 1 && data.first.is_a?(Hash)
|
|
64
|
+
binding = normalize_network_binding(data.first)
|
|
65
|
+
definition.verify_ownership!(binding, expected_id: expected_id)
|
|
66
|
+
definition.verify_definition!(binding)
|
|
67
|
+
definition.verify_isolation!(binding)
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_network(definition:)
|
|
72
|
+
definition.policy.require_support!(**capabilities)
|
|
73
|
+
existing = inspect_network(identifier: definition.backend_name)
|
|
74
|
+
if existing
|
|
75
|
+
definition.verify_ownership!(existing)
|
|
76
|
+
definition.verify_definition!(existing)
|
|
77
|
+
definition.verify_isolation!(existing)
|
|
78
|
+
return Providers::Result.new(resource: existing, changed: false)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
mutate_network(definition, 'create', create_arguments(definition))
|
|
82
|
+
resource = inspect_network(identifier: definition.backend_name)
|
|
83
|
+
definition.verify_ownership!(resource)
|
|
84
|
+
definition.verify_isolation!(resource)
|
|
85
|
+
Providers::Result.new(resource: resource, changed: true)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def remove_network(definition:, expected_id: nil)
|
|
89
|
+
resource = inspect_network(identifier: definition.backend_name)
|
|
90
|
+
definition.verify_ownership!(resource, expected_id: expected_id)
|
|
91
|
+
return Providers::Result.new(resource: nil, changed: false) unless resource
|
|
92
|
+
|
|
93
|
+
raise Providers::ExecutionError, "#{name} network still has attached resources; detach them before down" unless
|
|
94
|
+
resource.attachment_count.zero?
|
|
95
|
+
|
|
96
|
+
mutate_network(definition, 'remove', ['network', 'rm', resource.id])
|
|
97
|
+
if inspect_network(identifier: definition.backend_name)
|
|
98
|
+
raise Providers::ExecutionError, "#{name} network removal could not be verified"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
Providers::Result.new(resource: nil, changed: true)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def network_subnets
|
|
105
|
+
network_entries.flat_map do |entry|
|
|
106
|
+
data = json_command(['network', 'inspect', entry.fetch('id')], operation: 'subnet inventory').first
|
|
107
|
+
subnets = data['subnets'] || data.dig('IPAM', 'Config') || []
|
|
108
|
+
subnets.filter_map { |item| item['subnet'] || item['Subnet'] }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def network_details(identifier)
|
|
113
|
+
json_command(['network', 'inspect', identifier], operation: 'peer network inspection').first
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
protected
|
|
117
|
+
|
|
118
|
+
def command(arguments, operation:)
|
|
119
|
+
result = runner.run(name, arguments: arguments, timeout: 30)
|
|
120
|
+
return result if result.success?
|
|
121
|
+
|
|
122
|
+
outcome = result.timed_out ? 'timed out' : "exit #{result.exit_status.inspect}"
|
|
123
|
+
raise Providers::ExecutionError,
|
|
124
|
+
"#{name} #{operation} failed (#{outcome}) for workspace #{context.workspace.id}"
|
|
125
|
+
rescue Empeira::ExecutionError
|
|
126
|
+
raise Providers::ExecutionError,
|
|
127
|
+
"#{name} #{operation} could not execute. Check CLI installation and permissions.",
|
|
128
|
+
cause: nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def json_command(arguments, operation:)
|
|
132
|
+
parse_json(command(arguments, operation: operation).stdout)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def parse_json(output)
|
|
136
|
+
JSON.parse(output)
|
|
137
|
+
rescue JSON::ParserError
|
|
138
|
+
malformed!
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def malformed!
|
|
142
|
+
raise Providers::ExecutionError, "#{name} returned malformed network/engine data", cause: nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def label_arguments(definition)
|
|
146
|
+
definition.labels.flat_map { |key, value| ['--label', "#{key}=#{value}"] }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def validate_entries(entries)
|
|
150
|
+
unless entries.is_a?(Array) && entries.all? do |item|
|
|
151
|
+
item.is_a?(Hash) && %w[id name].all? do |key|
|
|
152
|
+
item[key].is_a?(String) && item[key].match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]*\z/)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
malformed!
|
|
156
|
+
end
|
|
157
|
+
entries
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def inspect_entry(entry)
|
|
163
|
+
data = json_command(['network', 'inspect', entry.fetch('id')], operation: 'network inspection')
|
|
164
|
+
malformed! unless data.is_a?(Array) && data.size == 1 && data.first.is_a?(Hash)
|
|
165
|
+
resource = normalize_network(data.first)
|
|
166
|
+
malformed! unless resource.id == entry.fetch('id') && resource.name == entry.fetch('name')
|
|
167
|
+
resource
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def mutate_network(definition, operation, arguments)
|
|
171
|
+
command(arguments, operation: "network #{operation}")
|
|
172
|
+
rescue Providers::ExecutionError, Interrupt => e
|
|
173
|
+
# A failed command may have committed. Inspect once, never blindly retry.
|
|
174
|
+
observed = inspect_network(identifier: definition.backend_name)
|
|
175
|
+
definition.verify_ownership!(observed)
|
|
176
|
+
raise e if e.is_a?(Interrupt)
|
|
177
|
+
|
|
178
|
+
outcome = observed ? 'present' : 'absent'
|
|
179
|
+
raise Providers::ExecutionError, "#{e.message}. Network observed #{outcome}; run status before retrying.",
|
|
180
|
+
cause: nil
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
module DiagnosticExecution
|
|
6
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- Stream, cancellation receipt and pipe closure share one transaction.
|
|
7
|
+
def diagnostic_exec(resource, arguments, timeout: nil, output: $stdout, error: $stderr)
|
|
8
|
+
token = SecureRandom.hex(16)
|
|
9
|
+
code = Pathname(__dir__).join('../../../resources/nodes/diagnostic_exec.rb').read
|
|
10
|
+
IO.pipe do |input, control|
|
|
11
|
+
native = runner.controlled_stream(name, arguments: ['exec', '--interactive', '--user', '0',
|
|
12
|
+
resource.fetch('id'), ControlPlane::Health::RUBY,
|
|
13
|
+
'-e', code, '--', 'run', token,
|
|
14
|
+
timeout ? timeout.to_s : 'none', *arguments],
|
|
15
|
+
input: input, control: control, output: output, error: error)
|
|
16
|
+
diagnostic_completion(resource, code, token, native)
|
|
17
|
+
rescue SignalException
|
|
18
|
+
native = Execution::Result.new(stdout: '', stderr: '', exit_status: 130, timed_out: false)
|
|
19
|
+
diagnostic_completion(resource, code, token, native)
|
|
20
|
+
raise
|
|
21
|
+
ensure
|
|
22
|
+
control.close unless control.closed?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def valid_diagnostic_completion?(data)
|
|
29
|
+
data.is_a?(Hash) && [true, false].include?(data['timed_out']) &&
|
|
30
|
+
((data['status'].is_a?(Integer) && data['status'].between?(0, 255)) ||
|
|
31
|
+
(data['status'].nil? && data['timed_out']))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def diagnostic_completion(resource, code, token, native)
|
|
35
|
+
result = runner.run(name, arguments: ['exec', '--user', '0', resource.fetch('id'),
|
|
36
|
+
ControlPlane::Health::RUBY, '-e', code, '--', 'read', token], timeout: 10)
|
|
37
|
+
data = JSON.parse(result.stdout) if result.success?
|
|
38
|
+
if valid_diagnostic_completion?(data)
|
|
39
|
+
return Execution::Result.new(stdout: '', stderr: '', exit_status: data['status'],
|
|
40
|
+
timed_out: data['timed_out'])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
details = Execution::Diagnostics.command(native, operation: 'Container diagnostic transport', tool: name)
|
|
44
|
+
raise Error, "Container command completion or cancellation is unconfirmed; node retained\n#{details}"
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
raise Error, 'Container command completion is malformed; node retained', cause: nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Empeira::Runtime::Container.include(Empeira::Runtime::DiagnosticExecution)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Runtime
|
|
5
|
+
class Docker < Container
|
|
6
|
+
def initialize(context:, runner:)
|
|
7
|
+
super(name: 'docker', context: context, runner: runner)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
protected
|
|
11
|
+
|
|
12
|
+
def remote_descriptors(image)
|
|
13
|
+
result = metadata_command(['manifest', 'inspect', '--verbose', image], image: image)
|
|
14
|
+
data = parse_json(result.stdout)
|
|
15
|
+
entries = data.is_a?(Array) ? data : [data]
|
|
16
|
+
entries.map { |entry| entry.is_a?(Hash) ? entry['Descriptor'] : nil }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def info_arguments
|
|
20
|
+
['info', '--format', '{{json .}}']
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_info!(info)
|
|
24
|
+
malformed! unless info.is_a?(Hash) && info['ServerVersion'].is_a?(String)
|
|
25
|
+
version = info['ServerVersion'][/\A\d+/].to_i
|
|
26
|
+
return if version >= 28 && bridge_available?(info['Plugins']) && info['OSType'] == 'linux'
|
|
27
|
+
|
|
28
|
+
raise UnsupportedCapability, 'Docker requires a Linux engine >= 28 with bridge and isolated gateway support'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def bridge_available?(plugins)
|
|
32
|
+
plugins.is_a?(Hash) && plugins['Network'].is_a?(Array) && plugins['Network'].include?('bridge')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def network_entries
|
|
36
|
+
output = command(['network', 'ls', '--no-trunc', '--format', '{{json .}}'],
|
|
37
|
+
operation: 'network inventory').stdout
|
|
38
|
+
entries = output.lines.reject { |line| line.strip.empty? }.map do |line|
|
|
39
|
+
item = parse_json(line)
|
|
40
|
+
malformed! unless item.is_a?(Hash)
|
|
41
|
+
{ 'id' => item['ID'], 'name' => item['Name'] }
|
|
42
|
+
end
|
|
43
|
+
validate_entries(entries)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def create_arguments(definition)
|
|
47
|
+
if definition.is_a?(Network::Egress)
|
|
48
|
+
return ['network', 'create', '--driver', 'bridge', *label_arguments(definition), definition.backend_name]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
['network', 'create', '--driver', 'bridge', '--internal',
|
|
52
|
+
'--opt', 'com.docker.network.bridge.gateway_mode_ipv4=isolated',
|
|
53
|
+
'--opt', 'com.docker.network.bridge.gateway_mode_ipv6=isolated',
|
|
54
|
+
*peer_network_options(definition), *label_arguments(definition), definition.backend_name]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def peer_network_options(definition)
|
|
58
|
+
return [] unless definition.allocation
|
|
59
|
+
|
|
60
|
+
layout = Network::Peer::Layout.new(definition.allocation)
|
|
61
|
+
['--subnet', layout.subnet, '--ip-range', layout.container_pool,
|
|
62
|
+
'--opt', "com.docker.network.bridge.name=ep#{context.workspace.id[0, 10]}"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def isolated_gateways?(options)
|
|
66
|
+
%w[ipv4 ipv6].all? { |family| options["com.docker.network.bridge.gateway_mode_#{family}"] == 'isolated' }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def isolated?(data)
|
|
70
|
+
data['Internal'] && data['Driver'] == 'bridge' && isolated_gateways?(data['Options'])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def normalize_network_binding(data)
|
|
74
|
+
malformed! unless data['Options'].is_a?(Hash) && [true, false].include?(data['Internal'])
|
|
75
|
+
Network::Binding.new(id: data['Id'], name: data['Name'], labels: data['Labels'] || {},
|
|
76
|
+
isolated: isolated?(data))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def normalize_network(data)
|
|
80
|
+
binding = normalize_network_binding(data)
|
|
81
|
+
containers = data['Containers']
|
|
82
|
+
malformed! unless containers.is_a?(Hash)
|
|
83
|
+
Network::Resource.new(**binding.to_h, attachment_count: containers.size)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|