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,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Server
|
|
7
|
+
# Export only the CA-issued service identity needed by the owned relay.
|
|
8
|
+
class RelayCertificate
|
|
9
|
+
NAME = 'puppetdb.empeira.internal'
|
|
10
|
+
SSL = '/etc/puppetlabs/puppet/ssl'
|
|
11
|
+
|
|
12
|
+
def initialize(context:)
|
|
13
|
+
@directory = context.locations.workspace(context.workspace).join('puppetdb-relay')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def path(name)
|
|
17
|
+
@directory.join(name).to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def prepare(runtime:, server:)
|
|
21
|
+
raise Error, 'Relay certificate directory is an unsafe symlink' if @directory.symlink?
|
|
22
|
+
return if File.file?(path('cert.pem')) && File.file?(path('key.pem'))
|
|
23
|
+
|
|
24
|
+
reject_partial_identity!
|
|
25
|
+
|
|
26
|
+
FileUtils.mkdir_p(@directory, mode: 0o700)
|
|
27
|
+
File.chmod(0o700, @directory)
|
|
28
|
+
issue(runtime, server)
|
|
29
|
+
export(runtime, server)
|
|
30
|
+
ensure
|
|
31
|
+
remove_files(%w[cert.pem.pending key.pem.pending]) unless @directory.symlink?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def issue(runtime, server)
|
|
35
|
+
result = runtime.service_exec(server, [Node::Certificates::CA, 'ca', 'generate', '--certname', NAME],
|
|
36
|
+
timeout: 60)
|
|
37
|
+
raise Error, 'Puppet CA could not issue the relay certificate; inspect server logs' unless result.success?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def export(runtime, server)
|
|
41
|
+
{ 'cert.pem' => "#{SSL}/certs/#{NAME}.pem",
|
|
42
|
+
'key.pem' => "#{SSL}/private_keys/#{NAME}.pem" }.each do |name, source|
|
|
43
|
+
temporary = path("#{name}.pending")
|
|
44
|
+
runtime.copy_from(server, source, temporary)
|
|
45
|
+
File.chmod(name == 'key.pem' ? 0o600 : 0o644, temporary)
|
|
46
|
+
File.rename(temporary, path(name))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reject_partial_identity!
|
|
51
|
+
return unless File.exist?(path('cert.pem')) || File.exist?(path('key.pem'))
|
|
52
|
+
|
|
53
|
+
raise Error, 'PuppetDB relay certificate is incomplete; restore or destroy the workspace'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cleanup
|
|
57
|
+
raise Error, 'Relay certificate directory is an unsafe symlink' if @directory.symlink?
|
|
58
|
+
return unless @directory.directory?
|
|
59
|
+
|
|
60
|
+
remove_files(%w[cert.pem key.pem cert.pem.pending key.pem.pending])
|
|
61
|
+
@directory.rmdir if @directory.children.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def remove_files(names)
|
|
67
|
+
names.each do |name|
|
|
68
|
+
file = path(name)
|
|
69
|
+
File.unlink(file) if File.exist?(file) || File.symlink?(file)
|
|
70
|
+
end
|
|
71
|
+
rescue SystemCallError
|
|
72
|
+
raise Error, 'Cannot remove owned relay certificate files; cleanup is incomplete'
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Server
|
|
5
|
+
# One Puppet-compatible runtime contract, with OpenVox defaults in configuration.
|
|
6
|
+
class Runtime
|
|
7
|
+
ENVIRONMENT_KEY = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
8
|
+
SEMANTICS = %w[certname dns_alt_names ca_hostname autosign server_hostname environment_timeout java_args
|
|
9
|
+
max_active_instances puppetdb_enabled storeconfigs reports puppetdb_server_urls].freeze
|
|
10
|
+
STARTUP_REQUIRED = %w[entrypoint arguments].freeze
|
|
11
|
+
STARTUP = [*STARTUP_REQUIRED, 'eyaml_keys'].freeze
|
|
12
|
+
PATHS = %w[puppetdb_config].freeze
|
|
13
|
+
FIELDS = { 'startup' => STARTUP, 'paths' => PATHS, 'environment_keys' => SEMANTICS }.freeze
|
|
14
|
+
|
|
15
|
+
def self.validate!(definition, path = 'server.runtime', effective: false)
|
|
16
|
+
validate_definition!(definition, path, effective: effective)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.validate_definition!(definition, location, effective:)
|
|
20
|
+
validate_fields!(definition, FIELDS.keys, location, effective: effective)
|
|
21
|
+
definition.each do |key, value|
|
|
22
|
+
child = "#{location}.#{key}"
|
|
23
|
+
validate_fields!(value, FIELDS.fetch(key), child, effective: effective)
|
|
24
|
+
validate_component!(key, value, child)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.validate_component!(key, value, location)
|
|
29
|
+
value.each do |field, item|
|
|
30
|
+
raise ConfigurationError, "#{location}.#{field} is invalid" unless valid_item?(key, field, item)
|
|
31
|
+
end
|
|
32
|
+
return unless key == 'environment_keys' && value.values.uniq.size != value.size
|
|
33
|
+
|
|
34
|
+
raise ConfigurationError, "#{location} must not reuse an environment variable name"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.valid_item?(component, field, item)
|
|
38
|
+
case component
|
|
39
|
+
when 'startup'
|
|
40
|
+
return valid_arguments?(item) if field == 'arguments'
|
|
41
|
+
return %w[direct staged].include?(item) if field == 'eyaml_keys'
|
|
42
|
+
|
|
43
|
+
safe_path?(item)
|
|
44
|
+
when 'paths' then safe_path?(item)
|
|
45
|
+
else item.is_a?(String) && item.match?(ENVIRONMENT_KEY)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.validate_fields!(value, allowed, location, effective:)
|
|
50
|
+
raise ConfigurationError, "#{location} must be a mapping" unless value.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
raise ConfigurationError, "#{location} contains an unsupported field" unless (value.keys - allowed).empty?
|
|
53
|
+
return unless effective
|
|
54
|
+
|
|
55
|
+
required = location.end_with?('.startup') ? STARTUP_REQUIRED : allowed
|
|
56
|
+
missing = required - value.keys
|
|
57
|
+
raise ConfigurationError, "#{location}.#{missing.first} is required" unless missing.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.safe_path?(value)
|
|
61
|
+
value.is_a?(String) && value.start_with?('/') && value != '/' &&
|
|
62
|
+
value.match?(%r{\A/[A-Za-z0-9_./-]+\z}) && !value.split('/').intersect?(%w[. ..]) &&
|
|
63
|
+
!value.include?('//')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.valid_arguments?(value)
|
|
67
|
+
value.is_a?(Array) && value.all? do |item|
|
|
68
|
+
item.is_a?(String) && !item.empty? && !item.match?(/[\x00-\x1f\x7f]/)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.resolve(config)
|
|
73
|
+
definition = config.fetch('server').fetch('runtime')
|
|
74
|
+
validate!(definition, effective: true)
|
|
75
|
+
new(definition)
|
|
76
|
+
end
|
|
77
|
+
attr_reader :startup, :paths, :environment_keys
|
|
78
|
+
|
|
79
|
+
def initialize(definition)
|
|
80
|
+
@startup = definition.fetch('startup')
|
|
81
|
+
@paths = definition.fetch('paths')
|
|
82
|
+
@environment_keys = definition.fetch('environment_keys')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def environment(values)
|
|
86
|
+
values.to_h { |semantic, value| [environment_keys.fetch(semantic.to_s), value] }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Services
|
|
5
|
+
# This domain describes containers without selecting a container engine.
|
|
6
|
+
class Definition
|
|
7
|
+
attr_reader :key, :options, :workspace
|
|
8
|
+
|
|
9
|
+
def initialize(key:, workspace:, **options)
|
|
10
|
+
@key = key
|
|
11
|
+
@workspace = workspace
|
|
12
|
+
@options = Immutable.deep_freeze(options.transform_keys(&:to_s))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def name
|
|
16
|
+
"empeira-#{workspace.id}-#{key}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def fingerprint
|
|
20
|
+
Infrastructure::Definition.fingerprint(options.except('dns', 'runtime_environment', 'runtime_ip'))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ownership_labels
|
|
24
|
+
{ 'io.empeira.managed-by' => 'empeira', 'io.empeira.workspace' => workspace.id,
|
|
25
|
+
'io.empeira.purpose' => key }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def labels
|
|
29
|
+
ownership_labels.merge('io.empeira.definition' => fingerprint)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def verify!(resource, expected_id: nil)
|
|
33
|
+
return unless resource
|
|
34
|
+
return if resource.fetch('name') == name &&
|
|
35
|
+
ownership_labels.all? { |key, value| resource.fetch('labels')[key] == value } &&
|
|
36
|
+
(expected_id.nil? || expected_id == resource.fetch('id'))
|
|
37
|
+
|
|
38
|
+
raise Providers::OwnershipError, "Service resource ownership or recorded ID conflicts: #{key}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Updates
|
|
5
|
+
# Container artifacts only; existing containers and immutable VM bases stay attached to their revisions.
|
|
6
|
+
class Images
|
|
7
|
+
def initialize(context:, runtime:, progress: Progress.new)
|
|
8
|
+
@context = context
|
|
9
|
+
@config = context.configuration
|
|
10
|
+
@runtime = runtime
|
|
11
|
+
@progress = progress
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def refresh
|
|
15
|
+
@state = Infrastructure::Store.new(context: @context).load
|
|
16
|
+
if @state && @state.fetch('runtime') != @context.container_engine
|
|
17
|
+
raise Error, 'Workspace belongs to another runtime; select its owning runtime before updating images'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
artifacts = catalog
|
|
21
|
+
@runtime.with_image_updates do
|
|
22
|
+
artifacts.each_with_index do |entry, index|
|
|
23
|
+
update(entry, index, artifacts.size)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def update(entry, index, total)
|
|
31
|
+
label = entry.fetch(:label)
|
|
32
|
+
@progress.count(index, total, "#{label} (#{index}/#{total})")
|
|
33
|
+
result = @runtime.refresh_image(entry.fetch(:image), **entry.except(:image, :label)) do |status|
|
|
34
|
+
@progress.count(index, total, "#{label} — #{status} (#{index + 1}/#{total})")
|
|
35
|
+
end
|
|
36
|
+
@progress.count(index + 1, total, "#{label} — #{result} (#{index + 1}/#{total})")
|
|
37
|
+
rescue Error => e
|
|
38
|
+
raise e.class, "#{label} (#{entry.fetch(:image)}): #{e.message}", cause: nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def catalog
|
|
42
|
+
(service_images + utility_images + node_images).uniq { |entry| entry.fetch(:image) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def service_images
|
|
46
|
+
references = [
|
|
47
|
+
['Configuration server', reference(@config.dig('images', 'server'))],
|
|
48
|
+
['CoreDNS', reference(@config.dig('images', 'dns'))],
|
|
49
|
+
['Browser', reference(@config.dig('browser', 'image'))]
|
|
50
|
+
]
|
|
51
|
+
references.concat(database_images)
|
|
52
|
+
references.concat(@config.dig('containers', 'additional').map do |entry|
|
|
53
|
+
[entry.fetch('name'), reference(entry.fetch('image'))]
|
|
54
|
+
end)
|
|
55
|
+
references.map { |label, image| { image: image, label: label } }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def database_images
|
|
59
|
+
return [] unless @config.dig('puppetdb', 'enabled')
|
|
60
|
+
|
|
61
|
+
[['PuppetDB', reference(@config.dig('images', 'puppetdb'))],
|
|
62
|
+
['PostgreSQL', reference(@config.dig('images', 'postgres'))]]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def utility_images
|
|
66
|
+
installer = Empeira::Modules::Image.new(@config.dig('images', 'r10k'),
|
|
67
|
+
registry: @config.dig('images', 'registry'))
|
|
68
|
+
adapter = Network::Peer::AdapterImage.new(context: @context)
|
|
69
|
+
direct = ControlPlane::Plan.new(context: @context).gateway_artifact
|
|
70
|
+
[utility_artifact('relay', 'Service relay', purpose: 'relay'), utility_artifact('proxy', 'Squid proxy'),
|
|
71
|
+
direct.merge(label: 'Workspace gateway'),
|
|
72
|
+
{ label: 'r10k helper', image: installer.reference, recipe: installer.recipe, files: installer.files },
|
|
73
|
+
{ label: 'VM network helper', image: adapter.image, recipe: adapter.recipe, files: adapter.files }]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def utility_artifact(key, label, **)
|
|
77
|
+
Empeira::Images::Configuration.artifact(
|
|
78
|
+
@config.dig('images', key), registry: @config.dig('images', 'registry'), **
|
|
79
|
+
).merge(label: label)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def node_images
|
|
83
|
+
architecture = @runtime.architecture
|
|
84
|
+
node_variants.map do |os, version|
|
|
85
|
+
request = Node::RunRequest.from_config(hostname: 'image-update', provider: 'container', config: @config)
|
|
86
|
+
.with(os: os, version: version)
|
|
87
|
+
image = Node::Image.new(config: @config, request: request, architecture: architecture)
|
|
88
|
+
{ label: "#{os.capitalize} #{version} node image", image: image.reference,
|
|
89
|
+
recipe: image.recipe, files: image.build_files }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def node_variants
|
|
94
|
+
used = @state.to_h.fetch('nodes', {}).values.select { |record| record['provider'] == 'container' }
|
|
95
|
+
[@config.fetch('node_defaults').values_at('os', 'version'),
|
|
96
|
+
*used.map { |record| record.values_at('os', 'version') }].uniq
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def reference(config)
|
|
100
|
+
Empeira::Images::Configuration.reference(config, registry: @config.dig('images', 'registry'))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Updates
|
|
5
|
+
# Caller owns the existing workspace lock. r10k updates the live module directory in place.
|
|
6
|
+
class Modules
|
|
7
|
+
def initialize(context:, runtime:, runner:, progress: Progress.new)
|
|
8
|
+
@context = context
|
|
9
|
+
@runtime = runtime
|
|
10
|
+
@runner = runner
|
|
11
|
+
@progress = progress
|
|
12
|
+
@storage = Empeira::Modules::Storage.new(context: context)
|
|
13
|
+
@state = Empeira::Modules::State.new(context: context)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def synchronize
|
|
17
|
+
request = Empeira::Modules::Request.new(context: @context)
|
|
18
|
+
request.warnings.each { |message| @progress.warning(message) }
|
|
19
|
+
return unless request.present?
|
|
20
|
+
|
|
21
|
+
Empeira::Modules::GitSafety.new(project: @context.project.path, runner: @runner, progress: @progress)
|
|
22
|
+
.verify(@storage.root, recommend: true)
|
|
23
|
+
install(request)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# rubocop:disable-next Metrics/AbcSize -- Preserve the ordered direct-sync and success-marker boundary.
|
|
29
|
+
def install(request)
|
|
30
|
+
@progress.stage(0, 'Reading Puppetfile...')
|
|
31
|
+
image = Empeira::Modules::Image.new(@context.configuration.dig('images', 'r10k'),
|
|
32
|
+
registry: @context.configuration.dig('images', 'registry'))
|
|
33
|
+
@runtime.ensure_image(image.reference, recipe: image.recipe, files: image.files)
|
|
34
|
+
@storage.prepare
|
|
35
|
+
@state.prepare
|
|
36
|
+
@state.invalidate
|
|
37
|
+
git = Empeira::Modules::HostGit.new(runner: @runner, project: @context.project.path, progress: @progress)
|
|
38
|
+
installer = Empeira::Modules::Installer.new(runtime: @runtime, image: image.reference,
|
|
39
|
+
git: git, progress: @progress)
|
|
40
|
+
names = installer.synchronize(request, @storage.root, state: @state)
|
|
41
|
+
@state.record(@storage.root, names)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Updates
|
|
5
|
+
class Service
|
|
6
|
+
TARGETS = %w[images modules all].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(context: nil, runner: nil, runtimes: nil, build_info: BuildInfo.load, progress: Progress.new)
|
|
9
|
+
@context = context
|
|
10
|
+
@runner = runner
|
|
11
|
+
@runtimes = runtimes
|
|
12
|
+
@build_info = build_info
|
|
13
|
+
@progress = progress
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self_update
|
|
17
|
+
raise UnavailableFeature,
|
|
18
|
+
'Empeira self-update is not implemented yet. Package replacement requires release packaging.'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def update(target)
|
|
22
|
+
raise Error, "Select an update target: #{TARGETS.join(', ')}" unless TARGETS.include?(target)
|
|
23
|
+
return update_artifacts(target) unless target == 'all'
|
|
24
|
+
|
|
25
|
+
raise UnavailableFeature,
|
|
26
|
+
'update all is unavailable: it includes Empeira itself, and self-update is not implemented.'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def update_artifacts(target)
|
|
32
|
+
@build_info.require_compatible!(@context.configuration.dig('requirements', 'empeira'))
|
|
33
|
+
Infrastructure::Store.new(context: @context).with_lock do
|
|
34
|
+
runtime = @runtimes.build(@context.container_engine, context: @context, runner: @runner)
|
|
35
|
+
runtime.check_available!
|
|
36
|
+
if target == 'modules'
|
|
37
|
+
Modules.new(context: @context, runtime: runtime, runner: @runner, progress: @progress).synchronize
|
|
38
|
+
else
|
|
39
|
+
Images.new(context: @context, runtime: runtime, progress: @progress).refresh
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubygems/requirement'
|
|
4
|
+
require 'rubygems/version'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
class VersionRequirement
|
|
8
|
+
def self.parse(value)
|
|
9
|
+
return if value.nil?
|
|
10
|
+
|
|
11
|
+
raise ArgumentError unless valid?(value)
|
|
12
|
+
|
|
13
|
+
Gem::Requirement.new(*value.split(',').map(&:strip))
|
|
14
|
+
rescue ArgumentError
|
|
15
|
+
raise ConfigurationError, 'requirements.empeira must be a RubyGems version requirement or null', cause: nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.valid?(value)
|
|
19
|
+
value.is_a?(String) && !value.strip.empty? && value.split(',', -1).none? { |part| part.strip.empty? }
|
|
20
|
+
end
|
|
21
|
+
private_class_method :valid?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module VM
|
|
5
|
+
# Guest package installation uses only the existing allowlisted HTTP/HTTPS proxy.
|
|
6
|
+
class Agent
|
|
7
|
+
PUPPET = Node::Certificates::PUPPET
|
|
8
|
+
|
|
9
|
+
def initialize(context:, guest:, runtime: nil, progress: Progress.new)
|
|
10
|
+
@context = context
|
|
11
|
+
@guest = guest
|
|
12
|
+
@runtime = runtime
|
|
13
|
+
@progress = progress
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ensure_installed(record, requirements: nil, proxy_url: nil)
|
|
17
|
+
requirements ||= BootstrapRequirements.new(context: @context, os: record.fetch('os'),
|
|
18
|
+
version: record.fetch('version'),
|
|
19
|
+
architecture: record.fetch('architecture'))
|
|
20
|
+
unless requirements.required?
|
|
21
|
+
verify_existing_agent!(record)
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
raise Error, 'Managed agent installation requires the bootstrap proxy' unless proxy_url
|
|
25
|
+
|
|
26
|
+
@proxy_url = proxy_url
|
|
27
|
+
@requirements = requirements
|
|
28
|
+
install_selected_agent(record, requirements)
|
|
29
|
+
ensure
|
|
30
|
+
@proxy_url = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :proxy_url
|
|
36
|
+
|
|
37
|
+
def verify_existing_agent!(record)
|
|
38
|
+
result = @guest.run(record, [PUPPET, '--version'])
|
|
39
|
+
return if result.success?
|
|
40
|
+
|
|
41
|
+
details = Execution::Diagnostics.command(result, operation: 'Existing VM Puppet agent verification',
|
|
42
|
+
tool: 'puppet')
|
|
43
|
+
raise Error, 'No usable agent was found and managed bootstrap is unavailable; ' \
|
|
44
|
+
"check bootstrap.guests metadata\n#{details}", cause: nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def install_selected_agent(record, requirements)
|
|
48
|
+
execute = ->(arguments) { @guest.run(record, arguments, timeout: 300) }
|
|
49
|
+
copy = ->(source, destination, mode) { @guest.copy_to(record, source, destination, mode: mode) }
|
|
50
|
+
@runtime ||= Runtime.registry.build(@context.container_engine, context: @context, runner: Execution::Runner.new)
|
|
51
|
+
installer = Node::AgentInstaller.build(context: @context, runtime: @runtime, requirements: requirements,
|
|
52
|
+
execute: execute, copy: copy, progress: @progress)
|
|
53
|
+
installer.install(proxy_url: proxy_url)
|
|
54
|
+
execute!(record, [PUPPET, '--version'], 'VM Puppet agent verification')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def execute!(record, arguments, operation)
|
|
58
|
+
result = @guest.run(record, arguments, timeout: 300)
|
|
59
|
+
return if result.success?
|
|
60
|
+
|
|
61
|
+
details = Execution::Diagnostics.command(result, operation: operation, tool: arguments.first)
|
|
62
|
+
raise Error, "#{operation} failed; VM retained for diagnosis\n#{details}", cause: nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module VM
|
|
7
|
+
# Reviewed image metadata determines installation needs before any download.
|
|
8
|
+
class BootstrapRequirements
|
|
9
|
+
attr_reader :repository, :agent, :guest, :target
|
|
10
|
+
|
|
11
|
+
def initialize(context:, os:, version:, agent_required: nil, distribution_required: false,
|
|
12
|
+
architecture: context.platform.architecture)
|
|
13
|
+
config = context.configuration
|
|
14
|
+
@target = ::Empeira::Agent::Target.new(os: os, release: version, architecture: architecture.to_s)
|
|
15
|
+
@os = os
|
|
16
|
+
@architecture = architecture.to_s
|
|
17
|
+
@guest = config.fetch('bootstrap').fetch('guests').fetch(os).fetch(version)
|
|
18
|
+
@agent_required = agent_required.nil? ? !guest.fetch('agent_preinstalled') : agent_required
|
|
19
|
+
@distribution_required = distribution_required || @agent_required
|
|
20
|
+
return unless @agent_required || @distribution_required
|
|
21
|
+
|
|
22
|
+
load_agent(config) if @agent_required
|
|
23
|
+
rescue KeyError
|
|
24
|
+
raise ConfigurationError,
|
|
25
|
+
'No reviewed agent source exists for this guest'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def required?
|
|
29
|
+
@agent_required
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def destinations
|
|
33
|
+
distribution_destinations = @distribution_required ? guest_destinations : []
|
|
34
|
+
distribution_destinations.uniq
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def report
|
|
38
|
+
"Managed bootstrap destinations: #{destinations.join(', ')}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rpm_options
|
|
42
|
+
return [] unless %w[rocky almalinux].include?(@os)
|
|
43
|
+
|
|
44
|
+
architecture = ImageSource::ARCHITECTURES.fetch(@architecture)
|
|
45
|
+
base = guest.fetch('baseurl')
|
|
46
|
+
{ 'baseos' => 'BaseOS', 'appstream' => 'AppStream', 'extras' => 'extras' }.flat_map do |id, directory|
|
|
47
|
+
["--setopt=#{id}.mirrorlist=", "--setopt=#{id}.metalink=",
|
|
48
|
+
"--setopt=#{id}.baseurl=#{base}/#{directory}/#{architecture}/os/"]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def guest_destinations
|
|
55
|
+
destinations = guest.fetch('destinations')
|
|
56
|
+
return destinations unless @os == 'ubuntu'
|
|
57
|
+
|
|
58
|
+
irrelevant = @architecture == 'arm64' ? %w[archive.ubuntu.com security.ubuntu.com] : ['ports.ubuntu.com']
|
|
59
|
+
destinations - irrelevant
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def load_agent(config)
|
|
63
|
+
@agent = config.fetch('agent')
|
|
64
|
+
install = @agent.fetch('install')
|
|
65
|
+
@repository = target.source(install)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module VM
|
|
5
|
+
# Reuse the same targeted CA enrollment rules as container nodes.
|
|
6
|
+
class CertificateRuntime
|
|
7
|
+
def initialize(runtime:, guest:, record:)
|
|
8
|
+
@runtime = runtime
|
|
9
|
+
@guest = guest
|
|
10
|
+
@record = record
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def service_exec(resource, arguments, timeout: 60)
|
|
14
|
+
return @guest.run(@record, arguments, timeout: timeout) if resource['vm']
|
|
15
|
+
|
|
16
|
+
@runtime.service_exec(resource, arguments, timeout: timeout)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy_from(resource, source, destination)
|
|
20
|
+
@runtime.copy_from(resource, source, destination)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def copy_to(resource, source, destination)
|
|
24
|
+
return @guest.copy_to(@record, source, destination) if resource['vm']
|
|
25
|
+
|
|
26
|
+
@runtime.copy_to(resource, source, destination)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|