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,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
# Scalar validation shared by fragments and resolved agent sources.
|
|
6
|
+
module AgentValues
|
|
7
|
+
def required!(data, keys, path)
|
|
8
|
+
keys.each { |key| raise ConfigurationError, "#{path}.#{key} is required" unless data.key?(key) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def package_token!(value, path)
|
|
12
|
+
return if value.is_a?(String) && value.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.+-]*\z/)
|
|
13
|
+
|
|
14
|
+
raise ConfigurationError, "#{path} must be a package name"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def token!(value, path)
|
|
18
|
+
return if value.is_a?(String) && value.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.+:~-]*\z/)
|
|
19
|
+
|
|
20
|
+
raise ConfigurationError, "#{path} must be a package/version token"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def suffix!(value, path)
|
|
24
|
+
return if value.is_a?(String) && value.match?(/\A[-+a-zA-Z0-9.]+\z/)
|
|
25
|
+
|
|
26
|
+
raise ConfigurationError, "#{path} is invalid"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def digest!(value, path)
|
|
30
|
+
return if value.is_a?(String) && value.match?(/\A[a-f0-9]{64}\z/)
|
|
31
|
+
|
|
32
|
+
raise ConfigurationError, "#{path} must be a SHA-256 digest"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def boolean!(value, path)
|
|
36
|
+
raise ConfigurationError, "#{path} must be boolean" unless [true, false].include?(value)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def https_url!(value, path)
|
|
40
|
+
uri = URI(value)
|
|
41
|
+
return if https_endpoint?(uri) && !value.match?(/[\s\x00-\x1f\x7f]/)
|
|
42
|
+
|
|
43
|
+
raise ConfigurationError, "#{path} must be a credential-free HTTPS URL"
|
|
44
|
+
rescue URI::InvalidURIError, TypeError
|
|
45
|
+
raise ConfigurationError, "#{path} must be a credential-free HTTPS URL"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def https_endpoint?(uri)
|
|
49
|
+
uri.is_a?(URI::HTTPS) && uri.host && !uri.host.empty? && uri.userinfo.nil? &&
|
|
50
|
+
uri.query.nil? && uri.fragment.nil?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
module BootstrapGuests
|
|
6
|
+
def self.validate!(data, path)
|
|
7
|
+
NodeImages.validate!(data, path) do |versions, os_path|
|
|
8
|
+
NodeImages.validate!(versions, os_path) do |guest, guest_path|
|
|
9
|
+
NodeImages.validate!(guest, guest_path) { |value, field| validate_field!(value, field) }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.effective!(data)
|
|
15
|
+
data.each do |os, versions|
|
|
16
|
+
versions.each do |version, guest|
|
|
17
|
+
fields = %w[agent_preinstalled destinations]
|
|
18
|
+
fields << 'baseurl' if %w[rocky almalinux].include?(os)
|
|
19
|
+
NodeImages.required!(guest, fields, "bootstrap.guests.#{os}.#{version}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.destinations?(value)
|
|
25
|
+
value.is_a?(Array) && value.all? { |domain| Network::ProxyPolicy.valid_domain?(domain) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.validate_field!(value, field)
|
|
29
|
+
key = field.split('.').last
|
|
30
|
+
valid = case key
|
|
31
|
+
when 'agent_preinstalled' then [true, false].include?(value)
|
|
32
|
+
when 'destinations'
|
|
33
|
+
destinations?(value)
|
|
34
|
+
when 'archive', 'security', 'ports', 'baseurl'
|
|
35
|
+
value.is_a?(String) && value.match?(%r{\Ahttps?://[a-z0-9./_-]+\z})
|
|
36
|
+
else false
|
|
37
|
+
end
|
|
38
|
+
raise ConfigurationError, "#{field} is invalid or unsupported" unless valid
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
class CommandMocks
|
|
6
|
+
FIELDS = %w[path mock_to exit_code].freeze
|
|
7
|
+
NAME = /\A[a-zA-Z0-9_][a-zA-Z0-9_.-]*\z/
|
|
8
|
+
|
|
9
|
+
def self.validate!(commands, path = 'mocks.commands', effective: false)
|
|
10
|
+
raise ConfigurationError, "#{path} must be a mapping" unless commands.is_a?(Hash)
|
|
11
|
+
|
|
12
|
+
targets = []
|
|
13
|
+
commands.each do |name, entry|
|
|
14
|
+
unless name.is_a?(String) && name.match?(NAME)
|
|
15
|
+
raise ConfigurationError, "#{path} keys must be logical command names"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
location = "#{path}.#{name}"
|
|
19
|
+
entry!(entry, location, effective: effective)
|
|
20
|
+
next unless entry.key?('path')
|
|
21
|
+
|
|
22
|
+
target!(entry['path'], location, targets: targets)
|
|
23
|
+
targets << entry['path']
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.entry!(entry, path, effective:)
|
|
28
|
+
unless entry.is_a?(Hash) && (entry.keys - FIELDS).empty?
|
|
29
|
+
raise ConfigurationError, "#{path} must contain only path, mock_to and exit_code"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
require_fields!(entry, path) if effective
|
|
33
|
+
mock_to!(entry['mock_to'], "#{path}.mock_to") if entry.key?('mock_to')
|
|
34
|
+
exit_code!(entry['exit_code'], "#{path}.exit_code") if entry.key?('exit_code')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.require_fields!(entry, path)
|
|
38
|
+
FIELDS.each do |field|
|
|
39
|
+
raise ConfigurationError, "#{path}.#{field} is required" unless entry.key?(field)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.exit_code!(value, path)
|
|
44
|
+
return if value == 'passthrough' || (value.is_a?(Integer) && value.between?(0, 255))
|
|
45
|
+
|
|
46
|
+
raise ConfigurationError, "#{path} must be an integer from 0 to 255 or passthrough"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.target!(target, path, targets: [])
|
|
50
|
+
unless ServiceConfiguration.safe_target?(target)
|
|
51
|
+
raise ConfigurationError, "#{path}.path must be a safe absolute file path without traversal"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
reserved = ServiceConfiguration::RUNTIME_TARGETS + [Node::ExternalFact::PATH]
|
|
55
|
+
ServiceConfiguration.validate_targets!(target, reserved + targets, path, field: 'path')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.mock_to!(value, path)
|
|
59
|
+
return if value == 'echo' || ServiceConfiguration.safe_target?(value)
|
|
60
|
+
|
|
61
|
+
raise ConfigurationError, "#{path} must be echo or a safe absolute executable path"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
module Display
|
|
6
|
+
SENSITIVE = /password|secret|token|credential|authorization|private.?key/i
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def redact(value)
|
|
11
|
+
case value
|
|
12
|
+
when Hash
|
|
13
|
+
value.to_h { |key, child| [key, key.to_s.match?(SENSITIVE) ? '[REDACTED]' : redact(child)] }
|
|
14
|
+
when Array then value.map { |child| redact(child) }
|
|
15
|
+
else value
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
class DNSRewrites
|
|
6
|
+
def self.normalize(name)
|
|
7
|
+
name.downcase.delete_suffix('.')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.entries(config)
|
|
11
|
+
normalized = config.dig('dns', 'rewrites').map do |entry|
|
|
12
|
+
entry.transform_values { |name| normalize(name) }
|
|
13
|
+
end
|
|
14
|
+
normalized.sort_by { |entry| entry.fetch('from') }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.validate!(entries, path)
|
|
18
|
+
raise ConfigurationError, "#{path} must be an array" unless entries.is_a?(Array)
|
|
19
|
+
|
|
20
|
+
sources = []
|
|
21
|
+
entries.each_with_index do |entry, index|
|
|
22
|
+
location = "#{path}[#{index}]"
|
|
23
|
+
validate_entry!(entry, location)
|
|
24
|
+
source = normalize(entry.fetch('from'))
|
|
25
|
+
raise ConfigurationError, "#{location}.from duplicates another rewrite source" if sources.include?(source)
|
|
26
|
+
|
|
27
|
+
sources << source
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.validate_entry!(entry, path)
|
|
32
|
+
unless entry.is_a?(Hash) && entry.size == 2 && %w[from to].all? { |key| entry.key?(key) }
|
|
33
|
+
raise ConfigurationError, "#{path} requires only from and to"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
%w[from to].each { |key| hostname!(entry.fetch(key), "#{path}.#{key}") }
|
|
37
|
+
validate_zones!(normalize(entry.fetch('from')), normalize(entry.fetch('to')), path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.validate_zones!(source, target, path)
|
|
41
|
+
if source == Network::Naming::DOMAIN || source.end_with?(".#{Network::Naming::DOMAIN}")
|
|
42
|
+
raise ConfigurationError, "#{path}.from must be outside empeira.internal; " \
|
|
43
|
+
'self references and cycles are forbidden'
|
|
44
|
+
end
|
|
45
|
+
return if target.match?(/\A#{Schema::DNS_LABEL}\.empeira\.internal\z/)
|
|
46
|
+
|
|
47
|
+
raise ConfigurationError, "#{path}.to must be an internal service hostname: <service>.empeira.internal"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.hostname!(name, path)
|
|
51
|
+
return if name.is_a?(String) && normalize(name).size <= 253 && name.match?(Schema::DNS_NAME) &&
|
|
52
|
+
!name.match?(/\A[\d.]+\z/)
|
|
53
|
+
|
|
54
|
+
raise ConfigurationError, "#{path} must be an exact DNS hostname without wildcards, URLs or IP addresses"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Configuration
|
|
7
|
+
class Document
|
|
8
|
+
SERVICE_CONTENT = /\Acontainers\.additional\.\d+\.configuration\.content\z/
|
|
9
|
+
|
|
10
|
+
def read(path)
|
|
11
|
+
text = File.read(path)
|
|
12
|
+
documents = Psych.parse_stream(text).children
|
|
13
|
+
return {} if documents.empty?
|
|
14
|
+
raise ConfigurationError, 'configuration must contain exactly one YAML document' unless documents.size == 1
|
|
15
|
+
|
|
16
|
+
check_keys!(documents.first.root, [])
|
|
17
|
+
YAML.safe_load(text, permitted_classes: [], permitted_symbols: [], aliases: false)
|
|
18
|
+
rescue Psych::Exception
|
|
19
|
+
raise ConfigurationError, "#{File.basename(path)} must contain valid YAML without aliases or object tags"
|
|
20
|
+
rescue SystemCallError
|
|
21
|
+
raise ConfigurationError, "#{File.basename(path)} could not be read"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# rubocop:disable-next Metrics/AbcSize -- Recursively check mapping keys, including service arrays.
|
|
27
|
+
def check_keys!(node, path)
|
|
28
|
+
check_plain_data!(node, path) if path.join('.').match?(SERVICE_CONTENT)
|
|
29
|
+
if node.is_a?(Psych::Nodes::Sequence)
|
|
30
|
+
node.children.each_with_index { |child, index| check_keys!(child, path + [index.to_s]) }
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
return unless node.is_a?(Psych::Nodes::Mapping)
|
|
34
|
+
|
|
35
|
+
seen = []
|
|
36
|
+
node.children.each_slice(2) do |key, value|
|
|
37
|
+
check_key!(key, path)
|
|
38
|
+
|
|
39
|
+
child_path = path + [key.value]
|
|
40
|
+
raise ConfigurationError, "#{location(child_path)} is defined more than once" if seen.include?(key.value)
|
|
41
|
+
|
|
42
|
+
seen << key.value
|
|
43
|
+
check_keys!(value, child_path)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check_plain_data!(node, path)
|
|
48
|
+
if node.is_a?(Psych::Nodes::Alias) || node.tag || node.anchor
|
|
49
|
+
raise ConfigurationError, "#{location(path)} must not contain YAML tags, anchors or aliases"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Array(node.children).each { |child| check_plain_data!(child, path) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_key!(key, path)
|
|
56
|
+
raise ConfigurationError, "#{location(path)} contains a non-string key" unless key.is_a?(Psych::Nodes::Scalar)
|
|
57
|
+
return unless key.value.match?(/[\p{Cc}\p{Cf}]/)
|
|
58
|
+
|
|
59
|
+
raise ConfigurationError, "#{location(path)} contains a key with control characters"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def location(path)
|
|
63
|
+
path.empty? ? '$' : path.join('.')
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
# Resolve paths without reading or copying key material.
|
|
6
|
+
class Eyaml
|
|
7
|
+
DESTINATION = '/etc/puppetlabs/puppet/eyaml'
|
|
8
|
+
|
|
9
|
+
def initialize(config:, project:)
|
|
10
|
+
@config = config
|
|
11
|
+
@project = project
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def mounts(destination: DESTINATION)
|
|
15
|
+
return [] unless @config.fetch('enabled')
|
|
16
|
+
|
|
17
|
+
%w[private_key public_key].map do |key|
|
|
18
|
+
source = resolve(key)
|
|
19
|
+
"type=bind,src=#{source},dst=#{destination}/#{key}.pkcs7.pem,readonly"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def safe_path?(value)
|
|
26
|
+
value.is_a?(String) && !value.empty? && !value.match?(/[\x00-\x1f,]/)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolve(key)
|
|
30
|
+
value = @config[key]
|
|
31
|
+
unless safe_path?(value)
|
|
32
|
+
raise ConfigurationError, "eyaml.#{key} must be a safe local file path when eyaml.enabled=true"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
path = Pathname(value).expand_path(@project).realpath
|
|
36
|
+
unless path.file? && path.readable? && safe_path?(path.to_s)
|
|
37
|
+
raise ConfigurationError, "eyaml.#{key} must be a readable regular file with a safe mount path"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
path
|
|
41
|
+
rescue SystemCallError, ArgumentError
|
|
42
|
+
raise ConfigurationError, "eyaml.#{key} must resolve to a readable regular file", cause: nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
class HieraModulepath
|
|
6
|
+
def initialize(project:, environment:)
|
|
7
|
+
@project = project
|
|
8
|
+
@environment = environment
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def verify!
|
|
12
|
+
value = configured_path
|
|
13
|
+
return unless value
|
|
14
|
+
return if value.strip.split(':').any? { |path| included_or_unknown?(path) }
|
|
15
|
+
|
|
16
|
+
raise ConfigurationError,
|
|
17
|
+
'hiera.mounts/Puppetfile module destinations are excluded by environment.conf modulepath; ' \
|
|
18
|
+
'include modules in the environment modulepath'
|
|
19
|
+
rescue SystemCallError
|
|
20
|
+
raise ConfigurationError, 'Cannot read environment.conf to verify the modulepath', cause: nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def configured_path
|
|
26
|
+
file = @project.join('environment.conf')
|
|
27
|
+
return unless file.exist?
|
|
28
|
+
|
|
29
|
+
file.read.lines.filter_map { |line| line.match(/^\s*modulepath\s*=\s*([^#]*)/)&.captures&.first }.last
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def included_or_unknown?(path)
|
|
33
|
+
path = path.gsub('$codedir', '/etc/puppetlabs/code').gsub('$environment', @environment)
|
|
34
|
+
relative = Pathname(path).cleanpath.to_s
|
|
35
|
+
relative == 'modules' || relative == "/etc/puppetlabs/code/environments/#{@environment}/modules" ||
|
|
36
|
+
(path.include?('$') && path != '$basemodulepath')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
class HieraMountSchema
|
|
6
|
+
FIELDS = %w[source type name target required].freeze
|
|
7
|
+
NAME = /\A[a-z][a-z0-9_]*\z/
|
|
8
|
+
PATH = %r{\A[a-zA-Z0-9_.-]+(?:/[a-zA-Z0-9_.-]+)*\z}
|
|
9
|
+
|
|
10
|
+
def self.validate!(entries, path = 'hiera.mounts')
|
|
11
|
+
raise ConfigurationError, "#{path} must be an array of mounts" unless entries.is_a?(Array)
|
|
12
|
+
|
|
13
|
+
targets = entries.each_with_index.map do |entry, index|
|
|
14
|
+
location = "#{path}.#{index}"
|
|
15
|
+
validate_entry!(entry, location)
|
|
16
|
+
entry['type'] == 'module' ? "modules/#{entry['name']}" : entry['target']
|
|
17
|
+
end
|
|
18
|
+
targets.each_with_index do |target, index|
|
|
19
|
+
next unless targets.take(index).any? { |other| overlap?(target, other) }
|
|
20
|
+
|
|
21
|
+
raise ConfigurationError, "#{path}.#{index} overlaps another mount target"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.overlap?(left, right)
|
|
26
|
+
left == right || left.start_with?("#{right}/") || right.start_with?("#{left}/")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.safe_source?(value)
|
|
30
|
+
value.is_a?(String) && !value.empty? && !value.match?(/[\x00-\x1f,]/)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.safe_target?(value)
|
|
34
|
+
value.is_a?(String) && value.match?(PATH) && !value.split('/').intersect?(%w[. ..])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Validate every definition even when its optional source is unavailable.
|
|
38
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- Validate the discriminated public mount shape together.
|
|
39
|
+
def self.validate_entry!(entry, path)
|
|
40
|
+
raise ConfigurationError, "#{path} must be a mount mapping" unless entry.is_a?(Hash)
|
|
41
|
+
raise ConfigurationError, "#{path} contains unsupported fields" unless (entry.keys - FIELDS).empty?
|
|
42
|
+
raise ConfigurationError, "#{path}.source must be a safe local path" unless safe_source?(entry['source'])
|
|
43
|
+
unless %w[module environment].include?(entry['type'])
|
|
44
|
+
raise ConfigurationError, "#{path}.type must be module or environment"
|
|
45
|
+
end
|
|
46
|
+
if entry.key?('required') && ![true, false].include?(entry['required'])
|
|
47
|
+
raise ConfigurationError, "#{path}.required must be a boolean"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if entry['type'] == 'module'
|
|
51
|
+
unless entry['name'].is_a?(String) && entry['name'].match?(NAME) && !entry.key?('target')
|
|
52
|
+
raise ConfigurationError, "#{path}.name must be a Puppet module name; module mounts do not accept target"
|
|
53
|
+
end
|
|
54
|
+
elsif !safe_target?(entry['target']) || entry.key?('name')
|
|
55
|
+
raise ConfigurationError,
|
|
56
|
+
"#{path}.target must be a safe environment-relative path; " \
|
|
57
|
+
'environment mounts do not accept name'
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
# Puppet destinations and source availability share one resolution boundary.
|
|
6
|
+
class HieraMounts
|
|
7
|
+
attr_reader :entries
|
|
8
|
+
|
|
9
|
+
def initialize(config:, project:, environment:)
|
|
10
|
+
@project = Pathname(project).realpath
|
|
11
|
+
@environment = environment
|
|
12
|
+
HieraMountSchema.validate!(config.fetch('mounts'))
|
|
13
|
+
@entries = config.fetch('mounts').each_with_index.map { |entry, index| resolve(entry, index) }
|
|
14
|
+
HieraModulepath.new(project: @project, environment: @environment).verify! if entries.any? do |entry|
|
|
15
|
+
entry['type'] == 'module'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def mounts
|
|
20
|
+
entries.filter_map do |entry|
|
|
21
|
+
next unless entry['status'] == 'available'
|
|
22
|
+
|
|
23
|
+
"type=bind,src=#{entry['resolved_source']},dst=#{entry['destination']},readonly"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def warnings
|
|
28
|
+
entries.filter_map do |entry|
|
|
29
|
+
next if entry['status'] == 'available'
|
|
30
|
+
|
|
31
|
+
"Warning: optional Hiera source is unavailable: #{entry['source']}; " \
|
|
32
|
+
"mount #{entry['destination']} skipped (#{entry['reason']})"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- Classify source availability without suppressing structural errors.
|
|
39
|
+
def resolve(entry, index)
|
|
40
|
+
target = entry['type'] == 'module' ? "modules/#{entry['name']}" : entry['target']
|
|
41
|
+
validate_destination!(target, index)
|
|
42
|
+
definition = entry.merge('required' => entry.fetch('required', false),
|
|
43
|
+
'destination' => "#{environment_root}/#{target}")
|
|
44
|
+
source = Pathname(entry['source']).expand_path(@project).realpath
|
|
45
|
+
unless source.directory? && HieraMountSchema.safe_source?(source.to_s)
|
|
46
|
+
raise ConfigurationError, "hiera.mounts.#{index}.source must resolve to a directory with a safe mount path"
|
|
47
|
+
end
|
|
48
|
+
raise Errno::EACCES unless source.readable? && source.executable?
|
|
49
|
+
|
|
50
|
+
definition.merge('resolved_source' => source.to_s, 'status' => 'available')
|
|
51
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::EPERM => e
|
|
52
|
+
if entry['required']
|
|
53
|
+
raise ConfigurationError, "hiera.mounts.#{index}.source is required but unavailable (#{e.class})",
|
|
54
|
+
cause: nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
definition.merge('status' => 'skipped',
|
|
58
|
+
'reason' => e.is_a?(Errno::ENOENT) ? 'missing source' : 'inaccessible source')
|
|
59
|
+
rescue SystemCallError
|
|
60
|
+
raise ConfigurationError, "hiera.mounts.#{index}.source cannot be resolved safely", cause: nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def environment_root
|
|
64
|
+
"/etc/puppetlabs/code/environments/#{@environment}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def validate_destination!(target, index)
|
|
68
|
+
current = @project
|
|
69
|
+
target.split('/').each do |part|
|
|
70
|
+
current = current.join(part)
|
|
71
|
+
next unless current.exist? || current.symlink?
|
|
72
|
+
|
|
73
|
+
resolved = current.realpath
|
|
74
|
+
unless resolved.directory? && resolved.to_s.start_with?("#{@project}/")
|
|
75
|
+
raise ConfigurationError, "hiera.mounts.#{index}.target must remain a directory inside the environment"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
rescue SystemCallError
|
|
79
|
+
raise ConfigurationError, "hiera.mounts.#{index}.target cannot be resolved safely", cause: nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
class ImageSchema
|
|
6
|
+
PATTERNS = {
|
|
7
|
+
'repository' => Images::Configuration::REPOSITORY, 'tag' => Images::Configuration::TAG,
|
|
8
|
+
'digest' => Images::Configuration::DIGEST, 'build' => %r{\A[a-zA-Z0-9_/-]+/Containerfile\z}
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
def self.validate!(value, path, effective: false)
|
|
12
|
+
raise ConfigurationError, "#{path} must be an image mapping" unless value.is_a?(Hash)
|
|
13
|
+
|
|
14
|
+
value.each do |key, item|
|
|
15
|
+
raise ConfigurationError, "#{path}.#{key} is invalid or unsupported" unless valid_field?(key, item)
|
|
16
|
+
end
|
|
17
|
+
validate_combination!(value, path) if effective || value.key?('reference')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.valid_field?(key, item)
|
|
21
|
+
return reference?(item) if key == 'reference'
|
|
22
|
+
|
|
23
|
+
item.is_a?(String) && PATTERNS.key?(key) && item.match?(PATTERNS[key])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.reference?(value)
|
|
27
|
+
return false unless value.is_a?(String)
|
|
28
|
+
|
|
29
|
+
repository, digest = value.split('@', 2)
|
|
30
|
+
if digest
|
|
31
|
+
return repository.match?(Images::Configuration::REPOSITORY) && digest.match?(Images::Configuration::DIGEST)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
repository, _, tag = value.rpartition(':')
|
|
35
|
+
repository.match?(Images::Configuration::REPOSITORY) && tag.match?(Images::Configuration::TAG)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.validate_combination!(value, path)
|
|
39
|
+
return validate_reference!(value, path) if value['reference']
|
|
40
|
+
|
|
41
|
+
if value['build']
|
|
42
|
+
raise ConfigurationError, "#{path}.build cannot be combined with an image reference" if value.keys.size > 1
|
|
43
|
+
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
return if value['repository'] && (value['tag'] || value['digest'])
|
|
47
|
+
|
|
48
|
+
raise ConfigurationError, "#{path} requires repository and tag/digest, reference, or build"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.validate_reference!(value, path)
|
|
52
|
+
return if value.keys.size == 1
|
|
53
|
+
|
|
54
|
+
raise ConfigurationError, "#{path}.reference cannot be combined with repository, tag, digest or build"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Explicit image selections replace inherited pins; a tag alongside an explicit digest is descriptive.
|
|
58
|
+
def self.override_base(lower, higher)
|
|
59
|
+
return {} if higher.key?('reference') || higher.key?('build')
|
|
60
|
+
return lower unless higher.keys.intersect?(%w[repository tag digest])
|
|
61
|
+
|
|
62
|
+
value = lower.except('reference', 'build')
|
|
63
|
+
value.delete('digest') if !higher.key?('digest') && higher.keys.intersect?(%w[repository tag])
|
|
64
|
+
value
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module Configuration
|
|
8
|
+
class Loader
|
|
9
|
+
DEFAULTS = File.expand_path('../../../config/defaults.yaml', __dir__).freeze
|
|
10
|
+
CLI_PATHS = [%w[runtime container_engine], %w[node_defaults os], %w[node_defaults version],
|
|
11
|
+
%w[node_defaults memory], %w[node_defaults cpus]].map(&:freeze).freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :ssh_preferences
|
|
14
|
+
|
|
15
|
+
def initialize(project_path: Dir.pwd, defaults_path: DEFAULTS, schema: Schema.new,
|
|
16
|
+
locations: Platform::Locations.new)
|
|
17
|
+
@project_path = ProjectRoot.resolve(project_path).path
|
|
18
|
+
@defaults_path = defaults_path
|
|
19
|
+
@schema = schema
|
|
20
|
+
@locations = locations
|
|
21
|
+
@ssh_preferences = {}.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load(overrides: {})
|
|
25
|
+
raise ConfigurationError, 'project path must be an existing directory' unless @project_path.directory?
|
|
26
|
+
|
|
27
|
+
defaults = read(@defaults_path)
|
|
28
|
+
project_file = @project_path.join('.empeira.yaml')
|
|
29
|
+
project = read(project_file)
|
|
30
|
+
user = User.new(path: @locations.user_configuration, schema: @schema).load
|
|
31
|
+
@ssh_preferences = Immutable.deep_freeze(user.delete('ssh') || {})
|
|
32
|
+
validate_overrides!(overrides)
|
|
33
|
+
effective([project, user, overrides].reduce(defaults) { |lower, higher| Merge.call(lower, higher) })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Recovery operations must not depend on a broken project or user file.
|
|
37
|
+
def load_defaults(overrides: {})
|
|
38
|
+
@ssh_preferences = {}.freeze
|
|
39
|
+
validate_overrides!(overrides)
|
|
40
|
+
effective(Merge.call(read(@defaults_path), overrides))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def effective(data)
|
|
46
|
+
@schema.validate_effective!(data)
|
|
47
|
+
HieraMounts.new(config: data.fetch('hiera'), project: @project_path,
|
|
48
|
+
environment: data.dig('server', 'environment'))
|
|
49
|
+
Eyaml.new(config: data.fetch('eyaml'), project: @project_path).mounts
|
|
50
|
+
ServerMounts.new(entries: data.dig('server', 'mounts'), project: @project_path)
|
|
51
|
+
data
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def read(path)
|
|
55
|
+
data = @schema.validate_fragment!(Document.new.read(path))
|
|
56
|
+
data.fetch('network', {}).delete('internet')
|
|
57
|
+
data
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_overrides!(overrides)
|
|
61
|
+
@schema.validate_fragment!(overrides)
|
|
62
|
+
overrides.each do |section, values|
|
|
63
|
+
unless values.is_a?(Hash) && values.keys.all? { |key| CLI_PATHS.include?([section, key]) }
|
|
64
|
+
raise ConfigurationError, "#{section} cannot be overridden through the CLI"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|