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,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Addons
|
|
5
|
+
class Installation
|
|
6
|
+
attr_reader :name, :source, :ref
|
|
7
|
+
|
|
8
|
+
def initialize(name:, definition:, context:, runner:)
|
|
9
|
+
@name = name
|
|
10
|
+
@source, @ref = definition.values_at('git', 'ref')
|
|
11
|
+
@runner = runner
|
|
12
|
+
@project = context.project.path
|
|
13
|
+
@storage = Storage.new(base: cache_root(context), source: source, ref: ref)
|
|
14
|
+
rescue Empeira::Error, SystemCallError => e
|
|
15
|
+
failure(e)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def local
|
|
19
|
+
record = @storage.installed
|
|
20
|
+
return unless record
|
|
21
|
+
|
|
22
|
+
commit, path = record
|
|
23
|
+
[commit, path, Manifest.new(name: name, path: path)]
|
|
24
|
+
rescue Empeira::Error, SystemCallError => e
|
|
25
|
+
failure(e)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def synchronize(missing_only: false)
|
|
29
|
+
return local if missing_only && local
|
|
30
|
+
|
|
31
|
+
@storage.synchronize do
|
|
32
|
+
return local if missing_only && local
|
|
33
|
+
|
|
34
|
+
acquire
|
|
35
|
+
commit = resolve
|
|
36
|
+
materialize(commit)
|
|
37
|
+
@storage.publish(commit)
|
|
38
|
+
local
|
|
39
|
+
end
|
|
40
|
+
rescue Empeira::Error, SystemCallError => e
|
|
41
|
+
failure(e)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def cache_root(context)
|
|
47
|
+
root = Modules::GitSafety.canonical_path(context.locations.addons)
|
|
48
|
+
if root == @project || root.to_s.start_with?("#{@project}/")
|
|
49
|
+
raise Error.new(name, 'user addon cache must be outside the control repository')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
root
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def failure(error)
|
|
56
|
+
raise error if error.is_a?(Error)
|
|
57
|
+
|
|
58
|
+
message = error.is_a?(SystemCallError) ? 'cache is inaccessible or installation is incomplete' : error.message
|
|
59
|
+
raise Error.new(name, message), cause: nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def acquire
|
|
63
|
+
git = Modules::HostGit.new(runner: @runner, project: @project, passive: true)
|
|
64
|
+
entries = [{ 'name' => 'addon', 'remote' => source }]
|
|
65
|
+
return git.prepare(entries, directory: @storage.root) if @storage.mirror.exist?
|
|
66
|
+
|
|
67
|
+
@storage.stage do |directory|
|
|
68
|
+
mirror = git.prepare(entries, directory: directory).fetch('addon')
|
|
69
|
+
File.rename(mirror, @storage.mirror)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def resolve
|
|
74
|
+
result = git('rev-parse', '--verify', '--end-of-options', "#{ref}^{commit}")
|
|
75
|
+
commit = result.stdout.strip
|
|
76
|
+
unless result.success? && commit.match?(Storage::COMMIT)
|
|
77
|
+
raise Error.new(name, 'Git reference is unknown or does not resolve to a commit'), cause: nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
commit
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def materialize(commit)
|
|
84
|
+
final = @storage.revision(commit)
|
|
85
|
+
return Manifest.new(name: name, path: final) if final.exist?
|
|
86
|
+
|
|
87
|
+
@storage.stage do |directory|
|
|
88
|
+
checkout(commit, directory)
|
|
89
|
+
Manifest.new(name: name, path: directory)
|
|
90
|
+
File.rename(directory, final)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def checkout(commit, directory)
|
|
95
|
+
Tempfile.create('.index-', @storage.root) do |index|
|
|
96
|
+
File.unlink(index.path)
|
|
97
|
+
environment = { 'GIT_INDEX_FILE' => index.path }
|
|
98
|
+
tree = git('read-tree', commit, environment: environment)
|
|
99
|
+
raise Error.new(name, 'Git could not read the resolved commit') unless tree.success?
|
|
100
|
+
|
|
101
|
+
files = git('--work-tree', directory.to_s, 'checkout-index', '--all', "--prefix=#{directory}/",
|
|
102
|
+
environment: environment)
|
|
103
|
+
raise Error.new(name, 'Git could not materialize the resolved commit') unless files.success?
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def git(*arguments, environment: {})
|
|
108
|
+
# Authentication happens only in HostGit. Local export must not run
|
|
109
|
+
# user-configured smudge filters from .gitattributes or hooks.
|
|
110
|
+
environment = { 'GIT_CONFIG_GLOBAL' => File::NULL, 'GIT_CONFIG_NOSYSTEM' => '1',
|
|
111
|
+
'GIT_CONFIG_COUNT' => '0', 'GIT_CONFIG_PARAMETERS' => nil, 'GIT_ATTR_NOSYSTEM' => '1' }
|
|
112
|
+
.merge(environment)
|
|
113
|
+
@runner.run('git', arguments: ['-c', "core.hooksPath=#{File::NULL}", '-c', 'core.fsmonitor=false',
|
|
114
|
+
'--git-dir', @storage.mirror.to_s, *arguments],
|
|
115
|
+
environment: environment, directory: @project, timeout: 300)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Addons
|
|
5
|
+
class Manifest
|
|
6
|
+
TOKEN = /\A[a-zA-Z0-9][a-zA-Z0-9_-]*\z/
|
|
7
|
+
OPTION = /\A--?[a-zA-Z0-9][a-zA-Z0-9-]*\z/
|
|
8
|
+
COMPLETIONS = %w[file directory enum none].freeze
|
|
9
|
+
attr_reader :data, :executable
|
|
10
|
+
|
|
11
|
+
def initialize(name:, path:)
|
|
12
|
+
@name = name
|
|
13
|
+
@root = Pathname(path).realpath
|
|
14
|
+
manifest = contained!('empeira-addon.yaml', 'manifest')
|
|
15
|
+
@data = Configuration::Document.new.read(manifest)
|
|
16
|
+
validate!
|
|
17
|
+
@executable = contained!(data.fetch('executable'), 'executable')
|
|
18
|
+
fail!('executable is not executable', 126) unless @executable.executable?
|
|
19
|
+
Immutable.deep_freeze(@data)
|
|
20
|
+
rescue ConfigurationError
|
|
21
|
+
fail!('manifest must contain valid, unique YAML data without aliases or object tags')
|
|
22
|
+
rescue SystemCallError
|
|
23
|
+
fail!('installation or manifest is missing or inaccessible')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def fail!(message, status = 1)
|
|
29
|
+
raise Error.new(@name, message, exit_status: status), cause: nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def contained!(relative, label)
|
|
33
|
+
fail!("#{label} must be a relative path without traversal") unless relative?(relative)
|
|
34
|
+
path = @root.join(relative)
|
|
35
|
+
fail!("#{label} is missing", label == 'executable' ? 127 : 1) unless path.file?
|
|
36
|
+
fail!("#{label} escapes the addon directory") unless path.realpath.to_s.start_with?("#{@root}/")
|
|
37
|
+
path.realpath
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def relative?(value)
|
|
41
|
+
value.is_a?(String) && !value.empty? && !value.match?(/[\p{Cc}\p{Cf}]/) &&
|
|
42
|
+
!Pathname(value).absolute? && !value.split('/').intersect?(%w[. ..])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate!
|
|
46
|
+
mapping!(data, %w[schema description executable commands], 'manifest', required: true)
|
|
47
|
+
unless data['schema'].is_a?(Integer) && data['schema'] == 1
|
|
48
|
+
fail!('unsupported manifest schema; expected integer 1')
|
|
49
|
+
end
|
|
50
|
+
text!(data['description'], 'description')
|
|
51
|
+
commands!(data['commands'])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def commands!(commands)
|
|
55
|
+
fail!('commands must be a mapping') unless commands.is_a?(Hash)
|
|
56
|
+
commands.each do |name, command|
|
|
57
|
+
fail!('invalid command name') unless name.is_a?(String) && name.match?(TOKEN)
|
|
58
|
+
command!(command)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def command!(command)
|
|
63
|
+
mapping!(command, %w[description arguments options], 'command')
|
|
64
|
+
text!(command['description'], 'command description')
|
|
65
|
+
arguments = command.fetch('arguments', [])
|
|
66
|
+
fail!('arguments must be an array') unless arguments.is_a?(Array)
|
|
67
|
+
arguments.each { |argument| argument!(argument) }
|
|
68
|
+
options!(command.fetch('options', {}))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def options!(options)
|
|
72
|
+
fail!('options must be a mapping') unless options.is_a?(Hash)
|
|
73
|
+
options.each do |name, value|
|
|
74
|
+
fail!('invalid option name') unless name.is_a?(String) && name.match?(OPTION)
|
|
75
|
+
completion!(value, option: true)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def argument!(argument)
|
|
80
|
+
completion!(argument)
|
|
81
|
+
name = argument['name']
|
|
82
|
+
fail!('argument requires a name') unless name.is_a?(String) && name.match?(TOKEN)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def completion!(value, option: false)
|
|
86
|
+
mapping!(value, option ? %w[description completion values] : %w[name completion values], 'completion')
|
|
87
|
+
text!(value['description'], 'option description') if option && value.key?('description')
|
|
88
|
+
fail!('unknown or missing completion type') unless COMPLETIONS.include?(value['completion'])
|
|
89
|
+
return enum!(value['values']) if value['completion'] == 'enum'
|
|
90
|
+
|
|
91
|
+
fail!('values are only supported for enum completion') if value.key?('values')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def enum!(values)
|
|
95
|
+
fail!('enum completion requires an array of values') unless values.is_a?(Array) && !values.empty?
|
|
96
|
+
values.each { |value| text!(value, 'enum value') }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def mapping!(value, fields, label, required: false)
|
|
100
|
+
return if value.is_a?(Hash) && (value.keys - fields).empty? && (!required || (fields - value.keys).empty?)
|
|
101
|
+
|
|
102
|
+
fail!("#{label} has missing or unsupported fields")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def text!(value, label)
|
|
106
|
+
return if value.is_a?(String) && !value.strip.empty? && !value.match?(/[\p{Cc}\p{Cf}]/)
|
|
107
|
+
|
|
108
|
+
fail!("#{label} must be a nonempty string without control characters")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Addons
|
|
5
|
+
class Service
|
|
6
|
+
def initialize(context:, runner:)
|
|
7
|
+
@context = context
|
|
8
|
+
@runner = runner
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def names
|
|
12
|
+
@context.configuration.fetch('addons').keys
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def installation(name)
|
|
16
|
+
definition = @context.configuration.fetch('addons')[name]
|
|
17
|
+
raise Error.new(name, 'not configured') unless definition
|
|
18
|
+
|
|
19
|
+
Installation.new(name: name, definition: definition, context: @context, runner: @runner)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def synchronize(missing_only: false)
|
|
23
|
+
errors = names.filter_map do |name|
|
|
24
|
+
installation(name).synchronize(missing_only: missing_only)
|
|
25
|
+
nil
|
|
26
|
+
rescue Error => e
|
|
27
|
+
e.message
|
|
28
|
+
end
|
|
29
|
+
raise Error.new('synchronization', errors.join("\n")) unless errors.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def execute(name, arguments)
|
|
33
|
+
entry = installation(name)
|
|
34
|
+
_, path, manifest = entry.synchronize(missing_only: true)
|
|
35
|
+
environment = execution_environment(entry, path, [manifest.executable.to_s, *arguments])
|
|
36
|
+
@runner.replace(manifest.executable.to_s, arguments: arguments, environment: environment)
|
|
37
|
+
rescue Execution::LaunchError => e
|
|
38
|
+
raise Error.new(name, e.message, exit_status: e.exit_status), cause: nil
|
|
39
|
+
rescue ExecutionError => e
|
|
40
|
+
raise Error.new(name, e.message), cause: nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def execution_environment(entry, path, arguments)
|
|
46
|
+
Environment.new(context: @context, name: entry.name, ref: entry.ref, path: path).export(arguments: arguments)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require 'digest'
|
|
6
|
+
|
|
7
|
+
module Empeira
|
|
8
|
+
module Addons
|
|
9
|
+
class Storage
|
|
10
|
+
COMMIT = /\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/
|
|
11
|
+
attr_reader :root, :source_key
|
|
12
|
+
|
|
13
|
+
def initialize(base:, source:, ref:)
|
|
14
|
+
@base = Pathname(base)
|
|
15
|
+
@source_key = Digest::SHA256.hexdigest(source)
|
|
16
|
+
@root = @base.join(source_key)
|
|
17
|
+
@reference = root.join('refs', Digest::SHA256.hexdigest(ref))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def installed
|
|
21
|
+
return unless @reference.exist?
|
|
22
|
+
|
|
23
|
+
directories.each { |directory| owned!(directory, directory: true) }
|
|
24
|
+
owned!(@reference)
|
|
25
|
+
commit = @reference.read.strip
|
|
26
|
+
raise Empeira::Error, 'Installation reference is incomplete' unless commit.match?(COMMIT)
|
|
27
|
+
|
|
28
|
+
path = revision(commit)
|
|
29
|
+
owned!(path, directory: true)
|
|
30
|
+
[commit, path]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def revision(commit)
|
|
34
|
+
root.join('revisions', commit)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def synchronize
|
|
38
|
+
prepare
|
|
39
|
+
File.open(root.join('lock'), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |file|
|
|
40
|
+
owned!(root.join('lock'))
|
|
41
|
+
file.flock(File::LOCK_EX)
|
|
42
|
+
yield
|
|
43
|
+
ensure
|
|
44
|
+
file.flock(File::LOCK_UN)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def publish(commit)
|
|
49
|
+
Tempfile.create('.reference-', @reference.dirname) do |file|
|
|
50
|
+
file.write("#{commit}\n")
|
|
51
|
+
file.flush
|
|
52
|
+
file.fsync
|
|
53
|
+
File.rename(file.path, @reference)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def stage
|
|
58
|
+
directory = Pathname(Dir.mktmpdir('.staging-', root))
|
|
59
|
+
yield directory
|
|
60
|
+
ensure
|
|
61
|
+
FileUtils.remove_entry(directory) if directory&.exist?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def mirror
|
|
65
|
+
root.join("#{source_key}.git")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def prepare
|
|
71
|
+
FileUtils.mkdir_p(@base.dirname, mode: 0o700)
|
|
72
|
+
directories.each do |path|
|
|
73
|
+
begin
|
|
74
|
+
Dir.mkdir(path, 0o700)
|
|
75
|
+
rescue Errno::EEXIST
|
|
76
|
+
# Stable private directories, following the existing agent cache convention.
|
|
77
|
+
end
|
|
78
|
+
owned!(path, directory: true)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def directories
|
|
83
|
+
[@base, root, root.join('refs'), root.join('revisions')]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def owned!(path, directory: false)
|
|
87
|
+
stat = path.lstat
|
|
88
|
+
valid = stat.uid == Process.uid && !stat.symlink? && stat.mode.nobits?(0o077)
|
|
89
|
+
valid &&= directory ? stat.directory? : stat.file? && stat.nlink == 1
|
|
90
|
+
raise Empeira::Error, 'Addon cache contains an unsafe, missing or foreign entry' unless valid
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Agent
|
|
5
|
+
# One source-resolution and acquisition path for both providers and both install methods.
|
|
6
|
+
class Acquisition
|
|
7
|
+
def initialize(context:, runtime:, progress: Progress.new)
|
|
8
|
+
@context = context
|
|
9
|
+
@runtime = runtime
|
|
10
|
+
@progress = progress
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def with_package(requirements)
|
|
14
|
+
prepare(requirements)
|
|
15
|
+
request = identity
|
|
16
|
+
acquire = ->(directory) { fetch(directory, request) }
|
|
17
|
+
cache.with_artifact(request, enabled: @context.configuration.dig('agent', 'cache', 'enabled'),
|
|
18
|
+
acquire: acquire, pin: package_pin) do |artifact|
|
|
19
|
+
@progress.heartbeat('Installing the resolved local agent package...')
|
|
20
|
+
yield artifact
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def preflight!(requirements)
|
|
25
|
+
return unless requirements.required?
|
|
26
|
+
|
|
27
|
+
@runtime.check_available!
|
|
28
|
+
return if @runtime.architecture == requirements.target.architecture
|
|
29
|
+
|
|
30
|
+
raise Error, 'Agent package helper architecture differs from the target; emulation is not enabled'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def cache
|
|
36
|
+
Cache.new(root: @context.locations.cache.join('agents'), temporary: @context.locations.temporary)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def prepare(requirements)
|
|
40
|
+
@requirements = requirements
|
|
41
|
+
@target = requirements.target
|
|
42
|
+
@source = requirements.repository
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def identity
|
|
46
|
+
{ 'package' => @requirements.agent.fetch('package'),
|
|
47
|
+
'requested_version' => @requirements.agent.fetch('version'),
|
|
48
|
+
'family' => @target.family, 'distribution' => @target.os, 'release' => @target.release,
|
|
49
|
+
'architecture' => @target.architecture, 'format' => @target.format, 'suffix' => @source['suffix'],
|
|
50
|
+
'source' => Cache.fingerprint(@source), 'method' => @requirements.agent.dig('install', 'method') }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def package_pin
|
|
54
|
+
return @source['sha256'] unless @source['url'].end_with?('.deb', '.rpm') &&
|
|
55
|
+
@requirements.agent.dig('install', 'method') == 'repository'
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def fetch(directory, request)
|
|
61
|
+
@authentication = Authentication.new(url: @source.fetch('url'), progress: @progress)
|
|
62
|
+
@download = Download.new(authentication: @authentication)
|
|
63
|
+
@progress.heartbeat('Resolving and acquiring the agent package...')
|
|
64
|
+
image = helper_image
|
|
65
|
+
@runtime.with_agent_helper(image: image.reference) do |resource|
|
|
66
|
+
resolver = native_source(resource, directory)
|
|
67
|
+
path, metadata, selected = acquire_package(resolver, directory, request)
|
|
68
|
+
describe(path, metadata, selected, resolver, request)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# rubocop:disable-next Metrics/AbcSize -- Keep native resolution, acquisition and identity verification together.
|
|
73
|
+
def acquire_package(resolver, directory, request)
|
|
74
|
+
direct = request.fetch('method') == 'package'
|
|
75
|
+
resolver.prepare_keys if direct
|
|
76
|
+
selected = direct ? { 'url' => @source.fetch('url'), 'sha256' => package_pin } : resolver.resolve
|
|
77
|
+
path = Pathname(directory).join("package.#{@target.format}")
|
|
78
|
+
@download.fetch(selected.fetch('url'), path, sha256: package_pin || selected['sha256'])
|
|
79
|
+
metadata = resolver.inspect_package(path)
|
|
80
|
+
if selected['version'] && metadata['version'] != selected['version']
|
|
81
|
+
raise Error, 'Agent package version differs from its native repository metadata'
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
[path, metadata, selected]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def describe(path, metadata, selected, resolver, request)
|
|
88
|
+
details = { 'schema' => 1, 'request' => request, 'url' => selected.fetch('url'),
|
|
89
|
+
'sha256' => Digest::SHA256.file(path).hexdigest, 'size' => path.size,
|
|
90
|
+
'public_keys' => resolver.public_keys,
|
|
91
|
+
'verify_signatures' => @source.fetch('verify_signatures', true) }
|
|
92
|
+
Artifact.new(path: path, metadata: metadata.merge(details))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def native_source(resource, directory)
|
|
96
|
+
klass = @target.family == 'debian' ? Node::AgentRepository : Node::DnfAgentRepository
|
|
97
|
+
execute = ->(arguments) { @runtime.service_exec(resource, arguments, timeout: 300) }
|
|
98
|
+
copy = lambda do |source, destination, mode|
|
|
99
|
+
@runtime.copy_to(resource, source, destination)
|
|
100
|
+
raise Error, 'Cannot set agent helper file permissions' unless execute.call(['chmod', mode,
|
|
101
|
+
destination]).success?
|
|
102
|
+
end
|
|
103
|
+
klass.new(source: @source, package: @requirements.agent.fetch('package'),
|
|
104
|
+
version: @requirements.agent.fetch('version'), target: @target, execute: execute, copy: copy,
|
|
105
|
+
download: @download, authentication: @authentication, directory: directory)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def helper_image
|
|
109
|
+
preflight!(@requirements)
|
|
110
|
+
request = Struct.new(:os, :version).new(@target.os, @target.release)
|
|
111
|
+
image = Node::Image.new(config: @context.configuration, request: request, architecture: @target.architecture)
|
|
112
|
+
@runtime.ensure_image(image.reference, recipe: image.recipe, files: image.build_files)
|
|
113
|
+
raise Error, 'Agent helper image architecture differs from the target' unless
|
|
114
|
+
@runtime.image_architecture(image.reference) == @target.architecture
|
|
115
|
+
|
|
116
|
+
image
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Agent
|
|
5
|
+
Artifact = Data.define(:path, :metadata) do
|
|
6
|
+
def valid_identity?(request)
|
|
7
|
+
metadata.is_a?(Hash) && metadata['schema'] == 1 && metadata['request'] == request &&
|
|
8
|
+
valid_version?(request) && valid_transport? && valid_keys? && valid_architecture?(request)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def valid_version?(request)
|
|
14
|
+
metadata['version'].is_a?(String) &&
|
|
15
|
+
Versions.match?(metadata['version'], request.fetch('requested_version'), suffix: request['suffix'])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def valid_transport?
|
|
19
|
+
uri = URI(metadata['url'])
|
|
20
|
+
Configuration::AgentSchema.https_endpoint?(uri) &&
|
|
21
|
+
[true, false].include?(metadata['verify_signatures'])
|
|
22
|
+
rescue URI::InvalidURIError, TypeError, ArgumentError
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def valid_keys?
|
|
27
|
+
keys = metadata['public_keys']
|
|
28
|
+
keys.is_a?(Array) && keys.all? { |key| key.is_a?(String) && !Base64.strict_decode64(key).empty? }
|
|
29
|
+
rescue ArgumentError
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def valid_architecture?(request)
|
|
34
|
+
target = Target.new(os: request.fetch('distribution'), release: request.fetch('release'),
|
|
35
|
+
architecture: request.fetch('architecture'))
|
|
36
|
+
generic = request.fetch('format') == 'deb' ? 'all' : 'noarch'
|
|
37
|
+
[target.native_architecture, generic].include?(metadata['architecture'])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'base64'
|
|
6
|
+
|
|
7
|
+
module Empeira
|
|
8
|
+
module Agent
|
|
9
|
+
class AuthenticationRequired < Error
|
|
10
|
+
attr_reader :url
|
|
11
|
+
|
|
12
|
+
def initialize(message = 'Agent repository requires authentication', url: nil)
|
|
13
|
+
super(message)
|
|
14
|
+
@url = url
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Authentication
|
|
19
|
+
def self.credentials
|
|
20
|
+
values = %w[EMPEIRA_AGENT_REPO_USERNAME EMPEIRA_AGENT_REPO_PASSWORD].map { |key| ENV.fetch(key, nil) }
|
|
21
|
+
return if values.all?(&:nil?)
|
|
22
|
+
return values if valid?(values)
|
|
23
|
+
|
|
24
|
+
raise ConfigurationError, 'Set both nonempty EMPEIRA_AGENT_REPO_USERNAME and ' \
|
|
25
|
+
'EMPEIRA_AGENT_REPO_PASSWORD for agent repository authentication'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.valid?(values)
|
|
29
|
+
values.all? { |value| value.is_a?(String) && !value.empty? && !value.match?(/[\x00-\x1f\x7f]/) } &&
|
|
30
|
+
!values.first.include?(':')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(url:, progress: Progress.new, input: $stdin, output: $stderr)
|
|
34
|
+
@origin = URI(url)
|
|
35
|
+
@credentials = self.class.credentials
|
|
36
|
+
@credential_source = @credentials ? 'ENV' : 'interactive'
|
|
37
|
+
@progress = progress
|
|
38
|
+
@input = input
|
|
39
|
+
@output = output
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def credentials(url = @origin.to_s)
|
|
43
|
+
uri = URI(url)
|
|
44
|
+
@credentials if same_origin?(uri)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def attempt
|
|
48
|
+
yield
|
|
49
|
+
rescue AuthenticationRequired => e
|
|
50
|
+
require_retry!(e)
|
|
51
|
+
login_after(e)
|
|
52
|
+
begin
|
|
53
|
+
yield
|
|
54
|
+
rescue AuthenticationRequired => retry_error
|
|
55
|
+
raise Error, failure_message(retry_error), cause: nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def redact(value)
|
|
60
|
+
secrets = [*@credentials, (Base64.strict_encode64(@credentials.join(':')) if @credentials)].compact
|
|
61
|
+
sorted = secrets.sort_by { |secret| -secret.length }
|
|
62
|
+
sorted.reduce(value.to_s) { |text, secret| text.gsub(secret, '[REDACTED]') }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def require_retry!(error)
|
|
68
|
+
if error.url && !same_origin?(URI(error.url))
|
|
69
|
+
raise Error, 'Agent authentication required at a different origin; source credentials were not forwarded.' \
|
|
70
|
+
"\n#{error.message}",
|
|
71
|
+
cause: nil
|
|
72
|
+
end
|
|
73
|
+
raise Error, failure_message(error), cause: nil if @credentials
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def login_after(error)
|
|
77
|
+
login
|
|
78
|
+
rescue Error => e
|
|
79
|
+
raise Error, "#{e.message}\n#{error.message}", cause: nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def same_origin?(uri)
|
|
83
|
+
[uri.scheme, uri.host&.downcase, uri.port] == [@origin.scheme, @origin.host&.downcase, @origin.port]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def failure_message(error)
|
|
87
|
+
source = @credential_source == 'ENV' ? 'ENV credentials were not replaced' : 'interactive login was rejected'
|
|
88
|
+
"Agent repository authentication failed: #{source}.\n#{error.message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def login
|
|
92
|
+
unless @input.tty? && @output.tty?
|
|
93
|
+
raise Error, 'Agent repository requires authentication; set EMPEIRA_AGENT_REPO_USERNAME and ' \
|
|
94
|
+
'EMPEIRA_AGENT_REPO_PASSWORD in noninteractive environments'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@progress.streaming { read_credentials }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def read_credentials
|
|
101
|
+
@output.puts("Agent repository requires authentication: #{@origin.host}")
|
|
102
|
+
@output.print('Log in? [y/N] ')
|
|
103
|
+
@output.flush
|
|
104
|
+
raise Error, 'Agent repository login cancelled' unless @input.gets&.strip&.match?(/\Ay(?:es)?\z/i)
|
|
105
|
+
|
|
106
|
+
username = read_value('Username: ')
|
|
107
|
+
password = read_value('Password: ', secret: true)
|
|
108
|
+
@output.puts
|
|
109
|
+
values = [username, password]
|
|
110
|
+
raise Error, 'Agent repository login cancelled or credentials are invalid' unless self.class.valid?(values)
|
|
111
|
+
|
|
112
|
+
@credentials = values
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def read_value(prompt, secret: false)
|
|
116
|
+
@output.print(prompt)
|
|
117
|
+
@output.flush
|
|
118
|
+
(secret ? @input.noecho(&:gets) : @input.gets)&.chomp
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|