kamal 2.0.0.alpha → 2.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) 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 +21 -49
  7. data/lib/kamal/cli/build.rb +21 -14
  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 +213 -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/execution.rb +1 -0
  25. data/lib/kamal/commands/app/proxy.rb +16 -0
  26. data/lib/kamal/commands/app.rb +7 -15
  27. data/lib/kamal/commands/auditor.rb +6 -3
  28. data/lib/kamal/commands/base.rb +8 -0
  29. data/lib/kamal/commands/builder/base.rb +2 -6
  30. data/lib/kamal/commands/builder/hybrid.rb +1 -1
  31. data/lib/kamal/commands/builder/remote.rb +27 -4
  32. data/lib/kamal/commands/builder.rb +1 -1
  33. data/lib/kamal/commands/docker.rb +4 -0
  34. data/lib/kamal/commands/hook.rb +8 -2
  35. data/lib/kamal/commands/lock.rb +2 -6
  36. data/lib/kamal/commands/proxy.rb +72 -0
  37. data/lib/kamal/commands/prune.rb +1 -9
  38. data/lib/kamal/commands/server.rb +11 -1
  39. data/lib/kamal/configuration/accessory.rb +14 -2
  40. data/lib/kamal/configuration/builder.rb +9 -3
  41. data/lib/kamal/configuration/docs/builder.yml +20 -10
  42. data/lib/kamal/configuration/docs/configuration.yml +16 -16
  43. data/lib/kamal/configuration/docs/env.yml +10 -11
  44. data/lib/kamal/configuration/docs/proxy.yml +100 -0
  45. data/lib/kamal/configuration/docs/registry.yml +4 -2
  46. data/lib/kamal/configuration/docs/role.yml +3 -5
  47. data/lib/kamal/configuration/env/tag.rb +4 -3
  48. data/lib/kamal/configuration/env.rb +10 -17
  49. data/lib/kamal/configuration/proxy.rb +66 -0
  50. data/lib/kamal/configuration/registry.rb +3 -2
  51. data/lib/kamal/configuration/role.rb +63 -94
  52. data/lib/kamal/configuration/validator/builder.rb +2 -0
  53. data/lib/kamal/configuration/validator/proxy.rb +11 -0
  54. data/lib/kamal/configuration/validator.rb +3 -1
  55. data/lib/kamal/configuration.rb +80 -33
  56. data/lib/kamal/env_file.rb +4 -0
  57. data/lib/kamal/secrets/adapters/base.rb +18 -0
  58. data/lib/kamal/secrets/adapters/bitwarden.rb +64 -0
  59. data/lib/kamal/secrets/adapters/last_pass.rb +30 -0
  60. data/lib/kamal/secrets/adapters/one_password.rb +61 -0
  61. data/lib/kamal/secrets/adapters/test.rb +10 -0
  62. data/lib/kamal/secrets/adapters.rb +14 -0
  63. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +32 -0
  64. data/lib/kamal/secrets.rb +37 -0
  65. data/lib/kamal/sshkit_with_ext.rb +1 -0
  66. data/lib/kamal/utils.rb +12 -0
  67. data/lib/kamal/version.rb +1 -1
  68. data/lib/kamal.rb +3 -1
  69. metadata +23 -16
  70. data/lib/kamal/cli/env.rb +0 -54
  71. data/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +0 -3
  72. data/lib/kamal/cli/templates/template.env +0 -2
  73. data/lib/kamal/cli/traefik.rb +0 -122
  74. data/lib/kamal/commands/app/cord.rb +0 -22
  75. data/lib/kamal/commands/traefik.rb +0 -85
  76. data/lib/kamal/configuration/docs/healthcheck.yml +0 -59
  77. data/lib/kamal/configuration/docs/traefik.yml +0 -62
  78. data/lib/kamal/configuration/healthcheck.rb +0 -63
  79. data/lib/kamal/configuration/traefik.rb +0 -60
  80. /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.4.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,40 @@ 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"
213
+ end
214
+
215
+ def app_directory
216
+ File.join apps_directory, [ service, destination ].compact.join("-")
210
217
  end
211
218
 
219
+ def env_directory
220
+ File.join app_directory, "env"
221
+ end
222
+
223
+ def assets_directory
224
+ File.join app_directory, "assets"
225
+ end
226
+
227
+
212
228
  def hooks_path
213
229
  raw_config.hooks_path || ".kamal/hooks"
214
230
  end
@@ -218,13 +234,9 @@ class Kamal::Configuration
218
234
  end
219
235
 
220
236
 
221
- def host_env_directory
222
- File.join(run_directory, "env")
223
- end
224
-
225
237
  def env_tags
226
238
  @env_tags ||= if (tags = raw_config.env["tags"])
227
- tags.collect { |name, config| Env::Tag.new(name, config: config) }
239
+ tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
228
240
  else
229
241
  []
230
242
  end
@@ -234,6 +246,18 @@ class Kamal::Configuration
234
246
  env_tags.detect { |t| t.name == name.to_s }
235
247
  end
236
248
 
249
+ def proxy_publish_args
250
+ argumentize "--publish", [ "#{PROXY_HTTP_PORT}:#{PROXY_HTTP_PORT}", "#{PROXY_HTTPS_PORT}:#{PROXY_HTTPS_PORT}" ]
251
+ end
252
+
253
+ def proxy_image
254
+ "basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}"
255
+ end
256
+
257
+ def proxy_container_name
258
+ "kamal-proxy"
259
+ end
260
+
237
261
 
238
262
  def to_h
239
263
  {
@@ -249,8 +273,7 @@ class Kamal::Configuration
249
273
  sshkit: sshkit.to_h,
250
274
  builder: builder.to_h,
251
275
  accessories: raw_config.accessories,
252
- logging: logging_args,
253
- healthcheck: healthcheck.to_h
276
+ logging: logging_args
254
277
  }.compact
255
278
  end
256
279
 
@@ -308,6 +331,30 @@ class Kamal::Configuration
308
331
  true
309
332
  end
310
333
 
334
+ def ensure_no_traefik_reboot_hooks
335
+ hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) }
336
+
337
+ if hooks.any?
338
+ raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot"
339
+ end
340
+
341
+ true
342
+ end
343
+
344
+ def ensure_one_host_for_ssl_roles
345
+ roles.each(&:ensure_one_host_for_ssl)
346
+
347
+ true
348
+ end
349
+
350
+ def ensure_unique_hosts_for_ssl_roles
351
+ hosts = roles.select(&:ssl?).map { |role| role.proxy.host }
352
+ duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
353
+
354
+ raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
355
+
356
+ true
357
+ end
311
358
 
312
359
  def role_names
313
360
  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.beta2"
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.