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,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Infrastructure
|
|
5
|
+
# Keep version gating and the single workspace mutation boundary together.
|
|
6
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
7
|
+
class Service
|
|
8
|
+
def initialize(context:, runner:, runtimes:, build_info:, store: Store.new(context: context),
|
|
9
|
+
progress: Progress.new)
|
|
10
|
+
@progress = progress
|
|
11
|
+
@context = context
|
|
12
|
+
@runner = runner
|
|
13
|
+
@runtimes = runtimes
|
|
14
|
+
@build_info = build_info
|
|
15
|
+
@store = store
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# rubocop:disable-next Metrics/AbcSize -- Read-only artifact validation precedes runtime mutation.
|
|
19
|
+
def up
|
|
20
|
+
require_compatible!
|
|
21
|
+
@store.with_lock do
|
|
22
|
+
@progress.stage(5, 'Checking runtime and workspace ownership...')
|
|
23
|
+
state = @store.load
|
|
24
|
+
runtime = mutation_runtime(state)
|
|
25
|
+
resource = observe(runtime, state)
|
|
26
|
+
verify_current!(state, resource)
|
|
27
|
+
plane = control_plane(runtime)
|
|
28
|
+
plane.preflight
|
|
29
|
+
@progress.stage(15, 'Preparing isolated network...')
|
|
30
|
+
result = ensure_network(runtime, state, resource)
|
|
31
|
+
changed = plane.up
|
|
32
|
+
changed = reconcile_nodes || changed
|
|
33
|
+
|
|
34
|
+
Providers::Result.new(resource: result.resource, changed: result.changed || changed)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def browser
|
|
39
|
+
require_compatible!
|
|
40
|
+
state = @store.load
|
|
41
|
+
raise Error, browser_workspace_error unless state
|
|
42
|
+
|
|
43
|
+
@store.with_lock do
|
|
44
|
+
state = @store.load
|
|
45
|
+
raise Error, browser_workspace_error unless state
|
|
46
|
+
|
|
47
|
+
@progress.stage(5, 'Checking runtime and workspace ownership...')
|
|
48
|
+
runtime = mutation_runtime(state)
|
|
49
|
+
resource = observe(runtime, state)
|
|
50
|
+
raise Error, browser_network_error unless resource
|
|
51
|
+
|
|
52
|
+
verify_current!(state, resource)
|
|
53
|
+
control_plane(runtime).browser
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# rubocop:disable-next Metrics/AbcSize -- Keep version checks and retained-storage teardown in one lock boundary.
|
|
58
|
+
def down
|
|
59
|
+
require_compatible!
|
|
60
|
+
@store.with_lock do
|
|
61
|
+
@progress.stage(5, 'Checking runtime and workspace ownership...')
|
|
62
|
+
state = @store.load
|
|
63
|
+
runtime = mutation_runtime(state)
|
|
64
|
+
resource = observe(runtime, state)
|
|
65
|
+
require_no_nodes!(state)
|
|
66
|
+
|
|
67
|
+
changed = control_plane(runtime).down
|
|
68
|
+
result = runtime.remove_network(definition: definition.network, expected_id: resource&.id)
|
|
69
|
+
retain_storage
|
|
70
|
+
Providers::Result.new(resource: result.resource, changed: result.changed || changed)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def destroy
|
|
75
|
+
Cleanup.new(context: @context, runner: @runner, runtimes: @runtimes, progress: @progress).destroy
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def status
|
|
79
|
+
status = Status.new(context: @context, build_info: @build_info, definition: definition)
|
|
80
|
+
state = @store.load
|
|
81
|
+
status.record(state)
|
|
82
|
+
runtime = runtime_for(state&.fetch('runtime') || @context.container_engine)
|
|
83
|
+
runtime.check_available!
|
|
84
|
+
status.observe(observe(runtime, state))
|
|
85
|
+
health_report(status, runtime, state)
|
|
86
|
+
rescue Error => e
|
|
87
|
+
status.report.merge('Infrastructure' => 'unknown', 'Diagnostic' => e.message)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def definition
|
|
93
|
+
@definition ||= Definition.new(context: @context)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def browser_workspace_error
|
|
97
|
+
"Workspace is not initialized. Start it with 'empeira up' first."
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def browser_network_error
|
|
101
|
+
"Required workspace network is not running. Start the workspace with 'empeira up' first."
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def reconcile_nodes
|
|
105
|
+
Node::Service.new(context: @context, runner: @runner, runtimes: @runtimes,
|
|
106
|
+
build_info: @build_info, progress: @progress).reconcile(state: @store.load)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def destroy_nodes(runtime, state)
|
|
110
|
+
Node::Container.new(context: @context, runner: @runner, backend: runtime).destroy_all(state: state)
|
|
111
|
+
engine = VM.registry.build('qemu', context: @context, runner: @runner)
|
|
112
|
+
Node::VM.new(context: @context, runner: @runner, backend: engine, runtime: runtime)
|
|
113
|
+
.destroy_all(state: state)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def require_no_nodes!(state)
|
|
117
|
+
return unless state&.fetch('nodes', {})&.any?
|
|
118
|
+
|
|
119
|
+
raise Error, 'Nodes exist; destroy them before empeira down, or use empeira destroy for complete removal'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def health_report(status, runtime, state)
|
|
123
|
+
report = status.report.merge(control_plane(runtime).status(state: state))
|
|
124
|
+
if report['Infrastructure'] == 'up' && report['Control plane'] == 'degraded'
|
|
125
|
+
report['Infrastructure'] = 'degraded'
|
|
126
|
+
end
|
|
127
|
+
report
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def allocate_network(runtime, state)
|
|
131
|
+
@peer_network = state&.fetch('peer_network', nil) || {
|
|
132
|
+
'subnet' => Network::Peer::Allocation.new(context: @context, runtime: runtime, runner: @runner).choose
|
|
133
|
+
}
|
|
134
|
+
definition.network.allocation = @peer_network.fetch('subnet')
|
|
135
|
+
definition.plan.validate_network_redirects!(subnet: @peer_network.fetch('subnet'))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def ensure_network(runtime, state, resource)
|
|
139
|
+
allocate_network(runtime, state)
|
|
140
|
+
definition.network.policy.require_support!(**runtime.capabilities)
|
|
141
|
+
persist(state, resource)
|
|
142
|
+
result = if resource
|
|
143
|
+
Providers::Result.new(resource: resource, changed: false)
|
|
144
|
+
else
|
|
145
|
+
runtime.create_network(definition: definition.network)
|
|
146
|
+
end
|
|
147
|
+
persist(state, result.resource)
|
|
148
|
+
result
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def retain_storage
|
|
152
|
+
retained = @store.load
|
|
153
|
+
if retained&.dig('control_plane', 'volumes')&.any?
|
|
154
|
+
retained['resources']['network']['id'] = nil
|
|
155
|
+
@store.write(retained)
|
|
156
|
+
else
|
|
157
|
+
@store.clear
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def control_plane(runtime)
|
|
162
|
+
ControlPlane::Controller.new(context: @context, runtime: runtime, store: @store, runner: @runner,
|
|
163
|
+
plan: definition.plan, progress: @progress)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def requirement
|
|
167
|
+
@context.configuration.fetch('requirements').fetch('empeira')
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def require_compatible!
|
|
171
|
+
@build_info.require_compatible!(requirement)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def runtime_for(name)
|
|
175
|
+
@runtimes.build(name, context: @context, runner: @runner)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def runtime_mismatch?(state)
|
|
179
|
+
state && state.fetch('runtime') != @context.container_engine
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def mutation_runtime(state)
|
|
183
|
+
if runtime_mismatch?(state)
|
|
184
|
+
raise Error, "Existing infrastructure belongs to #{state.fetch('runtime')}, configured runtime is " \
|
|
185
|
+
"#{@context.container_engine}. Restore the owning runtime and remove retained infrastructure " \
|
|
186
|
+
'before switching. ' \
|
|
187
|
+
'Runtime migration is not implemented.'
|
|
188
|
+
end
|
|
189
|
+
runtime = runtime_for(@context.container_engine)
|
|
190
|
+
runtime.check_available!
|
|
191
|
+
runtime
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def observe(runtime, state)
|
|
195
|
+
network = definition.network
|
|
196
|
+
expected_id = state&.dig('resources', 'network', 'id')
|
|
197
|
+
resource = runtime.inspect_network(identifier: network.backend_name)
|
|
198
|
+
network.verify_ownership!(resource, expected_id: expected_id)
|
|
199
|
+
if resource.nil? && expected_id && runtime.inspect_network(identifier: expected_id)
|
|
200
|
+
raise Providers::OwnershipError,
|
|
201
|
+
'Recorded network ID exists under a different name; automatic removal refused'
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
resource
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def verify_current!(state, resource)
|
|
208
|
+
if state && (state.dig('definition', 'revision') != definition.metadata['revision'] ||
|
|
209
|
+
state.dig('component_fingerprints', 'network') != definition.component_fingerprints['network'])
|
|
210
|
+
raise Error,
|
|
211
|
+
'Infrastructure definition is stale. Inspect status, then explicitly run ' \
|
|
212
|
+
'empeira destroy and empeira up.'
|
|
213
|
+
end
|
|
214
|
+
return unless resource
|
|
215
|
+
|
|
216
|
+
definition.network.verify_definition!(resource)
|
|
217
|
+
definition.network.verify_isolation!(resource)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# The inventory envelope is deliberately written atomically in one operation.
|
|
221
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
222
|
+
def persist(previous, resource)
|
|
223
|
+
now = Time.now.utc.iso8601
|
|
224
|
+
@store.write(
|
|
225
|
+
'schema_version' => Store::SCHEMA_VERSION, 'workspace' => @context.workspace.id,
|
|
226
|
+
'runtime' => @context.container_engine,
|
|
227
|
+
'resources' => { 'network' => { 'logical_identity' => definition.network.identity,
|
|
228
|
+
'name' => definition.network.backend_name, 'id' => resource&.id } },
|
|
229
|
+
'fingerprint' => definition.fingerprint, 'definition' => definition.metadata,
|
|
230
|
+
'component_fingerprints' => definition.component_fingerprints, 'peer_network' => @peer_network,
|
|
231
|
+
'created_at' => previous&.fetch('created_at') || now, 'reconciled_at' => now,
|
|
232
|
+
**previous.to_h.slice('nodes', 'retired_certificates', 'control_plane')
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Infrastructure
|
|
5
|
+
class Status
|
|
6
|
+
attr_reader :report
|
|
7
|
+
|
|
8
|
+
def initialize(context:, build_info:, definition:)
|
|
9
|
+
@context = context
|
|
10
|
+
@definition = definition
|
|
11
|
+
requirement = context.configuration.fetch('requirements').fetch('empeira')
|
|
12
|
+
@report = {
|
|
13
|
+
'Workspace' => context.workspace.id, 'Platform' => "#{context.platform.os}/#{context.platform.architecture}",
|
|
14
|
+
'Empeira version' => build_info.version, 'Project Empeira requirement' => requirement || 'none',
|
|
15
|
+
'Version requirement' => build_info.requirement_status(requirement),
|
|
16
|
+
'Configured runtime' => context.container_engine, 'Owning runtime' => 'unknown',
|
|
17
|
+
'Network' => 'unknown', 'Infrastructure' => 'unknown', 'Infrastructure fingerprint' => 'unknown'
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def record(state)
|
|
22
|
+
@state = state
|
|
23
|
+
report['Owning runtime'] = state&.fetch('runtime') || 'none recorded'
|
|
24
|
+
report['Infrastructure fingerprint'] = fingerprint_status
|
|
25
|
+
report['Runtime ownership'] =
|
|
26
|
+
if state && state.fetch('runtime') != @context.container_engine
|
|
27
|
+
'mismatch; restore owning runtime for cleanup'
|
|
28
|
+
else
|
|
29
|
+
'consistent'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def observe(resource)
|
|
34
|
+
report['Network'] = network_status(resource)
|
|
35
|
+
report['Infrastructure'] = infrastructure_status(resource)
|
|
36
|
+
return unless resource
|
|
37
|
+
return if resource.labels['io.empeira.definition'] == @definition.network.labels.fetch('io.empeira.definition')
|
|
38
|
+
|
|
39
|
+
report['Infrastructure fingerprint'] = 'stale runtime definition'
|
|
40
|
+
report['Infrastructure'] = 'stale'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def network_status(resource)
|
|
46
|
+
return 'absent' unless resource
|
|
47
|
+
|
|
48
|
+
resource.isolated ? 'isolated' : 'unsafe isolation'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fingerprint_status
|
|
52
|
+
return 'unrecorded' unless @state
|
|
53
|
+
|
|
54
|
+
@state.fetch('fingerprint') == @definition.fingerprint ? 'current' : 'stale'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def missing_status
|
|
58
|
+
return 'down; persistent data retained' if @state&.dig('control_plane', 'stopped')
|
|
59
|
+
|
|
60
|
+
@state ? 'missing; reconciliation required' : 'down'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def infrastructure_status(resource)
|
|
64
|
+
return missing_status unless resource
|
|
65
|
+
|
|
66
|
+
return 'unsafe; isolation cannot be verified' unless resource.isolated
|
|
67
|
+
return 'recoverable; local state incomplete' unless @state&.dig('resources', 'network', 'id')
|
|
68
|
+
|
|
69
|
+
fingerprint_status == 'current' ? 'up' : 'stale'
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module Empeira
|
|
8
|
+
module Infrastructure
|
|
9
|
+
class StateError < Error; end
|
|
10
|
+
class Locked < Error; end
|
|
11
|
+
|
|
12
|
+
# Keep atomic persistence and the complete inventory envelope validator together.
|
|
13
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
14
|
+
class Store
|
|
15
|
+
SCHEMA_VERSION = 2
|
|
16
|
+
attr_reader :directory
|
|
17
|
+
|
|
18
|
+
def initialize(context:, recovery: false)
|
|
19
|
+
@recovery = recovery
|
|
20
|
+
@workspace_id = context.workspace.id
|
|
21
|
+
@state_root = context.locations.state
|
|
22
|
+
@network = Network::Definition.new(workspace: context.workspace, policy: Network::Policy.new)
|
|
23
|
+
@directory = context.locations.workspace(context.workspace)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# rubocop:disable-next Metrics/MethodLength -- Stable lock ownership, acquisition and release share an ensure boundary.
|
|
27
|
+
def with_lock
|
|
28
|
+
verify_paths!(inventory: false)
|
|
29
|
+
FileUtils.mkdir_p(directory, mode: 0o700)
|
|
30
|
+
verify_paths!(inventory: false)
|
|
31
|
+
File.open(directory.join('mutation.lock'), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock|
|
|
32
|
+
verify_lock!(lock)
|
|
33
|
+
unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
34
|
+
raise Locked, "Another Empeira mutation owns workspace #{@workspace_id}. Wait and retry."
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
yield
|
|
39
|
+
ensure
|
|
40
|
+
lock.flock(File::LOCK_UN)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
rescue SystemCallError
|
|
44
|
+
raise StateError, 'Cannot access workspace mutation lock', cause: nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def load
|
|
48
|
+
verify_paths!
|
|
49
|
+
data = JSON.parse(File.read(path))
|
|
50
|
+
validate!(data)
|
|
51
|
+
data
|
|
52
|
+
rescue Errno::ENOENT
|
|
53
|
+
raise StateError, 'Infrastructure state is an unreadable symlink' if path.symlink?
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
rescue JSON::ParserError, SystemCallError
|
|
57
|
+
raise StateError, 'Cannot read infrastructure state. Preserve it and investigate before retrying.', cause: nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def write(data)
|
|
61
|
+
verify_paths!
|
|
62
|
+
validate!(data)
|
|
63
|
+
Tempfile.create(['state-', '.json'], directory) do |file|
|
|
64
|
+
file.chmod(0o600)
|
|
65
|
+
file.write(JSON.pretty_generate(data))
|
|
66
|
+
file.flush
|
|
67
|
+
file.fsync
|
|
68
|
+
File.rename(file.path, path)
|
|
69
|
+
end
|
|
70
|
+
rescue SystemCallError, IOError
|
|
71
|
+
raise StateError, 'Cannot persist infrastructure state. Run status before retrying.', cause: nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def clear
|
|
75
|
+
verify_paths!
|
|
76
|
+
File.unlink(path) if path.exist?
|
|
77
|
+
rescue SystemCallError
|
|
78
|
+
raise StateError, 'Cannot clear infrastructure state. Run status before retrying.', cause: nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def verify_lock!(lock)
|
|
84
|
+
metadata = lock.stat
|
|
85
|
+
return if metadata.file? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
|
|
86
|
+
|
|
87
|
+
raise StateError, 'Workspace mutation lock ownership is invalid'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def verify_paths!(inventory: true)
|
|
91
|
+
[@state_root, directory.dirname, directory].each do |entry|
|
|
92
|
+
verify_directory!(entry)
|
|
93
|
+
end
|
|
94
|
+
if directory.exist? && !directory.lstat.mode.nobits?(0o077)
|
|
95
|
+
raise StateError, 'Workspace state directory is not private'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
verify_inventory_file! if inventory
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def verify_directory!(entry)
|
|
102
|
+
return unless entry.exist? || entry.symlink?
|
|
103
|
+
|
|
104
|
+
raise StateError, 'Workspace state directory is an unsafe symlink' if entry.symlink?
|
|
105
|
+
|
|
106
|
+
metadata = entry.lstat
|
|
107
|
+
return if metadata.directory? && metadata.uid == Process.uid
|
|
108
|
+
|
|
109
|
+
raise StateError, 'Workspace state directory ownership or path is unsafe'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def verify_inventory_file!
|
|
113
|
+
return unless path.exist? || path.symlink?
|
|
114
|
+
|
|
115
|
+
raise StateError, 'Infrastructure state is an unsafe symlink' if path.symlink?
|
|
116
|
+
|
|
117
|
+
metadata = path.lstat
|
|
118
|
+
return if metadata.file? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
|
|
119
|
+
|
|
120
|
+
raise StateError, 'Infrastructure state ownership or path is unsafe'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def path
|
|
124
|
+
directory.join('infrastructure.json')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def validate!(data)
|
|
128
|
+
return RecoveryInventory.validate!(data, workspace: @workspace_id) if @recovery
|
|
129
|
+
|
|
130
|
+
unless data.is_a?(Hash) && data['schema_version'] == SCHEMA_VERSION && data['workspace'] == @workspace_id
|
|
131
|
+
raise StateError,
|
|
132
|
+
'Invalid infrastructure state schema or workspace. Preserve the state and inspect its ownership.'
|
|
133
|
+
end
|
|
134
|
+
if legacy_vm_network?(data)
|
|
135
|
+
raise StateError,
|
|
136
|
+
'Legacy VM gateway state: use the previous Empeira version to destroy this workspace, then recreate it'
|
|
137
|
+
end
|
|
138
|
+
validate_vm_layouts!(data)
|
|
139
|
+
return if valid_inventory?(data)
|
|
140
|
+
|
|
141
|
+
raise StateError, 'Invalid infrastructure inventory. Preserve it for recovery.'
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def validate_vm_layouts!(data)
|
|
145
|
+
nodes = data['nodes']
|
|
146
|
+
return unless nodes.is_a?(Hash)
|
|
147
|
+
|
|
148
|
+
nodes.each_value do |record|
|
|
149
|
+
VM::Management.validate!(record) if record.is_a?(Hash) && record['provider'] == 'vm'
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def legacy_vm_network?(data)
|
|
154
|
+
return true if data.key?('vm_gateway')
|
|
155
|
+
return false if data.key?('peer_network') || !data['nodes'].is_a?(Hash)
|
|
156
|
+
|
|
157
|
+
data['nodes'].values.any? { |record| record.is_a?(Hash) && record['provider'] == 'vm' }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def valid_inventory?(data)
|
|
161
|
+
Runtime.registry.names.include?(data['runtime']) &&
|
|
162
|
+
data['resources'].is_a?(Hash) && data['resources'].keys == ['network'] &&
|
|
163
|
+
valid_network?(data['resources']['network']) &&
|
|
164
|
+
valid_definition?(data) && valid_extensions?(data)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def valid_extensions?(data)
|
|
168
|
+
valid_timestamps?(data) && valid_control_plane?(data) &&
|
|
169
|
+
(!data.key?('nodes') || Node::Inventory.valid?(data['nodes'])) && valid_peer_network?(data) &&
|
|
170
|
+
valid_bootstrap_proxy?(data['bootstrap_proxy']) && RecoveryInventory.valid_retired_certificates?(data)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def valid_peer_network?(data)
|
|
174
|
+
return true unless data.key?('peer_network')
|
|
175
|
+
|
|
176
|
+
entry = data['peer_network']
|
|
177
|
+
return false unless entry.is_a?(Hash) && entry.keys == ['subnet']
|
|
178
|
+
|
|
179
|
+
layout = Network::Peer::Layout.new(entry['subnet'])
|
|
180
|
+
valid_peer_leases?(data, layout) && valid_container_leases?(data, layout)
|
|
181
|
+
rescue Infrastructure::StateError
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def valid_peer_leases?(data, layout)
|
|
186
|
+
leases = data.fetch('nodes', {}).values.select { |record| record['provider'] == 'vm' }
|
|
187
|
+
.map { |record| record['peer'] }
|
|
188
|
+
leases.all? { |lease| layout.valid_lease?(lease) && valid_transport?(lease) } &&
|
|
189
|
+
leases.map { |lease| lease['ip'] }.uniq.size == leases.size
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def valid_transport?(lease)
|
|
193
|
+
%w[LinuxPodman DockerAdapter PodmanMachine].include?(lease['backend']) &&
|
|
194
|
+
lease['dns'].is_a?(String) && IPAddr.new(lease['dns']).ipv4? &&
|
|
195
|
+
Node::Inventory.valid_id?(lease['machine']) &&
|
|
196
|
+
Node::Inventory.valid_certificate?(lease['helper_sha256'])
|
|
197
|
+
rescue IPAddr::InvalidAddressError
|
|
198
|
+
false
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def valid_container_leases?(data, layout)
|
|
202
|
+
records = data.fetch('nodes', {}).values.select { |record| record['provider'] == 'container' }
|
|
203
|
+
addresses = records.map { |record| record.dig('definition', 'ip') }
|
|
204
|
+
addresses.uniq.size == addresses.size && addresses.all? do |ip|
|
|
205
|
+
(96..127).any? { |offset| layout.address(offset) == ip }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def valid_bootstrap_proxy?(entry)
|
|
210
|
+
entry.nil? || (entry.is_a?(Hash) && entry.keys == ['id'] && valid_resource_id?(entry['id']))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def valid_control_plane?(data)
|
|
214
|
+
plane = data['control_plane']
|
|
215
|
+
return true if plane.nil?
|
|
216
|
+
return false unless plane.is_a?(Hash)
|
|
217
|
+
|
|
218
|
+
!plane.key?('provider') &&
|
|
219
|
+
valid_records?(plane['services'], /\A[a-z][a-z0-9]*(?:-[a-z0-9]+)*\z/) &&
|
|
220
|
+
valid_records?(plane['volumes'],
|
|
221
|
+
/\Aopenvox-(?:ca|ssl|server-data|postgres-data|puppetdb-data)\z/) &&
|
|
222
|
+
valid_resource_id?(plane['egress']) &&
|
|
223
|
+
valid_plane_metadata?(plane)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def valid_plane_metadata?(plane)
|
|
227
|
+
flags = [plane['stopped'], plane['bridge_prepared']]
|
|
228
|
+
flags.all? { |flag| [nil, true, false].include?(flag) } &&
|
|
229
|
+
(plane['bridge_image'].nil? || plane['bridge_image'].is_a?(String)) &&
|
|
230
|
+
(plane['gateway_policy'].nil? || valid_code_digest?(plane['gateway_policy'])) &&
|
|
231
|
+
valid_proxy_reload?(plane['proxy_reload'])
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def valid_proxy_reload?(checkpoint)
|
|
235
|
+
return true if checkpoint.nil?
|
|
236
|
+
|
|
237
|
+
checkpoint.is_a?(Hash) && checkpoint.keys.sort == %w[fingerprint id] &&
|
|
238
|
+
checkpoint['id'].is_a?(String) && valid_resource_id?(checkpoint['id']) &&
|
|
239
|
+
valid_code_digest?(checkpoint['fingerprint'])
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def valid_code_digest?(value)
|
|
243
|
+
value.is_a?(String) && value.match?(/\A[a-f0-9]{64}\z/)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def valid_records?(records, pattern)
|
|
247
|
+
records.is_a?(Hash) && records.all? do |key, value|
|
|
248
|
+
key.is_a?(String) && key.match?(pattern) && value.is_a?(Hash) &&
|
|
249
|
+
value.keys == ['id'] && valid_resource_id?(value['id'])
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def valid_definition?(data)
|
|
254
|
+
data['definition'].is_a?(Hash) && data['definition']['revision'].is_a?(Integer) &&
|
|
255
|
+
data['definition']['runtime'] == data['runtime'] &&
|
|
256
|
+
data['fingerprint'] == Definition.fingerprint(data['definition'])
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def valid_timestamps?(data)
|
|
260
|
+
%w[created_at reconciled_at].all? { |key| data[key].is_a?(String) }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def valid_resource_id?(id)
|
|
264
|
+
id.nil? || (id.is_a?(String) && id.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]*\z/))
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def valid_network?(network)
|
|
268
|
+
network.is_a?(Hash) && network['logical_identity'] == @network.identity &&
|
|
269
|
+
network['name'] == @network.backend_name &&
|
|
270
|
+
valid_resource_id?(network['id'])
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Modules
|
|
5
|
+
# Only controlled worker records affect counts; ordinary tool output stays buffered.
|
|
6
|
+
class Events
|
|
7
|
+
attr_reader :name
|
|
8
|
+
|
|
9
|
+
def initialize(progress:)
|
|
10
|
+
@progress = progress
|
|
11
|
+
@pending = +''.b
|
|
12
|
+
@completed = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def observe(chunk)
|
|
16
|
+
@pending << chunk
|
|
17
|
+
while (line = @pending.slice!(/.*?\n/m))
|
|
18
|
+
event = line.match(/\AEMPEIRA_MODULE_(TOTAL|START|DONE):([a-z0-9_]+)\n\z/)
|
|
19
|
+
receive(*event.captures) if event
|
|
20
|
+
end
|
|
21
|
+
@pending.clear if @pending.bytesize > 1024
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def start(name)
|
|
25
|
+
@name = name
|
|
26
|
+
show
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def finish!
|
|
30
|
+
raise Error, 'Incomplete Puppetfile module progress protocol' unless @total == @completed.size
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def receive(kind, value)
|
|
36
|
+
case kind
|
|
37
|
+
when 'TOTAL' then total(value)
|
|
38
|
+
when 'START' then start(value)
|
|
39
|
+
when 'DONE' then done(value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def total(value)
|
|
44
|
+
raise Error, 'Invalid Puppetfile module total' unless @total.nil? && value.match?(/\A\d+\z/)
|
|
45
|
+
|
|
46
|
+
@total = Integer(value, 10)
|
|
47
|
+
@name = 'Puppetfile modules'
|
|
48
|
+
show
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def done(name)
|
|
52
|
+
unless name == @name && @total && @completed.size < @total && !@completed.include?(name)
|
|
53
|
+
raise Error, 'Invalid Puppetfile module completion record'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@completed << name
|
|
57
|
+
show
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def show
|
|
61
|
+
return @progress.heartbeat(@name) unless @total
|
|
62
|
+
|
|
63
|
+
@progress.count(@completed.size, @total, "#{@name} (#{@completed.size}/#{@total})")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|