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.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +13 -0
  4. data/bin/dash +18 -0
  5. data/bin/kamal +18 -0
  6. data/lib/kamal/cli/accessory.rb +342 -0
  7. data/lib/kamal/cli/alias/command.rb +10 -0
  8. data/lib/kamal/cli/app/assets.rb +24 -0
  9. data/lib/kamal/cli/app/boot.rb +126 -0
  10. data/lib/kamal/cli/app/error_pages.rb +33 -0
  11. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  12. data/lib/kamal/cli/app.rb +368 -0
  13. data/lib/kamal/cli/base.rb +324 -0
  14. data/lib/kamal/cli/build/clone.rb +59 -0
  15. data/lib/kamal/cli/build/port_forwarding.rb +66 -0
  16. data/lib/kamal/cli/build.rb +242 -0
  17. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  18. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  19. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  20. data/lib/kamal/cli/lock.rb +34 -0
  21. data/lib/kamal/cli/main.rb +299 -0
  22. data/lib/kamal/cli/proxy.rb +419 -0
  23. data/lib/kamal/cli/prune.rb +34 -0
  24. data/lib/kamal/cli/registry.rb +49 -0
  25. data/lib/kamal/cli/secrets.rb +50 -0
  26. data/lib/kamal/cli/server.rb +70 -0
  27. data/lib/kamal/cli/templates/deploy.yml +102 -0
  28. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  29. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  30. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  31. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  32. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  33. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  34. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  35. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
  36. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  37. data/lib/kamal/cli/templates/secrets +22 -0
  38. data/lib/kamal/cli.rb +9 -0
  39. data/lib/kamal/commander/specifics.rb +62 -0
  40. data/lib/kamal/commander.rb +230 -0
  41. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  42. data/lib/kamal/commands/accessory.rb +118 -0
  43. data/lib/kamal/commands/app/assets.rb +51 -0
  44. data/lib/kamal/commands/app/containers.rb +31 -0
  45. data/lib/kamal/commands/app/error_pages.rb +9 -0
  46. data/lib/kamal/commands/app/execution.rb +38 -0
  47. data/lib/kamal/commands/app/images.rb +13 -0
  48. data/lib/kamal/commands/app/logging.rb +28 -0
  49. data/lib/kamal/commands/app/proxy.rb +32 -0
  50. data/lib/kamal/commands/app.rb +125 -0
  51. data/lib/kamal/commands/auditor.rb +39 -0
  52. data/lib/kamal/commands/base.rb +147 -0
  53. data/lib/kamal/commands/builder/base.rb +143 -0
  54. data/lib/kamal/commands/builder/clone.rb +32 -0
  55. data/lib/kamal/commands/builder/cloud.rb +22 -0
  56. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  57. data/lib/kamal/commands/builder/local.rb +20 -0
  58. data/lib/kamal/commands/builder/pack.rb +46 -0
  59. data/lib/kamal/commands/builder/remote.rb +75 -0
  60. data/lib/kamal/commands/builder.rb +54 -0
  61. data/lib/kamal/commands/docker.rb +50 -0
  62. data/lib/kamal/commands/hook.rb +20 -0
  63. data/lib/kamal/commands/loadbalancer.rb +130 -0
  64. data/lib/kamal/commands/lock.rb +70 -0
  65. data/lib/kamal/commands/proxy.rb +150 -0
  66. data/lib/kamal/commands/prune.rb +38 -0
  67. data/lib/kamal/commands/registry.rb +38 -0
  68. data/lib/kamal/commands/server.rb +15 -0
  69. data/lib/kamal/commands.rb +2 -0
  70. data/lib/kamal/configuration/accessory.rb +280 -0
  71. data/lib/kamal/configuration/alias.rb +15 -0
  72. data/lib/kamal/configuration/boot.rb +29 -0
  73. data/lib/kamal/configuration/builder.rb +218 -0
  74. data/lib/kamal/configuration/docs/accessory.yml +160 -0
  75. data/lib/kamal/configuration/docs/alias.yml +29 -0
  76. data/lib/kamal/configuration/docs/boot.yml +21 -0
  77. data/lib/kamal/configuration/docs/builder.yml +132 -0
  78. data/lib/kamal/configuration/docs/configuration.yml +228 -0
  79. data/lib/kamal/configuration/docs/env.yml +118 -0
  80. data/lib/kamal/configuration/docs/logging.yml +21 -0
  81. data/lib/kamal/configuration/docs/output.yml +25 -0
  82. data/lib/kamal/configuration/docs/proxy.yml +207 -0
  83. data/lib/kamal/configuration/docs/registry.yml +64 -0
  84. data/lib/kamal/configuration/docs/role.yml +54 -0
  85. data/lib/kamal/configuration/docs/servers.yml +27 -0
  86. data/lib/kamal/configuration/docs/ssh.yml +81 -0
  87. data/lib/kamal/configuration/docs/sshkit.yml +31 -0
  88. data/lib/kamal/configuration/env/tag.rb +13 -0
  89. data/lib/kamal/configuration/env.rb +42 -0
  90. data/lib/kamal/configuration/loadbalancer.rb +34 -0
  91. data/lib/kamal/configuration/logging.rb +33 -0
  92. data/lib/kamal/configuration/output.rb +34 -0
  93. data/lib/kamal/configuration/proxy/boot.rb +124 -0
  94. data/lib/kamal/configuration/proxy/run.rb +152 -0
  95. data/lib/kamal/configuration/proxy.rb +156 -0
  96. data/lib/kamal/configuration/registry.rb +40 -0
  97. data/lib/kamal/configuration/role.rb +247 -0
  98. data/lib/kamal/configuration/servers.rb +25 -0
  99. data/lib/kamal/configuration/ssh.rb +76 -0
  100. data/lib/kamal/configuration/sshkit.rb +26 -0
  101. data/lib/kamal/configuration/validation.rb +27 -0
  102. data/lib/kamal/configuration/validator/accessory.rb +13 -0
  103. data/lib/kamal/configuration/validator/alias.rb +15 -0
  104. data/lib/kamal/configuration/validator/builder.rb +15 -0
  105. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  106. data/lib/kamal/configuration/validator/env.rb +54 -0
  107. data/lib/kamal/configuration/validator/proxy.rb +47 -0
  108. data/lib/kamal/configuration/validator/registry.rb +27 -0
  109. data/lib/kamal/configuration/validator/role.rb +13 -0
  110. data/lib/kamal/configuration/validator/servers.rb +7 -0
  111. data/lib/kamal/configuration/validator.rb +251 -0
  112. data/lib/kamal/configuration/volume.rb +29 -0
  113. data/lib/kamal/configuration.rb +465 -0
  114. data/lib/kamal/docker.rb +30 -0
  115. data/lib/kamal/env_file.rb +44 -0
  116. data/lib/kamal/git.rb +37 -0
  117. data/lib/kamal/otel_shipper.rb +176 -0
  118. data/lib/kamal/output/base_logger.rb +29 -0
  119. data/lib/kamal/output/file_logger.rb +51 -0
  120. data/lib/kamal/output/formatter.rb +36 -0
  121. data/lib/kamal/output/otel_logger.rb +70 -0
  122. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +59 -0
  123. data/lib/kamal/secrets/adapters/base.rb +33 -0
  124. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  125. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  126. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  127. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  128. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  129. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  130. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  131. data/lib/kamal/secrets/adapters/passbolt.rb +129 -0
  132. data/lib/kamal/secrets/adapters/test.rb +16 -0
  133. data/lib/kamal/secrets/adapters.rb +16 -0
  134. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +47 -0
  135. data/lib/kamal/secrets.rb +53 -0
  136. data/lib/kamal/sshkit_with_ext.rb +273 -0
  137. data/lib/kamal/tags.rb +40 -0
  138. data/lib/kamal/utils/sensitive.rb +20 -0
  139. data/lib/kamal/utils.rb +110 -0
  140. data/lib/kamal/version.rb +3 -0
  141. data/lib/kamal.rb +15 -0
  142. 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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Kamal
2
+ VERSION = "2.12.0"
3
+ 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.