kamal 2.0.0.alpha → 2.0.0.beta1

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/kamal/cli/accessory.rb +44 -20
  4. data/lib/kamal/cli/app/boot.rb +22 -16
  5. data/lib/kamal/cli/app.rb +37 -3
  6. data/lib/kamal/cli/base.rb +9 -48
  7. data/lib/kamal/cli/build.rb +8 -3
  8. data/lib/kamal/cli/healthcheck/barrier.rb +2 -0
  9. data/lib/kamal/cli/healthcheck/poller.rb +18 -39
  10. data/lib/kamal/cli/lock.rb +2 -3
  11. data/lib/kamal/cli/main.rb +54 -51
  12. data/lib/kamal/cli/proxy.rb +224 -0
  13. data/lib/kamal/cli/prune.rb +0 -1
  14. data/lib/kamal/cli/secrets.rb +36 -0
  15. data/lib/kamal/cli/server.rb +0 -2
  16. data/lib/kamal/cli/templates/deploy.yml +0 -11
  17. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  18. data/lib/kamal/cli/templates/secrets +16 -0
  19. data/lib/kamal/cli.rb +1 -0
  20. data/lib/kamal/commander/specifics.rb +3 -3
  21. data/lib/kamal/commander.rb +17 -9
  22. data/lib/kamal/commands/accessory.rb +7 -7
  23. data/lib/kamal/commands/app/assets.rb +8 -8
  24. data/lib/kamal/commands/app/proxy.rb +16 -0
  25. data/lib/kamal/commands/app.rb +7 -15
  26. data/lib/kamal/commands/auditor.rb +6 -3
  27. data/lib/kamal/commands/base.rb +8 -0
  28. data/lib/kamal/commands/builder/base.rb +2 -6
  29. data/lib/kamal/commands/builder/hybrid.rb +1 -1
  30. data/lib/kamal/commands/builder/remote.rb +27 -4
  31. data/lib/kamal/commands/builder.rb +1 -1
  32. data/lib/kamal/commands/docker.rb +4 -0
  33. data/lib/kamal/commands/hook.rb +5 -2
  34. data/lib/kamal/commands/lock.rb +2 -6
  35. data/lib/kamal/commands/proxy.rb +77 -0
  36. data/lib/kamal/commands/prune.rb +1 -9
  37. data/lib/kamal/commands/server.rb +11 -1
  38. data/lib/kamal/configuration/accessory.rb +14 -2
  39. data/lib/kamal/configuration/builder.rb +9 -3
  40. data/lib/kamal/configuration/docs/builder.yml +20 -10
  41. data/lib/kamal/configuration/docs/configuration.yml +16 -16
  42. data/lib/kamal/configuration/docs/env.yml +10 -11
  43. data/lib/kamal/configuration/docs/proxy.yml +100 -0
  44. data/lib/kamal/configuration/docs/registry.yml +4 -2
  45. data/lib/kamal/configuration/docs/role.yml +3 -5
  46. data/lib/kamal/configuration/env/tag.rb +4 -3
  47. data/lib/kamal/configuration/env.rb +10 -17
  48. data/lib/kamal/configuration/proxy.rb +66 -0
  49. data/lib/kamal/configuration/registry.rb +3 -2
  50. data/lib/kamal/configuration/role.rb +63 -94
  51. data/lib/kamal/configuration/validator/builder.rb +2 -0
  52. data/lib/kamal/configuration/validator/proxy.rb +11 -0
  53. data/lib/kamal/configuration/validator.rb +3 -1
  54. data/lib/kamal/configuration.rb +90 -33
  55. data/lib/kamal/env_file.rb +4 -0
  56. data/lib/kamal/secrets/adapters/base.rb +18 -0
  57. data/lib/kamal/secrets/adapters/bitwarden.rb +64 -0
  58. data/lib/kamal/secrets/adapters/last_pass.rb +30 -0
  59. data/lib/kamal/secrets/adapters/one_password.rb +61 -0
  60. data/lib/kamal/secrets/adapters/test.rb +10 -0
  61. data/lib/kamal/secrets/adapters.rb +14 -0
  62. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +32 -0
  63. data/lib/kamal/secrets.rb +37 -0
  64. data/lib/kamal/sshkit_with_ext.rb +1 -0
  65. data/lib/kamal/utils.rb +12 -0
  66. data/lib/kamal/version.rb +1 -1
  67. data/lib/kamal.rb +3 -1
  68. metadata +23 -16
  69. data/lib/kamal/cli/env.rb +0 -54
  70. data/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +0 -3
  71. data/lib/kamal/cli/templates/template.env +0 -2
  72. data/lib/kamal/cli/traefik.rb +0 -122
  73. data/lib/kamal/commands/app/cord.rb +0 -22
  74. data/lib/kamal/commands/traefik.rb +0 -85
  75. data/lib/kamal/configuration/docs/healthcheck.yml +0 -59
  76. data/lib/kamal/configuration/docs/traefik.yml +0 -62
  77. data/lib/kamal/configuration/healthcheck.rb +0 -63
  78. data/lib/kamal/configuration/traefik.rb +0 -60
  79. /data/lib/kamal/cli/templates/sample_hooks/{pre-traefik-reboot.sample → pre-proxy-reboot.sample} +0 -0
