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,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Completion
|
|
5
|
+
# Only local configuration and declarative manifests; never acquire or execute.
|
|
6
|
+
class Addons
|
|
7
|
+
attr_reader :file_completion, :file_prefix, :replacement
|
|
8
|
+
|
|
9
|
+
def initialize(service: Application.new.addons)
|
|
10
|
+
@service = service
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def candidates(words, index, management: false)
|
|
14
|
+
prefix = words[index].to_s
|
|
15
|
+
return @service.names.grep(/^#{Regexp.escape(prefix)}/) if index.zero? || management
|
|
16
|
+
|
|
17
|
+
commands = local_commands(words.first)
|
|
18
|
+
return commands.keys.grep(/^#{Regexp.escape(prefix)}/) if index == 1
|
|
19
|
+
|
|
20
|
+
command = commands[words[1]]
|
|
21
|
+
command ? command_candidates(command, words[2...index], prefix) : []
|
|
22
|
+
rescue Empeira::Error, SystemCallError
|
|
23
|
+
[]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def local_commands(name)
|
|
29
|
+
manifest = @service.installation(name).local&.last
|
|
30
|
+
manifest ? manifest.data.fetch('commands') : {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def command_candidates(command, before, prefix)
|
|
34
|
+
options = command.fetch('options', {})
|
|
35
|
+
position, pending, literal = argument_position(before, options)
|
|
36
|
+
if !literal && prefix.start_with?('-') && !pending
|
|
37
|
+
return assignment(options, prefix) if prefix.include?('=')
|
|
38
|
+
|
|
39
|
+
return options.keys.grep(/^#{Regexp.escape(prefix)}/)
|
|
40
|
+
end
|
|
41
|
+
descriptor = pending || command.fetch('arguments', [])[position]
|
|
42
|
+
complete(descriptor, prefix)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def argument_position(words, options)
|
|
46
|
+
@position = 0
|
|
47
|
+
@pending = nil
|
|
48
|
+
@literal = false
|
|
49
|
+
words.each { |word| consume_word(word, options) }
|
|
50
|
+
[@position, @pending, @literal]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def consume_word(word, options)
|
|
54
|
+
if @pending
|
|
55
|
+
@pending = nil unless word == '='
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
return @position += 1 if @literal
|
|
59
|
+
|
|
60
|
+
case word
|
|
61
|
+
when '--' then @literal = true
|
|
62
|
+
when /\A-/ then @pending = option_value(word, options)
|
|
63
|
+
else @position += 1
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def option_value(word, options)
|
|
68
|
+
name, value = word.split('=', 2)
|
|
69
|
+
descriptor = options[name]
|
|
70
|
+
descriptor if descriptor && descriptor['completion'] != 'none' && (value.nil? || value.empty?)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def assignment(options, prefix)
|
|
74
|
+
name, value = prefix.split('=', 2)
|
|
75
|
+
@replacement = "#{name}="
|
|
76
|
+
complete(options[name], value.to_s).map { |item| "#{@replacement}#{item}" }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def complete(descriptor, prefix)
|
|
80
|
+
return [] unless descriptor
|
|
81
|
+
|
|
82
|
+
case descriptor['completion']
|
|
83
|
+
when 'file', 'directory'
|
|
84
|
+
@file_completion = descriptor['completion']
|
|
85
|
+
@file_prefix = prefix
|
|
86
|
+
[]
|
|
87
|
+
when 'enum' then descriptor.fetch('values').grep(/^#{Regexp.escape(prefix)}/)
|
|
88
|
+
else []
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Completion
|
|
7
|
+
module Bash
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# COMP_WORDS retains shell quoting, including an unfinished quoted word.
|
|
11
|
+
# Shellwords only parses text; it never evaluates substitutions or commands.
|
|
12
|
+
def path_prefix(value)
|
|
13
|
+
return value unless value.match?(/[\\'"]/)
|
|
14
|
+
|
|
15
|
+
[value, "#{value}'", "#{value}\""].each do |candidate|
|
|
16
|
+
words = Shellwords.split(candidate)
|
|
17
|
+
return words.first if words.size == 1
|
|
18
|
+
rescue ArgumentError
|
|
19
|
+
next
|
|
20
|
+
end
|
|
21
|
+
value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def generate
|
|
25
|
+
<<~'BASH'
|
|
26
|
+
# Empeira Bash completion. Load with: source <(empeira completion bash)
|
|
27
|
+
_empeira_complete() {
|
|
28
|
+
local candidate mode='' prefix='' replacement='' phase=0
|
|
29
|
+
COMPREPLY=()
|
|
30
|
+
while IFS= read -r -d '' candidate; do
|
|
31
|
+
if [[ "$candidate" == $'\x1f'file || "$candidate" == $'\x1f'directory ]]; then
|
|
32
|
+
mode="${candidate:1}"
|
|
33
|
+
phase=1
|
|
34
|
+
elif [[ $phase == 1 ]]; then
|
|
35
|
+
prefix="$candidate"
|
|
36
|
+
phase=2
|
|
37
|
+
elif [[ $phase == 2 ]]; then
|
|
38
|
+
replacement="$candidate"
|
|
39
|
+
phase=3
|
|
40
|
+
else
|
|
41
|
+
COMPREPLY+=("$candidate")
|
|
42
|
+
fi
|
|
43
|
+
done < <(printf '%s\0' "${COMP_WORDS[@]:1}" | "${COMP_WORDS[0]}" completion candidates "$COMP_CWORD" --null)
|
|
44
|
+
if [[ -n "$mode" ]]; then
|
|
45
|
+
compopt -o filenames 2>/dev/null || :
|
|
46
|
+
while IFS= read -r -d '' candidate; do COMPREPLY+=("$replacement$candidate"); done < <(
|
|
47
|
+
# Native pathname expansion with NUL framing also preserves newlines.
|
|
48
|
+
# Shell settings change only in this subshell, including on Bash 3.2.
|
|
49
|
+
shopt -s nullglob dotglob
|
|
50
|
+
for candidate in "$prefix"*; do
|
|
51
|
+
[[ "$mode" == directory && ! -d "$candidate" ]] && continue
|
|
52
|
+
printf '%s\0' "$candidate"
|
|
53
|
+
done
|
|
54
|
+
)
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
complete -o filenames -F _empeira_complete empeira
|
|
58
|
+
BASH
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'addons'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Completion
|
|
7
|
+
# Shared CLI vocabulary and context parsing stay in one shell-neutral catalog.
|
|
8
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
9
|
+
class Catalog
|
|
10
|
+
COMMANDS = {
|
|
11
|
+
[] => %w[help version config proxy up status down destroy node images self-update update browser completion
|
|
12
|
+
addon addons],
|
|
13
|
+
['addons'] => %w[help list info sync update],
|
|
14
|
+
['update'] => ['help', *Updates::Service::TARGETS],
|
|
15
|
+
['proxy'] => %w[help show],
|
|
16
|
+
['config'] => %w[help show validate],
|
|
17
|
+
['node'] => %w[help run start stop destroy list exec shell console ssh puppet logs],
|
|
18
|
+
['completion'] => %w[help bash],
|
|
19
|
+
['version'] => ['--verbose']
|
|
20
|
+
}.tap { |commands| Immutable.deep_freeze(commands) }
|
|
21
|
+
GLOBAL_OPTIONS = %w[--container-engine --help].freeze
|
|
22
|
+
NODE_OPTIONS = %w[--provider --os --version --memory --cpus].freeze
|
|
23
|
+
VALUE_OPTIONS = %w[--container-engine --provider --os --version --memory --cpus --user
|
|
24
|
+
--identity --port --timeout --project-path].freeze
|
|
25
|
+
|
|
26
|
+
attr_reader :addon_completion
|
|
27
|
+
|
|
28
|
+
def candidates(words, index)
|
|
29
|
+
before = words.take(index)
|
|
30
|
+
context = command_path(before)
|
|
31
|
+
return addon_candidates(words, index, context) if addon_context?(context)
|
|
32
|
+
return [] if before.include?('--')
|
|
33
|
+
|
|
34
|
+
@before = before
|
|
35
|
+
prefix = words[index].to_s
|
|
36
|
+
assignment = assignment_prefix(prefix, before)
|
|
37
|
+
return assignment_candidates(assignment, context) if assignment
|
|
38
|
+
|
|
39
|
+
choices = values(pending_option(before), context)
|
|
40
|
+
choices ||= context_choices(context)
|
|
41
|
+
choices.grep(/^#{Regexp.escape(prefix)}/)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def addon_context?(context)
|
|
47
|
+
context.first == 'addon' || context == %w[addons info]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def addon_candidates(words, index, context)
|
|
51
|
+
@addon_completion = Addons.new
|
|
52
|
+
offset = words.index(context.first) + (context.first == 'addon' ? 1 : 2)
|
|
53
|
+
@addon_completion.candidates(words.drop(offset), index - offset, management: context.first == 'addons')
|
|
54
|
+
rescue Error, SystemCallError
|
|
55
|
+
[]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def context_choices(context)
|
|
59
|
+
Array(node_names(context)) + COMMANDS.fetch(context, []) + options(context)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def assignment_candidates(assignment, context)
|
|
63
|
+
option, value, replacement = assignment
|
|
64
|
+
values(option, context).to_a.grep(/^#{Regexp.escape(value)}/).map { |item| "#{replacement}#{item}" }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def assignment_prefix(prefix, before)
|
|
68
|
+
if prefix.start_with?('--') && prefix.include?('=')
|
|
69
|
+
option, value = prefix.split('=', 2)
|
|
70
|
+
return [option, value, "#{option}="] if VALUE_OPTIONS.include?(option)
|
|
71
|
+
elsif prefix.start_with?('=') && VALUE_OPTIONS.include?(before.last)
|
|
72
|
+
return [before.last, prefix.delete_prefix('='), '=']
|
|
73
|
+
end
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def pending_option(before)
|
|
78
|
+
option = before.last == '=' ? before[-2] : before.last
|
|
79
|
+
option&.delete_suffix('=')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def command_path(words)
|
|
83
|
+
path = []
|
|
84
|
+
pending_value = false
|
|
85
|
+
words.each do |word|
|
|
86
|
+
if pending_value
|
|
87
|
+
pending_value = word == '='
|
|
88
|
+
elsif value_option?(word, path)
|
|
89
|
+
pending_value = true
|
|
90
|
+
elsif command_word?(word, path)
|
|
91
|
+
path << word
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
path
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def value_option?(word, path)
|
|
98
|
+
option = word.delete_suffix('=')
|
|
99
|
+
VALUE_OPTIONS.include?(option) && !(option == '--version' && path.empty?)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def command_word?(word, path)
|
|
103
|
+
!word.start_with?('-') && path.size < 2
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def values(option, context)
|
|
107
|
+
return Configuration::Schema::CONTAINER_ENGINES if option == '--container-engine'
|
|
108
|
+
return Empeira::Node.registry.names if option == '--provider' && context == %w[node run]
|
|
109
|
+
return node_values(option) if %w[--os --version].include?(option) && context == %w[node run]
|
|
110
|
+
return [] if VALUE_OPTIONS.include?(option)
|
|
111
|
+
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def node_names(context)
|
|
116
|
+
return unless context.size == 2 && context.first == 'node' && %w[start stop destroy exec shell console ssh
|
|
117
|
+
puppet logs].include?(context.last)
|
|
118
|
+
return recovery_node_names if context.last == 'destroy'
|
|
119
|
+
|
|
120
|
+
completion_application.nodes.names
|
|
121
|
+
rescue Error
|
|
122
|
+
[]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def completion_application
|
|
126
|
+
Application.new
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def recovery_node_names
|
|
130
|
+
context = Application.new(recovery: true, recovery_path: option_value('--project-path')).context
|
|
131
|
+
state = Infrastructure::Store.new(context: context, recovery: true).load
|
|
132
|
+
(state&.fetch('nodes', {})&.keys || []).select { |name| Node::Inventory.valid_name?(name) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def option_value(option)
|
|
136
|
+
@before.each_with_index.filter_map do |word, index|
|
|
137
|
+
if word == option || word == "#{option}="
|
|
138
|
+
value = @before[index + 1]
|
|
139
|
+
value == '=' ? @before[index + 2] : value&.delete_prefix('=')
|
|
140
|
+
elsif word.start_with?("#{option}=")
|
|
141
|
+
word.split('=', 2).last
|
|
142
|
+
end
|
|
143
|
+
end.last
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def node_values(option)
|
|
147
|
+
images = completion_application.context.configuration.dig('images', 'nodes')
|
|
148
|
+
return images.keys if option == '--os'
|
|
149
|
+
|
|
150
|
+
images.fetch(selected_os || completion_application.context.configuration.dig('node_defaults', 'os'), {}).keys
|
|
151
|
+
rescue Error
|
|
152
|
+
[]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def selected_os
|
|
156
|
+
option_value('--os')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def options(context)
|
|
160
|
+
result = GLOBAL_OPTIONS.dup
|
|
161
|
+
result << '--version' if context.empty?
|
|
162
|
+
result.push('--user', '--identity', '--port') if context == %w[node ssh]
|
|
163
|
+
result << '--timeout' if context == %w[node exec]
|
|
164
|
+
result << '--project-path' if [['destroy'], %w[node destroy]].include?(context)
|
|
165
|
+
result.concat(NODE_OPTIONS) if context == %w[node run]
|
|
166
|
+
result
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
class AdditionalServices
|
|
6
|
+
NAME = /\A[a-z][a-z0-9]*(?:-[a-z0-9]+)*\z/
|
|
7
|
+
RESERVED = %w[server puppetdb puppetdb-backend postgres dns coredns proxy gateway browser browser-ui vm-gateway
|
|
8
|
+
bootstrap-proxy module-installer ca ssl server-data postgres-data
|
|
9
|
+
puppetdb-data node localhost].freeze
|
|
10
|
+
FIELDS = %w[name image environment command configuration].freeze
|
|
11
|
+
|
|
12
|
+
def self.validate!(entries, path)
|
|
13
|
+
raise ConfigurationError, "#{path} must be an array" unless entries.is_a?(Array)
|
|
14
|
+
|
|
15
|
+
names = []
|
|
16
|
+
entries.each_with_index do |entry, index|
|
|
17
|
+
location = "#{path}[#{index}]"
|
|
18
|
+
validate_entry!(entry, location)
|
|
19
|
+
name = entry.fetch('name')
|
|
20
|
+
raise ConfigurationError, "#{location}.name collides with another service" if names.include?(name)
|
|
21
|
+
|
|
22
|
+
names << name
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.validate_entry!(entry, path)
|
|
27
|
+
unless entry.is_a?(Hash) && (entry.keys - FIELDS).empty?
|
|
28
|
+
raise ConfigurationError, "#{path} must contain only name, image, environment, command and configuration"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
name!(entry['name'], path)
|
|
32
|
+
image!(entry['image'], "#{path}.image")
|
|
33
|
+
optional_fields!(entry, path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.optional_fields!(entry, path)
|
|
37
|
+
environment!(entry.fetch('environment', {}), "#{path}.environment")
|
|
38
|
+
command!(entry['command'], "#{path}.command") if entry.key?('command')
|
|
39
|
+
ServiceConfiguration.validate!(entry['configuration'], "#{path}.configuration") if entry.key?('configuration')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.name!(name, path)
|
|
43
|
+
return if name.is_a?(String) && name.length <= 63 && name.match?(NAME) && !RESERVED.include?(name)
|
|
44
|
+
|
|
45
|
+
raise ConfigurationError, "#{path}.name must be a unique lowercase DNS label, excluding built-in names"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.image!(image, path)
|
|
49
|
+
unless image.is_a?(Hash) && image.size == 2 && %w[repository tag].all? { |key| image.key?(key) }
|
|
50
|
+
raise ConfigurationError, "#{path} requires only repository and tag"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
ImageSchema.validate!(image, path, effective: true)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.environment!(environment, path)
|
|
57
|
+
return if environment.is_a?(Hash) && environment.all? do |key, value|
|
|
58
|
+
key.is_a?(String) && key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/) &&
|
|
59
|
+
value.is_a?(String) && !value.include?("\0")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
raise ConfigurationError, "#{path} must map variable names to strings without null bytes"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.command!(command, path)
|
|
66
|
+
return if command.is_a?(Array) && !command.empty? && command.all? do |value|
|
|
67
|
+
value.is_a?(String) && !value.include?("\0")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
raise ConfigurationError, "#{path} must be a nonempty argument array of strings without null bytes"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Configuration
|
|
5
|
+
module Addons
|
|
6
|
+
NAME = /\A[a-z][a-z0-9_-]*\z/
|
|
7
|
+
|
|
8
|
+
def self.validate!(entries, path = 'addons', effective: false)
|
|
9
|
+
raise ConfigurationError, "#{path} must be a mapping" unless entries.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
entries.each do |name, entry|
|
|
12
|
+
unless name.is_a?(String) && name.size <= 63 && name.match?(NAME)
|
|
13
|
+
raise ConfigurationError, "#{path} contains an invalid addon name"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
entry!(entry, "#{path}.#{name}", effective: effective)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.entry!(entry, path, effective:)
|
|
21
|
+
unless entry.is_a?(Hash) && (entry.keys - %w[git ref config]).empty?
|
|
22
|
+
raise ConfigurationError, "#{path} may contain only git, ref and config"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
require_fields!(entry, path) if effective
|
|
26
|
+
validate_fields!(entry, path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.validate_fields!(entry, path)
|
|
30
|
+
validate_config!(entry['config'], "#{path}.config") if entry.key?('config')
|
|
31
|
+
if entry.key?('git') && !valid_source?(entry['git'])
|
|
32
|
+
raise ConfigurationError, "#{path}.git must be a credential-free HTTP(S) or SSH Git source"
|
|
33
|
+
end
|
|
34
|
+
return unless entry.key?('ref') && !valid_ref?(entry['ref'])
|
|
35
|
+
|
|
36
|
+
raise ConfigurationError, "#{path}.ref must be a nonempty Git reference without control characters"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.validate_config!(config, path)
|
|
40
|
+
raise ConfigurationError, "#{path} must be a mapping" unless config.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
config_value!(config, path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.config_value!(value, path)
|
|
46
|
+
case value
|
|
47
|
+
when Hash then config_mapping!(value, path)
|
|
48
|
+
when Array then value.each_with_index { |child, index| config_value!(child, "#{path}[#{index}]") }
|
|
49
|
+
when Float
|
|
50
|
+
raise ConfigurationError, "#{path} must contain finite numbers" unless value.finite?
|
|
51
|
+
when String, Integer, TrueClass, FalseClass, NilClass then nil
|
|
52
|
+
else raise ConfigurationError, "#{path} must contain only mappings, arrays and scalar values"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.config_mapping!(value, path)
|
|
57
|
+
value.each do |key, child|
|
|
58
|
+
unless key.is_a?(String) && key.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
59
|
+
raise ConfigurationError, "#{path} has an invalid key; use lowercase letters, digits and underscores"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
config_value!(child, "#{path}.#{key}")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.require_fields!(entry, path)
|
|
67
|
+
%w[git ref].each do |key|
|
|
68
|
+
raise ConfigurationError, "#{path}.#{key} is required" unless entry.key?(key)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.valid_ref?(value)
|
|
73
|
+
value.is_a?(String) && !value.strip.empty? && !value.start_with?('-') && !value.match?(/[\p{Cc}\p{Cf}]/)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.valid_source?(value)
|
|
77
|
+
return false unless Modules::HostGit.valid_remote?(value)
|
|
78
|
+
return true unless value.include?('://')
|
|
79
|
+
|
|
80
|
+
uri = URI.parse(value)
|
|
81
|
+
uri.query.nil? && uri.fragment.nil?
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Configuration
|
|
7
|
+
# Fragment validation allows partial OS overrides; effective validation checks the resolved source.
|
|
8
|
+
# rubocop:disable-next Metrics/ModuleLength -- Keep fragment and effective validation of this source contract together.
|
|
9
|
+
module AgentSchema
|
|
10
|
+
extend AgentValues
|
|
11
|
+
|
|
12
|
+
SOURCE_VALIDATORS = { 'url' => :template_url!, 'sha256' => :digest!, 'suffix' => :suffix!,
|
|
13
|
+
'suite' => :token!, 'component' => :token!, 'key' => :artifact!,
|
|
14
|
+
'release' => :artifact!, 'verify_signatures' => :boolean!,
|
|
15
|
+
'destinations' => :destinations! }.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def mapping!(value, path)
|
|
20
|
+
raise ConfigurationError, "#{path} must be a mapping" unless value.is_a?(Hash)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def catalog!(value, path)
|
|
24
|
+
mapping!(value, path)
|
|
25
|
+
value.each do |key, item|
|
|
26
|
+
unless key.is_a?(String) && key.match?(/\A[a-z0-9][a-z0-9_.-]*\z/)
|
|
27
|
+
raise ConfigurationError, "#{path} has an invalid catalog key"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
yield item, "#{path}.#{key}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate!(data, path)
|
|
35
|
+
mapping!(data, path)
|
|
36
|
+
data.each do |key, value|
|
|
37
|
+
field = "#{path}.#{key}"
|
|
38
|
+
case key
|
|
39
|
+
when 'package' then package_token!(value, field)
|
|
40
|
+
when 'version' then token!(value, field)
|
|
41
|
+
when 'cache' then cache!(value, field)
|
|
42
|
+
when 'install' then install!(value, field)
|
|
43
|
+
else raise ConfigurationError, "#{field} is unsupported"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cache!(data, path)
|
|
49
|
+
mapping!(data, path)
|
|
50
|
+
data.each do |key, value|
|
|
51
|
+
raise ConfigurationError, "#{path}.#{key} is unsupported" unless key == 'enabled'
|
|
52
|
+
|
|
53
|
+
boolean!(value, "#{path}.enabled")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def effective!(data)
|
|
58
|
+
required!(data, %w[package version cache install], 'agent')
|
|
59
|
+
required!(data.fetch('cache'), ['enabled'], 'agent.cache')
|
|
60
|
+
install = data.fetch('install')
|
|
61
|
+
required!(install, %w[method repositories apt dnf packages], 'agent.install')
|
|
62
|
+
install.fetch('repositories').each do |key, source|
|
|
63
|
+
required!(source, ['url'], "agent.install.repositories.#{key}")
|
|
64
|
+
end
|
|
65
|
+
%w[apt dnf].each { |manager| effective_sources!(install.fetch(manager), "agent.install.#{manager}") }
|
|
66
|
+
return unless install.fetch('method') == 'package' && install.fetch('packages').empty?
|
|
67
|
+
|
|
68
|
+
raise ConfigurationError, 'agent.install.packages must define at least one distribution and architecture'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def effective_sources!(sources, path)
|
|
72
|
+
sources.each do |key, source|
|
|
73
|
+
resolved = Merge.call(sources.fetch('default', {}), source, path: [*path.split('.'), key])
|
|
74
|
+
required!(resolved, ['url'], "#{path}.#{key}")
|
|
75
|
+
source!(resolved, "#{path}.#{key}", manager: path.split('.').last)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def install!(data, path)
|
|
80
|
+
mapping!(data, path)
|
|
81
|
+
data.each do |key, value|
|
|
82
|
+
field = "#{path}.#{key}"
|
|
83
|
+
case key
|
|
84
|
+
when 'method'
|
|
85
|
+
raise ConfigurationError, "#{field} must be repository or package" unless %w[repository
|
|
86
|
+
package].include?(value)
|
|
87
|
+
when 'repositories' then catalog!(value, field) { |source, source_path| source!(source, source_path) }
|
|
88
|
+
when 'apt', 'dnf' then catalog!(value, field) do |source, source_path|
|
|
89
|
+
source!(source, source_path, manager: key)
|
|
90
|
+
end
|
|
91
|
+
when 'packages' then package_sources!(value, field)
|
|
92
|
+
else raise ConfigurationError, "#{field} is unsupported"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def package_sources!(data, path)
|
|
98
|
+
catalog!(data, path) do |architectures, os_path|
|
|
99
|
+
catalog!(architectures, os_path) do |artifact, artifact_path|
|
|
100
|
+
raise ConfigurationError, "#{artifact_path} has unsupported architecture" unless
|
|
101
|
+
%w[amd64 arm64].include?(artifact_path.split('.').last)
|
|
102
|
+
|
|
103
|
+
artifact!(artifact, artifact_path, package: true)
|
|
104
|
+
family = os_path.delete_prefix("#{path}.")
|
|
105
|
+
extension = family.start_with?('ubuntu', 'debian') ? '.deb' : '.rpm'
|
|
106
|
+
unless URI(artifact.fetch('url')).path.end_with?(extension)
|
|
107
|
+
raise ConfigurationError, "#{artifact_path}.url must end in #{extension}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def artifact!(data, path, package: false)
|
|
114
|
+
mapping!(data, path)
|
|
115
|
+
allowed = package ? %w[url sha256 verify_signatures key] : %w[url sha256]
|
|
116
|
+
fields!(data, allowed, path)
|
|
117
|
+
|
|
118
|
+
required!(data, ['url'], path)
|
|
119
|
+
https_url!(data.fetch('url'), "#{path}.url")
|
|
120
|
+
digest!(data['sha256'], "#{path}.sha256") if data.key?('sha256')
|
|
121
|
+
boolean!(data['verify_signatures'], "#{path}.verify_signatures") if data.key?('verify_signatures')
|
|
122
|
+
artifact!(data['key'], "#{path}.key") if data.key?('key')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def source!(data, path, manager: nil)
|
|
126
|
+
mapping!(data, path)
|
|
127
|
+
allowed = %w[url sha256 suffix destinations verify_signatures]
|
|
128
|
+
allowed += %w[suite component release key] if manager == 'apt'
|
|
129
|
+
allowed += ['key'] if manager == 'dnf'
|
|
130
|
+
fields!(data, allowed, path)
|
|
131
|
+
data.each { |key, value| public_send(SOURCE_VALIDATORS.fetch(key), value, "#{path}.#{key}") }
|
|
132
|
+
return unless data.key?('release') && data.key?('key')
|
|
133
|
+
|
|
134
|
+
raise ConfigurationError, "#{path} cannot specify both release and key"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def destinations!(value, path)
|
|
138
|
+
raise ConfigurationError, "#{path} must contain valid proxy destination domains" unless
|
|
139
|
+
BootstrapGuests.destinations?(value)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def fields!(data, allowed, path)
|
|
143
|
+
key = (data.keys - allowed).first
|
|
144
|
+
raise ConfigurationError, "#{path}.#{key} is unsupported" if key
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def template_url!(value, path)
|
|
148
|
+
raise ConfigurationError, "#{path} must be a credential-free HTTPS URL" unless value.is_a?(String)
|
|
149
|
+
|
|
150
|
+
expanded = value.gsub(/\$(?:\{([^}]+)\}|([a-zA-Z_][a-zA-Z_0-9]*))/) do
|
|
151
|
+
name = Regexp.last_match(1) || Regexp.last_match(2)
|
|
152
|
+
raise ConfigurationError, "#{path} has unknown placeholder #{name}" unless %w[suite releasever
|
|
153
|
+
basearch].include?(name)
|
|
154
|
+
|
|
155
|
+
'target'
|
|
156
|
+
end
|
|
157
|
+
raise ConfigurationError, "#{path} has an invalid placeholder" if expanded.include?('$')
|
|
158
|
+
|
|
159
|
+
https_url!(expanded, path)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|