dash 2.12.0
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/MIT-LICENSE +20 -0
- data/README.md +13 -0
- data/bin/dash +18 -0
- data/bin/kamal +18 -0
- data/lib/kamal/cli/accessory.rb +342 -0
- data/lib/kamal/cli/alias/command.rb +10 -0
- data/lib/kamal/cli/app/assets.rb +24 -0
- data/lib/kamal/cli/app/boot.rb +126 -0
- data/lib/kamal/cli/app/error_pages.rb +33 -0
- data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
- data/lib/kamal/cli/app.rb +368 -0
- data/lib/kamal/cli/base.rb +324 -0
- data/lib/kamal/cli/build/clone.rb +59 -0
- data/lib/kamal/cli/build/port_forwarding.rb +66 -0
- data/lib/kamal/cli/build.rb +242 -0
- data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
- data/lib/kamal/cli/healthcheck/error.rb +2 -0
- data/lib/kamal/cli/healthcheck/poller.rb +42 -0
- data/lib/kamal/cli/lock.rb +34 -0
- data/lib/kamal/cli/main.rb +299 -0
- data/lib/kamal/cli/proxy.rb +419 -0
- data/lib/kamal/cli/prune.rb +34 -0
- data/lib/kamal/cli/registry.rb +49 -0
- data/lib/kamal/cli/secrets.rb +50 -0
- data/lib/kamal/cli/server.rb +70 -0
- data/lib/kamal/cli/templates/deploy.yml +102 -0
- data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
- data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/secrets +22 -0
- data/lib/kamal/cli.rb +9 -0
- data/lib/kamal/commander/specifics.rb +62 -0
- data/lib/kamal/commander.rb +230 -0
- data/lib/kamal/commands/accessory/proxy.rb +16 -0
- data/lib/kamal/commands/accessory.rb +118 -0
- data/lib/kamal/commands/app/assets.rb +51 -0
- data/lib/kamal/commands/app/containers.rb +31 -0
- data/lib/kamal/commands/app/error_pages.rb +9 -0
- data/lib/kamal/commands/app/execution.rb +38 -0
- data/lib/kamal/commands/app/images.rb +13 -0
- data/lib/kamal/commands/app/logging.rb +28 -0
- data/lib/kamal/commands/app/proxy.rb +32 -0
- data/lib/kamal/commands/app.rb +125 -0
- data/lib/kamal/commands/auditor.rb +39 -0
- data/lib/kamal/commands/base.rb +147 -0
- data/lib/kamal/commands/builder/base.rb +143 -0
- data/lib/kamal/commands/builder/clone.rb +32 -0
- data/lib/kamal/commands/builder/cloud.rb +22 -0
- data/lib/kamal/commands/builder/hybrid.rb +21 -0
- data/lib/kamal/commands/builder/local.rb +20 -0
- data/lib/kamal/commands/builder/pack.rb +46 -0
- data/lib/kamal/commands/builder/remote.rb +75 -0
- data/lib/kamal/commands/builder.rb +54 -0
- data/lib/kamal/commands/docker.rb +50 -0
- data/lib/kamal/commands/hook.rb +20 -0
- data/lib/kamal/commands/loadbalancer.rb +130 -0
- data/lib/kamal/commands/lock.rb +70 -0
- data/lib/kamal/commands/proxy.rb +150 -0
- data/lib/kamal/commands/prune.rb +38 -0
- data/lib/kamal/commands/registry.rb +38 -0
- data/lib/kamal/commands/server.rb +15 -0
- data/lib/kamal/commands.rb +2 -0
- data/lib/kamal/configuration/accessory.rb +280 -0
- data/lib/kamal/configuration/alias.rb +15 -0
- data/lib/kamal/configuration/boot.rb +29 -0
- data/lib/kamal/configuration/builder.rb +218 -0
- data/lib/kamal/configuration/docs/accessory.yml +160 -0
- data/lib/kamal/configuration/docs/alias.yml +29 -0
- data/lib/kamal/configuration/docs/boot.yml +21 -0
- data/lib/kamal/configuration/docs/builder.yml +132 -0
- data/lib/kamal/configuration/docs/configuration.yml +228 -0
- data/lib/kamal/configuration/docs/env.yml +118 -0
- data/lib/kamal/configuration/docs/logging.yml +21 -0
- data/lib/kamal/configuration/docs/output.yml +25 -0
- data/lib/kamal/configuration/docs/proxy.yml +207 -0
- data/lib/kamal/configuration/docs/registry.yml +64 -0
- data/lib/kamal/configuration/docs/role.yml +54 -0
- data/lib/kamal/configuration/docs/servers.yml +27 -0
- data/lib/kamal/configuration/docs/ssh.yml +81 -0
- data/lib/kamal/configuration/docs/sshkit.yml +31 -0
- data/lib/kamal/configuration/env/tag.rb +13 -0
- data/lib/kamal/configuration/env.rb +42 -0
- data/lib/kamal/configuration/loadbalancer.rb +34 -0
- data/lib/kamal/configuration/logging.rb +33 -0
- data/lib/kamal/configuration/output.rb +34 -0
- data/lib/kamal/configuration/proxy/boot.rb +124 -0
- data/lib/kamal/configuration/proxy/run.rb +152 -0
- data/lib/kamal/configuration/proxy.rb +156 -0
- data/lib/kamal/configuration/registry.rb +40 -0
- data/lib/kamal/configuration/role.rb +247 -0
- data/lib/kamal/configuration/servers.rb +25 -0
- data/lib/kamal/configuration/ssh.rb +76 -0
- data/lib/kamal/configuration/sshkit.rb +26 -0
- data/lib/kamal/configuration/validation.rb +27 -0
- data/lib/kamal/configuration/validator/accessory.rb +13 -0
- data/lib/kamal/configuration/validator/alias.rb +15 -0
- data/lib/kamal/configuration/validator/builder.rb +15 -0
- data/lib/kamal/configuration/validator/configuration.rb +6 -0
- data/lib/kamal/configuration/validator/env.rb +54 -0
- data/lib/kamal/configuration/validator/proxy.rb +47 -0
- data/lib/kamal/configuration/validator/registry.rb +27 -0
- data/lib/kamal/configuration/validator/role.rb +13 -0
- data/lib/kamal/configuration/validator/servers.rb +7 -0
- data/lib/kamal/configuration/validator.rb +251 -0
- data/lib/kamal/configuration/volume.rb +29 -0
- data/lib/kamal/configuration.rb +465 -0
- data/lib/kamal/docker.rb +30 -0
- data/lib/kamal/env_file.rb +44 -0
- data/lib/kamal/git.rb +37 -0
- data/lib/kamal/otel_shipper.rb +176 -0
- data/lib/kamal/output/base_logger.rb +29 -0
- data/lib/kamal/output/file_logger.rb +51 -0
- data/lib/kamal/output/formatter.rb +36 -0
- data/lib/kamal/output/otel_logger.rb +70 -0
- data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +59 -0
- data/lib/kamal/secrets/adapters/base.rb +33 -0
- data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
- data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
- data/lib/kamal/secrets/adapters/doppler.rb +57 -0
- data/lib/kamal/secrets/adapters/enpass.rb +71 -0
- data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
- data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
- data/lib/kamal/secrets/adapters/one_password.rb +104 -0
- data/lib/kamal/secrets/adapters/passbolt.rb +129 -0
- data/lib/kamal/secrets/adapters/test.rb +16 -0
- data/lib/kamal/secrets/adapters.rb +16 -0
- data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +47 -0
- data/lib/kamal/secrets.rb +53 -0
- data/lib/kamal/sshkit_with_ext.rb +273 -0
- data/lib/kamal/tags.rb +40 -0
- data/lib/kamal/utils/sensitive.rb +20 -0
- data/lib/kamal/utils.rb +110 -0
- data/lib/kamal/version.rb +3 -0
- data/lib/kamal.rb +15 -0
- metadata +388 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
require "sshkit"
|
|
2
|
+
require "sshkit/dsl"
|
|
3
|
+
require "net/scp"
|
|
4
|
+
require "active_support/core_ext/hash/deep_merge"
|
|
5
|
+
require "json"
|
|
6
|
+
require "resolv"
|
|
7
|
+
require "concurrent/atomic/semaphore"
|
|
8
|
+
|
|
9
|
+
class SSHKit::Backend::Abstract
|
|
10
|
+
def capture_with_info(*args, **kwargs)
|
|
11
|
+
capture(*args, **kwargs, verbosity: Logger::INFO)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def capture_with_debug(*args, **kwargs)
|
|
15
|
+
capture(*args, **kwargs, verbosity: Logger::DEBUG)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def capture_with_pretty_json(*args, **kwargs)
|
|
19
|
+
JSON.pretty_generate(JSON.parse(capture(*args, **kwargs)))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def puts_by_host(host, output, type: "App", quiet: false, raw: false)
|
|
23
|
+
if raw
|
|
24
|
+
$stdout.binmode
|
|
25
|
+
$stdout.write(output)
|
|
26
|
+
else
|
|
27
|
+
unless quiet
|
|
28
|
+
puts "#{type} Host: #{host}"
|
|
29
|
+
end
|
|
30
|
+
puts "#{output}\n\n"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Our execution pattern is for the CLI execute args lists returned
|
|
35
|
+
# from commands, but this doesn't support returning execution options
|
|
36
|
+
# from the command.
|
|
37
|
+
#
|
|
38
|
+
# Support this by using kwargs for CLI options and merging with the
|
|
39
|
+
# args-extracted options.
|
|
40
|
+
module CommandEnvMerge
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Override to merge options returned by commands in the args list with
|
|
44
|
+
# options passed by the CLI and pass them along as kwargs.
|
|
45
|
+
def command(args, options)
|
|
46
|
+
more_options, args = args.partition { |a| a.is_a? Hash }
|
|
47
|
+
more_options << options
|
|
48
|
+
|
|
49
|
+
build_command(args, **more_options.reduce(:deep_merge))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Destructure options to pluck out env for merge
|
|
53
|
+
def build_command(args, env: nil, **options)
|
|
54
|
+
# Rely on native Ruby kwargs precedence rather than explicit Hash merges
|
|
55
|
+
SSHKit::Command.new(*args, **default_command_options, **options, env: env_for(env))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def default_command_options
|
|
59
|
+
{ in: pwd_path, host: @host, user: @user, group: @group }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def env_for(env)
|
|
63
|
+
@env.to_h.merge(env.to_h)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
prepend CommandEnvMerge
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class SSHKit::Backend::Netssh::Configuration
|
|
70
|
+
attr_accessor :max_concurrent_starts, :dns_retries
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class SSHKit::Backend::Netssh
|
|
74
|
+
module DnsRetriable
|
|
75
|
+
DNS_RETRY_BASE = 0.1
|
|
76
|
+
DNS_RETRY_MAX = 2.0
|
|
77
|
+
DNS_RETRY_JITTER = 0.1
|
|
78
|
+
DNS_ERROR_MESSAGE = /getaddrinfo|Temporary failure in name resolution|Name or service not known|nodename nor servname provided|No address associated|failed to look up|resolve/i
|
|
79
|
+
|
|
80
|
+
def with_dns_retry(hostname, retries: config.dns_retries, base: DNS_RETRY_BASE, max_sleep: DNS_RETRY_MAX, jitter: DNS_RETRY_JITTER)
|
|
81
|
+
attempts = 0
|
|
82
|
+
begin
|
|
83
|
+
attempts += 1
|
|
84
|
+
yield
|
|
85
|
+
rescue => error
|
|
86
|
+
raise unless retryable_dns_error?(error) && attempts <= retries
|
|
87
|
+
|
|
88
|
+
delay = dns_retry_sleep(attempts, base: base, jitter: jitter, max_sleep: max_sleep)
|
|
89
|
+
SSHKit.config.output.warn("Retrying DNS for #{hostname} (attempt #{attempts}/#{retries}) in #{format("%0.2f", delay)}s: #{error.message}")
|
|
90
|
+
sleep delay
|
|
91
|
+
retry
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
def retryable_dns_error?(error)
|
|
97
|
+
case error
|
|
98
|
+
when Resolv::ResolvError, Resolv::ResolvTimeout
|
|
99
|
+
true
|
|
100
|
+
when SocketError
|
|
101
|
+
error.message =~ DNS_ERROR_MESSAGE
|
|
102
|
+
else
|
|
103
|
+
error.cause && retryable_dns_error?(error.cause)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def dns_retry_sleep(attempt, base:, jitter:, max_sleep:)
|
|
108
|
+
sleep_for = [ base * (2 ** (attempt - 1)), max_sleep ].min
|
|
109
|
+
sleep_for += Kernel.rand * jitter
|
|
110
|
+
sleep_for
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
module LimitConcurrentStartsClass
|
|
115
|
+
attr_reader :start_semaphore
|
|
116
|
+
|
|
117
|
+
def configure(&block)
|
|
118
|
+
super &block
|
|
119
|
+
# Create this here to avoid lazy creation by multiple threads
|
|
120
|
+
if config.max_concurrent_starts
|
|
121
|
+
@start_semaphore = Concurrent::Semaphore.new(config.max_concurrent_starts)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
class << self
|
|
127
|
+
prepend LimitConcurrentStartsClass
|
|
128
|
+
prepend DnsRetriable
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
module ConnectSsh
|
|
132
|
+
private
|
|
133
|
+
def connect_ssh(...)
|
|
134
|
+
Net::SSH.start(...)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
include ConnectSsh
|
|
138
|
+
|
|
139
|
+
module DnsRetriableConnection
|
|
140
|
+
private
|
|
141
|
+
def connect_ssh(...)
|
|
142
|
+
self.class.with_dns_retry(host.hostname) { super }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
prepend DnsRetriableConnection
|
|
146
|
+
|
|
147
|
+
module LimitConcurrentStartsInstance
|
|
148
|
+
private
|
|
149
|
+
def with_ssh(&block)
|
|
150
|
+
host.ssh_options = self.class.config.ssh_options.merge(host.ssh_options || {})
|
|
151
|
+
self.class.pool.with(
|
|
152
|
+
method(:connect_ssh),
|
|
153
|
+
String(host.hostname),
|
|
154
|
+
host.username,
|
|
155
|
+
host.netssh_options,
|
|
156
|
+
&block
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def connect_ssh(...)
|
|
161
|
+
with_concurrency_limit { super }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def with_concurrency_limit(&block)
|
|
165
|
+
if self.class.start_semaphore
|
|
166
|
+
self.class.start_semaphore.acquire(&block)
|
|
167
|
+
else
|
|
168
|
+
yield
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
prepend LimitConcurrentStartsInstance
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class SSHKit::Runner::Parallel
|
|
176
|
+
# SSHKit joins the threads in sequence and fails on the first error it encounters, which means that we wait threads
|
|
177
|
+
# before the first failure to complete but not for ones after.
|
|
178
|
+
#
|
|
179
|
+
# We'll patch it to wait for them all to complete, and to record all the threads that errored so we can see when a
|
|
180
|
+
# problem occurs on multiple hosts.
|
|
181
|
+
module CompleteAll
|
|
182
|
+
def execute
|
|
183
|
+
threads = hosts.map do |host|
|
|
184
|
+
Thread.new(host) do |h|
|
|
185
|
+
Thread.current.report_on_exception = false
|
|
186
|
+
backend(h, &block).run
|
|
187
|
+
rescue ::StandardError => e
|
|
188
|
+
e2 = SSHKit::Runner::ExecuteError.new e
|
|
189
|
+
raise e2, "Exception while executing #{host.user ? "as #{host.user}@" : "on host "}#{host}: #{e.message}"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
exceptions = []
|
|
194
|
+
threads.each do |t|
|
|
195
|
+
begin
|
|
196
|
+
t.join
|
|
197
|
+
rescue SSHKit::Runner::ExecuteError => e
|
|
198
|
+
exceptions << e
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
if exceptions.one?
|
|
202
|
+
raise exceptions.first
|
|
203
|
+
elsif exceptions.many?
|
|
204
|
+
raise exceptions.first, [ "Exceptions on #{exceptions.count} hosts:", exceptions.map(&:message) ].join("\n")
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
prepend CompleteAll
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Avoid net-ssh debug, until https://github.com/net-ssh/net-ssh/pull/953 is merged
|
|
213
|
+
module NetSshForwardingNoPuts
|
|
214
|
+
def puts(*)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
Net::SSH::Service::Forward.prepend NetSshForwardingNoPuts
|
|
219
|
+
|
|
220
|
+
module SSHKitDslRoles
|
|
221
|
+
# Execute on hosts grouped by role.
|
|
222
|
+
#
|
|
223
|
+
# Unlike `on()` which deduplicates hosts, this allows the same host to have
|
|
224
|
+
# multiple concurrent connections when it appears in multiple roles.
|
|
225
|
+
#
|
|
226
|
+
# Options:
|
|
227
|
+
# hosts: The hosts to run on (required)
|
|
228
|
+
# parallel: When true, each role runs in its own thread with separate
|
|
229
|
+
# connections. When false, hosts run in parallel but roles on each
|
|
230
|
+
# host run sequentially (default: true)
|
|
231
|
+
#
|
|
232
|
+
# Example:
|
|
233
|
+
# on_roles(roles) do |host, role|
|
|
234
|
+
# # deploy role to host
|
|
235
|
+
# end
|
|
236
|
+
def on_roles(roles, hosts:, parallel: true, &block)
|
|
237
|
+
if parallel
|
|
238
|
+
threads = roles.filter_map do |role|
|
|
239
|
+
if (role_hosts = role.hosts & hosts).any?
|
|
240
|
+
Thread.new do
|
|
241
|
+
on(role_hosts) { |host| instance_exec(host, role, &block) }
|
|
242
|
+
rescue StandardError => e
|
|
243
|
+
raise SSHKit::Runner::ExecuteError.new(e), "Exception while executing on #{role}: #{e.message}"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
exceptions = []
|
|
249
|
+
threads.each do |t|
|
|
250
|
+
begin
|
|
251
|
+
t.join
|
|
252
|
+
rescue SSHKit::Runner::ExecuteError => e
|
|
253
|
+
exceptions << e
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if exceptions.one?
|
|
258
|
+
raise exceptions.first
|
|
259
|
+
elsif exceptions.many?
|
|
260
|
+
raise exceptions.first, [ "Exceptions on #{exceptions.count} roles:", exceptions.map(&:message) ].join("\n")
|
|
261
|
+
end
|
|
262
|
+
else
|
|
263
|
+
# Host-first iteration: hosts run in parallel, roles on each host run sequentially
|
|
264
|
+
on(hosts) do |host|
|
|
265
|
+
roles.each do |role|
|
|
266
|
+
instance_exec(host, role, &block) if role.hosts.include?(host.to_s)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
SSHKit::DSL.prepend SSHKitDslRoles
|
data/lib/kamal/tags.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
class Kamal::Tags
|
|
4
|
+
attr_reader :config, :tags
|
|
5
|
+
|
|
6
|
+
class << self
|
|
7
|
+
def from_config(config, **extra)
|
|
8
|
+
new(**default_tags(config), **extra)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def default_tags(config)
|
|
12
|
+
{ recorded_at: Time.now.utc.iso8601,
|
|
13
|
+
performer: Kamal::Git.email.presence || `whoami`.chomp,
|
|
14
|
+
destination: config.destination,
|
|
15
|
+
version: config.version,
|
|
16
|
+
service_version: service_version(config),
|
|
17
|
+
service: config.service }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def service_version(config)
|
|
21
|
+
[ config.service, config.abbreviated_version ].compact.join("@")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(**tags)
|
|
26
|
+
@tags = tags.compact
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def env
|
|
30
|
+
tags.transform_keys { |detail| "KAMAL_#{detail.upcase}" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
tags.values.map { |value| "[#{value}]" }.join(" ")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def except(*tags)
|
|
38
|
+
self.class.new(**self.tags.except(*tags))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "active_support/core_ext/module/delegation"
|
|
2
|
+
require "sshkit"
|
|
3
|
+
|
|
4
|
+
class Kamal::Utils::Sensitive
|
|
5
|
+
# So SSHKit knows to redact these values.
|
|
6
|
+
include SSHKit::Redaction
|
|
7
|
+
|
|
8
|
+
attr_reader :unredacted, :redaction
|
|
9
|
+
delegate :to_s, to: :unredacted
|
|
10
|
+
delegate :inspect, to: :redaction
|
|
11
|
+
|
|
12
|
+
def initialize(value, redaction: "[REDACTED]")
|
|
13
|
+
@unredacted, @redaction = value, redaction
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Sensitive values won't leak into YAML output.
|
|
17
|
+
def encode_with(coder)
|
|
18
|
+
coder.represent_scalar nil, redaction
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/kamal/utils.rb
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require "active_support/core_ext/object/try"
|
|
2
|
+
|
|
3
|
+
module Kamal::Utils
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX = /\$(?!{[^\}]*\})/
|
|
7
|
+
|
|
8
|
+
# Return a list of escaped shell arguments using the same named argument against the passed attributes (hash or array).
|
|
9
|
+
def argumentize(argument, attributes, sensitive: false)
|
|
10
|
+
Array(attributes).flat_map do |key, value|
|
|
11
|
+
if value.present?
|
|
12
|
+
attr = "#{key}=#{escape_shell_value(value)}"
|
|
13
|
+
attr = self.sensitive(attr, redaction: "#{key}=[REDACTED]") if sensitive
|
|
14
|
+
[ argument, attr ]
|
|
15
|
+
elsif value == false
|
|
16
|
+
[ argument, "#{key}=false" ]
|
|
17
|
+
else
|
|
18
|
+
[ argument, key ]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns a list of shell-dashed option arguments. If the value is true, it's treated like a value-less option.
|
|
24
|
+
def optionize(args, with: nil, escape: true)
|
|
25
|
+
options = if with
|
|
26
|
+
flatten_args(args).collect { |(key, value)| value == true ? "--#{key}" : "--#{key}#{with}#{escape ? escape_shell_value(value) : value}" }
|
|
27
|
+
else
|
|
28
|
+
flatten_args(args).collect { |(key, value)| [ "--#{key}", value == true ? nil : escape ? escape_shell_value(value) : value ] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
options.flatten.compact
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Flattens a one-to-many structure into an array of two-element arrays each containing a key-value pair
|
|
35
|
+
def flatten_args(args)
|
|
36
|
+
args.flat_map { |key, value| value.try(:map) { |entry| [ key, entry ] } || [ [ key, value ] ] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Marks sensitive values for redaction in logs and human-visible output.
|
|
40
|
+
# Pass `redaction:` to change the default `"[REDACTED]"` redaction, e.g.
|
|
41
|
+
# `sensitive "#{arg}=#{secret}", redaction: "#{arg}=xxxx"
|
|
42
|
+
def sensitive(...)
|
|
43
|
+
Kamal::Utils::Sensitive.new(...)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def redacted(value)
|
|
47
|
+
case
|
|
48
|
+
when value.respond_to?(:redaction)
|
|
49
|
+
value.redaction
|
|
50
|
+
when value.respond_to?(:transform_values)
|
|
51
|
+
value.transform_values { |value| redacted value }
|
|
52
|
+
when value.respond_to?(:map)
|
|
53
|
+
value.map { |element| redacted element }
|
|
54
|
+
else
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Escape a value to make it safe for shell use.
|
|
60
|
+
def escape_shell_value(value)
|
|
61
|
+
value.to_s.scan(/[\x00-\x7F]+|[^\x00-\x7F]+/) \
|
|
62
|
+
.map { |part| part.ascii_only? ? escape_ascii_shell_value(part) : part }
|
|
63
|
+
.join
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def escape_ascii_shell_value(value)
|
|
67
|
+
value.to_s.dump
|
|
68
|
+
.gsub(/`/, '\\\\`')
|
|
69
|
+
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Apply a list of host or role filters, including wildcard matches
|
|
73
|
+
def filter_specific_items(filters, items)
|
|
74
|
+
matches = []
|
|
75
|
+
|
|
76
|
+
Array(filters).select do |filter|
|
|
77
|
+
matches += Array(items).select do |item|
|
|
78
|
+
# Only allow * for a wildcard
|
|
79
|
+
# items are roles or hosts
|
|
80
|
+
File.fnmatch(filter, item.to_s, File::FNM_EXTGLOB)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
matches.uniq
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def stable_sort!(elements, &block)
|
|
88
|
+
elements.sort_by!.with_index { |element, index| [ block.call(element), index ] }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def join_commands(commands)
|
|
92
|
+
commands.map(&:strip).join(" ")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def docker_arch
|
|
96
|
+
arch = `docker info --format '{{.Architecture}}'`.strip
|
|
97
|
+
case arch
|
|
98
|
+
when /aarch64/
|
|
99
|
+
"arm64"
|
|
100
|
+
when /x86_64/
|
|
101
|
+
"amd64"
|
|
102
|
+
else
|
|
103
|
+
arch
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def older_version?(version, other_version)
|
|
108
|
+
Gem::Version.new(version.delete_prefix("v")) < Gem::Version.new(other_version.delete_prefix("v"))
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/kamal.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Kamal
|
|
2
|
+
class ConfigurationError < StandardError; end
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "zeitwerk"
|
|
7
|
+
require "yaml"
|
|
8
|
+
require "tmpdir"
|
|
9
|
+
require "pathname"
|
|
10
|
+
require "uri"
|
|
11
|
+
|
|
12
|
+
loader = Zeitwerk::Loader.for_gem
|
|
13
|
+
loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
|
|
14
|
+
loader.setup
|
|
15
|
+
loader.eager_load_namespace(Kamal::Cli) # We need all commands loaded.
|