@@ -2,19 +2,22 @@ require "active_support/ordered_options"
2
2
  require "active_support/core_ext/string/inquiry"
3
3
  require "active_support/core_ext/module/delegation"
4
4
  require "active_support/core_ext/hash/keys"
5
- require "pathname"
6
5
  require "erb"
7
6
  require "net/ssh/proxy/jump"
8
7
 
9
8
  class Kamal::Configuration
10
- delegate :service, :image, :labels, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
9
+ delegate :service, :image, :labels, :hooks_path, to: :raw_config, allow_nil: true
11
10
  delegate :argumentize, :optionize, to: Kamal::Utils
12
11
 
13
- attr_reader :destination, :raw_config
14
- attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry
12
+ attr_reader :destination, :raw_config, :secrets
13
+ attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :servers, :ssh, :sshkit, :registry
15
14
 
16
15
  include Validation
17
16
 
17
+ PROXY_MINIMUM_VERSION = "v0.3.0"
18
+ PROXY_HTTP_PORT = 80
19
+ PROXY_HTTPS_PORT = 443
20
+
18
21
  class << self
19
22
  def create_from(config_file:, destination: nil, version: nil)
20
23
  raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
@@ -49,6 +52,8 @@ class Kamal::Configuration
49
52
 
50
53
  validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
51
54
 
55
+ @secrets = Kamal::Secrets.new(destination: destination)
56
+
52
57
  # Eager load config to validate it, these are first as they have dependencies later on
53
58
  @servers = Servers.new(config: self)
54
59
  @registry = Registry.new(config: self)
@@ -57,11 +62,10 @@ class Kamal::Configuration
57
62
  @aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
58
63
  @boot = Boot.new(config: self)
59
64
  @builder = Builder.new(config: self)
60
- @env = Env.new(config: @raw_config.env || {})
65
+ @env = Env.new(config: @raw_config.env || {}, secrets: secrets)
61
66
 
62
- @healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
63
67
  @logging = Logging.new(logging_config: @raw_config.logging)
64
- @traefik = Traefik.new(config: self)
68
+ @proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy || {})
65
69
  @ssh = Ssh.new(config: self)
66
70
  @sshkit = Sshkit.new(config: self)
67
71
 
@@ -70,6 +74,9 @@ class Kamal::Configuration
70
74
  ensure_valid_kamal_version
71
75
  ensure_retain_containers_valid
72
76
  ensure_valid_service_name
77
+ ensure_no_traefik_reboot_hooks
78
+ ensure_one_host_for_ssl_roles
79
+ ensure_unique_hosts_for_ssl_roles
73
80
  end
74
81
 
75
82
 
@@ -130,16 +137,16 @@ class Kamal::Configuration
130
137
  raw_config.allow_empty_roles
131
138
  end
132
139
 
133
- def traefik_roles
134
- roles.select(&:running_traefik?)
140
+ def proxy_roles
141
+ roles.select(&:running_proxy?)
135
142
  end
136
143
 
137
- def traefik_role_names
138
- traefik_roles.flat_map(&:name)
144
+ def proxy_role_names
145
+ proxy_roles.flat_map(&:name)
139
146
  end
140
147
 
141
- def traefik_hosts
142
- traefik_roles.flat_map(&:hosts).uniq
148
+ def proxy_hosts
149
+ proxy_roles.flat_map(&:hosts).uniq
143
150
  end
144
151
 
145
152
  def repository
@@ -184,31 +191,44 @@ class Kamal::Configuration
184
191
  end
185
192
 
186
193
 
187
- def healthcheck_service
188
- [ "healthcheck", service, destination ].compact.join("-")
189
- end
190
-
191
194
  def readiness_delay
192
195
  raw_config.readiness_delay || 7
193
196
  end
194
197
 
195
- def run_id
196
- @run_id ||= SecureRandom.hex(16)
198
+ def deploy_timeout
199
+ raw_config.deploy_timeout || 30
200
+ end
201
+
202
+ def drain_timeout
203
+ raw_config.drain_timeout || 30
197
204
  end
198
205
 
199
206
 
200
207
  def run_directory
201
- raw_config.run_directory || ".kamal"
208
+ ".kamal"
202
209
  end
203
210
 
204
- def run_directory_as_docker_volume
205
- if Pathname.new(run_directory).absolute?
206
- run_directory
207
- else
208
- File.join "$(pwd)", run_directory
209
- end
211
+ def apps_directory
212
+ File.join run_directory, "apps"
210
213
  end
211
214
 
215
+ def app_directory
216
+ File.join apps_directory, [ service, destination ].compact.join("-")
217
+ end
218
+
219
+ def proxy_directory
220
+ File.join run_directory, "proxy"
221
+ end
222
+
223
+ def env_directory
224
+ File.join app_directory, "env"
225
+ end
226
+
227
+ def assets_directory
228
+ File.join app_directory, "assets"
229
+ end
230
+
231
+
212
232
  def hooks_path
213
233
  raw_config.hooks_path || ".kamal/hooks"
214
234
  end
@@ -218,13 +238,9 @@ class Kamal::Configuration
218
238
  end
219
239
 
220
240
 
221
- def host_env_directory
222
- File.join(run_directory, "env")
223
- end
224
-
225
241
  def env_tags
226
242
  @env_tags ||= if (tags = raw_config.env["tags"])
227
- tags.collect { |name, config| Env::Tag.new(name, config: config) }
243
+ tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
228
244
  else
229
245
  []
230
246
  end
@@ -234,6 +250,24 @@ class Kamal::Configuration
234
250
  env_tags.detect { |t| t.name == name.to_s }
235
251
  end
236
252
 
253
+ def proxy_publish_args
254
+ argumentize "--publish", [ "#{PROXY_HTTP_PORT}:#{PROXY_HTTP_PORT}", "#{PROXY_HTTPS_PORT}:#{PROXY_HTTPS_PORT}" ]
255
+ end
256
+
257
+ def proxy_image
258
+ "basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}"
259
+ end
260
+
261
+ def proxy_container_name
262
+ "kamal-proxy"
263
+ end
264
+
265
+ def proxy_config_volume
266
+ Kamal::Configuration::Volume.new \
267
+ host_path: File.join(proxy_directory, "config"),
268
+ container_path: "/home/kamal-proxy/.config/kamal-proxy"
269
+ end
270
+
237
271
 
238
272
  def to_h
239
273
  {
@@ -249,8 +283,7 @@ class Kamal::Configuration
249
283
  sshkit: sshkit.to_h,
250
284
  builder: builder.to_h,
251
285
  accessories: raw_config.accessories,
252
- logging: logging_args,
253
- healthcheck: healthcheck.to_h
286
+ logging: logging_args
254
287
  }.compact
255
288
  end
256
289
 
@@ -308,6 +341,30 @@ class Kamal::Configuration
308
341
  true
309
342
  end
310
343
 
344
+ def ensure_no_traefik_reboot_hooks
345
+ hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) }
346
+
347
+ if hooks.any?
348
+ raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot"
349
+ end
350
+
351
+ true
352
+ end
353
+
354
+ def ensure_one_host_for_ssl_roles
355
+ roles.each(&:ensure_one_host_for_ssl)
356
+
357
+ true
358
+ end
359
+
360
+ def ensure_unique_hosts_for_ssl_roles
361
+ hosts = roles.select(&:ssl?).map { |role| role.proxy.host }
362
+ duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
363
+
364
+ raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
365
+
366
+ true
367
+ end
311
368
 
312
369
  def role_names
313
370
  raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
@@ -15,6 +15,10 @@ class Kamal::EnvFile
15
15
  env_file.presence || "\n"
16
16
  end
17
17
 
18
+ def to_io
19
+ StringIO.new(to_s)
20
+ end
21
+
18
22
  alias to_str to_s
19
23
 
20
24
  private
@@ -0,0 +1,18 @@
1
+ class Kamal::Secrets::Adapters::Base
2
+ delegate :optionize, to: Kamal::Utils
3
+
4
+ def fetch(secrets, account:, from: nil)
5
+ session = login(account)
6
+ full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
7
+ fetch_secrets(full_secrets, account: account, session: session)
8
+ end
9
+
10
+ private
11
+ def login(...)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def fetch_secrets(...)
16
+ raise NotImplementedError
17
+ end
18
+ end
@@ -0,0 +1,64 @@
1
+ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
2
+ private
3
+ def login(account)
4
+ status = run_command("status")
5
+
6
+ if status["status"] == "unauthenticated"
7
+ run_command("login #{account.shellescape}", raw: true)
8
+ status = run_command("status")
9
+ end
10
+
11
+ if status["status"] == "locked"
12
+ session = run_command("unlock --raw", raw: true).presence
13
+ status = run_command("status", session: session)
14
+ end
15
+
16
+ raise RuntimeError, "Failed to login to and unlock Bitwarden" unless status["status"] == "unlocked"
17
+
18
+ run_command("sync", session: session, raw: true)
19
+ raise RuntimeError, "Failed to sync Bitwarden" unless $?.success?
20
+
21
+ session
22
+ end
23
+
24
+ def fetch_secrets(secrets, account:, session:)
25
+ {}.tap do |results|
26
+ items_fields(secrets).each do |item, fields|
27
+ item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
28
+ raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success?
29
+ item_json = JSON.parse(item_json)
30
+
31
+ if fields.any?
32
+ fields.each do |field|
33
+ item_field = item_json["fields"].find { |f| f["name"] == field }
34
+ raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
35
+ value = item_field["value"]
36
+ results["#{item}/#{field}"] = value
37
+ end
38
+ else
39
+ results[item] = item_json["login"]["password"]
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def items_fields(secrets)
46
+ {}.tap do |items|
47
+ secrets.each do |secret|
48
+ item, field = secret.split("/")
49
+ items[item] ||= []
50
+ items[item] << field
51
+ end
52
+ end
53
+ end
54
+
55
+ def signedin?(account)
56
+ run_command("status")["status"] != "unauthenticated"
57
+ end
58
+
59
+ def run_command(command, session: nil, raw: false)
60
+ full_command = [ *("BW_SESSION=#{session.shellescape}" if session), "bw", command ].join(" ")
61
+ result = `#{full_command}`.strip
62
+ raw ? result : JSON.parse(result)
63
+ end
64
+ end
@@ -0,0 +1,30 @@
1
+ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
2
+ private
3
+ def login(account)
4
+ unless loggedin?(account)
5
+ `lpass login #{account.shellescape}`
6
+ raise RuntimeError, "Failed to login to 1Password" unless $?.success?
7
+ end
8
+ end
9
+
10
+ def loggedin?(account)
11
+ `lpass status --color never`.strip == "Logged in as #{account}."
12
+ end
13
+
14
+ def fetch_secrets(secrets, account:, session:)
15
+ items = `lpass show #{secrets.map(&:shellescape).join(" ")} --json`
16
+ raise RuntimeError, "Could not read #{secrets} from 1Password" unless $?.success?
17
+
18
+ items = JSON.parse(items)
19
+
20
+ {}.tap do |results|
21
+ items.each do |item|
22
+ results[item["fullname"]] = item["password"]
23
+ end
24
+
25
+ if (missing_items = secrets - results.keys).any?
26
+ raise RuntimeError, "Could not find #{missing_items.join(", ")} in LassPass"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,61 @@
1
+ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
2
+ delegate :optionize, to: Kamal::Utils
3
+
4
+ private
5
+ def login(account)
6
+ unless loggedin?(account)
7
+ `op signin #{to_options(account: account, force: true, raw: true)}`.tap do
8
+ raise RuntimeError, "Failed to login to 1Password" unless $?.success?
9
+ end
10
+ end
11
+ end
12
+
13
+ def loggedin?(account)
14
+ `op account get --account #{account.shellescape} 2> /dev/null`
15
+ $?.success?
16
+ end
17
+
18
+ def fetch_secrets(secrets, account:, session:)
19
+ {}.tap do |results|
20
+ vaults_items_fields(secrets).map do |vault, items|
21
+ items.each do |item, fields|
22
+ fields_json = JSON.parse(op_item_get(vault, item, fields, account: account, session: session))
23
+ fields_json = [ fields_json ] if fields.one?
24
+
25
+ fields_json.each do |field_json|
26
+ # The reference is in the form `op://vault/item/field[/field]`
27
+ field = field_json["reference"].delete_prefix("op://").delete_suffix("/password")
28
+ results[field] = field_json["value"]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def to_options(**options)
36
+ optionize(options.compact).join(" ")
37
+ end
38
+
39
+ def vaults_items_fields(secrets)
40
+ {}.tap do |vaults|
41
+ secrets.each do |secret|
42
+ secret = secret.delete_prefix("op://")
43
+ vault, item, *fields = secret.split("/")
44
+ fields << "password" if fields.empty?
45
+
46
+ vaults[vault] ||= {}
47
+ vaults[vault][item] ||= []
48
+ vaults[vault][item] << fields.join(".")
49
+ end
50
+ end
51
+ end
52
+
53
+ def op_item_get(vault, item, fields, account:, session:)
54
+ labels = fields.map { |field| "label=#{field}" }.join(",")
55
+ options = to_options(vault: vault, fields: labels, format: "json", account: account, session: session.presence)
56
+
57
+ `op item get #{item.shellescape} #{options}`.tap do
58
+ raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,10 @@
1
+ class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
2
+ private
3
+ def login(account)
4
+ true
5
+ end
6
+
7
+ def fetch_secrets(secrets, account:, session:)
8
+ secrets.to_h { |secret| [ secret, secret.reverse ] }
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ require "active_support/core_ext/string/inflections"
2
+ module Kamal::Secrets::Adapters
3
+ def self.lookup(name)
4
+ name = "one_password" if name.downcase == "1password"
5
+ name = "last_pass" if name.downcase == "lastpass"
6
+ adapter_class(name)
7
+ end
8
+
9
+ def self.adapter_class(name)
10
+ Object.const_get("Kamal::Secrets::Adapters::#{name.camelize}").new
11
+ rescue NameError => e
12
+ raise RuntimeError, "Unknown secrets adapter: #{name}"
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ class Kamal::Secrets::Dotenv::InlineCommandSubstitution
2
+ class << self
3
+ def install!
4
+ ::Dotenv::Parser.substitutions.map! { |sub| sub == ::Dotenv::Substitutions::Command ? self : sub }
5
+ end
6
+
7
+ def call(value, _env, overwrite: false)
8
+ # Process interpolated shell commands
9
+ value.gsub(Dotenv::Substitutions::Command.singleton_class::INTERPOLATED_SHELL_COMMAND) do |*|
10
+ # Eliminate opening and closing parentheses
11
+ command = $LAST_MATCH_INFO[:cmd][1..-2]
12
+
13
+ if $LAST_MATCH_INFO[:backslash]
14
+ # Command is escaped, don't replace it.
15
+ $LAST_MATCH_INFO[0][1..]
16
+ else
17
+ if command =~ /\A\s*kamal\s*secrets\s+/
18
+ # Inline the command
19
+ inline_secrets_command(command)
20
+ else
21
+ # Execute the command and return the value
22
+ `#{command}`.chomp
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def inline_secrets_command(command)
29
+ Kamal::Cli::Main.start(command.shellsplit[1..] + [ "--inline" ]).chomp
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ require "dotenv"
2
+
3
+ class Kamal::Secrets
4
+ attr_reader :secrets_files
5
+
6
+ Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
7
+
8
+ def initialize(destination: nil)
9
+ @secrets_files = \
10
+ [ ".kamal/secrets-common", ".kamal/secrets#{(".#{destination}" if destination)}" ].select { |f| File.exist?(f) }
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def [](key)
15
+ # Fetching secrets may ask the user for input, so ensure only one thread does that
16
+ @mutex.synchronize do
17
+ secrets.fetch(key)
18
+ end
19
+ rescue KeyError
20
+ if secrets_files
21
+ raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
22
+ else
23
+ raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided"
24
+ end
25
+ end
26
+
27
+ def to_h
28
+ secrets
29
+ end
30
+
31
+ private
32
+ def secrets
33
+ @secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
34
+ secrets.merge!(::Dotenv.parse(secrets_file))
35
+ end
36
+ end
37
+ end
@@ -3,6 +3,7 @@ require "sshkit/dsl"
3
3
  require "net/scp"
4
4
  require "active_support/core_ext/hash/deep_merge"
5
5
  require "json"
6
+ require "concurrent/atomic/semaphore"
6
7
 
7
8
  class SSHKit::Backend::Abstract
8
9
  def capture_with_info(*args, **kwargs)
data/lib/kamal/utils.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require "active_support/core_ext/object/try"
2
+
1
3
  module Kamal::Utils
2
4
  extend self
3
5
 
@@ -54,6 +56,12 @@ module Kamal::Utils
54
56
 
55
57
  # Escape a value to make it safe for shell use.
56
58
  def escape_shell_value(value)
59
+ value.to_s.scan(/[\x00-\x7F]+|[^\x00-\x7F]+/) \
60
+ .map { |part| part.ascii_only? ? escape_ascii_shell_value(part) : part }
61
+ .join
62
+ end
63
+
64
+ def escape_ascii_shell_value(value)
57
65
  value.to_s.dump
58
66
  .gsub(/`/, '\\\\`')
59
67
  .gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
@@ -93,4 +101,8 @@ module Kamal::Utils
93
101
  arch
94
102
  end
95
103
  end
104
+
105
+ def older_version?(version, other_version)
106
+ Gem::Version.new(version.delete_prefix("v")) < Gem::Version.new(other_version.delete_prefix("v"))
107
+ end
96
108
  end
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "2.0.0.alpha"
2
+ VERSION = "2.0.0.beta1"
3
3
  end
data/lib/kamal.rb CHANGED
@@ -5,8 +5,10 @@ end
5
5
  require "active_support"
6
6
  require "zeitwerk"
7
7
  require "yaml"
8
+ require "tmpdir"
9
+ require "pathname"
8
10
 
9
11
  loader = Zeitwerk::Loader.for_gem
10
12
  loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
11
13
  loader.setup
12
- loader.eager_load # We need all commands loaded.
14
+ loader.eager_load_namespace(Kamal::Cli) # We need all commands loaded.