kamal 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2071f73b4e7a6d63939bbfd4f07caacd95aa637964c07fffaa261711f602b5f6
4
- data.tar.gz: 0c265ae0c39673018ece39e7fc16acfcc6e7148efdf6a011827548ffb5a332de
3
+ metadata.gz: df82a171edf966865ca45112056965b93b50a700c4ae190cebe791992c41a825
4
+ data.tar.gz: 9c5290566c477597460910a80a6e0694285e6d9779b6897bc6c1c10f5bd23b7b
5
5
  SHA512:
6
- metadata.gz: 0d21ac54e6eea13f7e89280d3358a342e54e629f402864b591538d018f19f3275fe88104c06e85a73e3846ddfd606f3106dfb3ab7757629dd931e6b63f111767
7
- data.tar.gz: be9bc4f8a641ef661adb78af0853ee3045d93f40d48c357627f80a5c43a5295125b4629658332b314c45f6134b5b87f03e386b0c432b734a5ca838fda0a027e2
6
+ metadata.gz: c2995cb335465be583037f0a23e2a5d3ce128b47e162b69d7666dbc3e313b542c1d10abef9fa2bb542bdb679003dc8993c3037c4738a9e8544fb346755e8ced0
7
+ data.tar.gz: a0b7d5500f7ad5408409e0f655274e19a833a8158d2013e79a70e7209d87f5c350b715071d8981f9119f9b3d8c66033bfcd23b5290d3d3880511b3c98c7cff83
@@ -5,11 +5,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
5
5
  if name == "all"
6
6
  KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
7
7
  else
8
- with_accessory(name) do |accessory|
8
+ with_accessory(name) do |accessory, hosts|
9
9
  directories(name)
10
10
  upload(name)
11
11
 
12
- on(accessory.hosts) do
12
+ on(hosts) do
13
13
  execute *KAMAL.registry.login if login
14
14
  execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
15
15
  execute *accessory.run
@@ -22,8 +22,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
22
22
  desc "upload [NAME]", "Upload accessory files to host", hide: true
23
23
  def upload(name)
24
24
  mutating do
25
- with_accessory(name) do |accessory|
26
- on(accessory.hosts) do
25
+ with_accessory(name) do |accessory, hosts|
26
+ on(hosts) do
27
27
  accessory.files.each do |(local, remote)|
28
28
  accessory.ensure_local_file_present(local)
29
29
 
@@ -39,8 +39,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
39
39
  desc "directories [NAME]", "Create accessory directories on host", hide: true
40
40
  def directories(name)
41
41
  mutating do
42
- with_accessory(name) do |accessory|
43
- on(accessory.hosts) do
42
+ with_accessory(name) do |accessory, hosts|
43
+ on(hosts) do
44
44
  accessory.directories.keys.each do |host_path|
45
45
  execute *accessory.make_directory(host_path)
46
46
  end
@@ -55,8 +55,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
55
55
  if name == "all"
56
56
  KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
57
57
  else
58
- with_accessory(name) do |accessory|
59
- on(accessory.hosts) do
58
+ with_accessory(name) do |accessory, hosts|
59
+ on(hosts) do
60
60
  execute *KAMAL.registry.login
61
61
  end
62
62
 
@@ -71,8 +71,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
71
71
  desc "start [NAME]", "Start existing accessory container on host"
72
72
  def start(name)
73
73
  mutating do
74
- with_accessory(name) do |accessory|
75
- on(accessory.hosts) do
74
+ with_accessory(name) do |accessory, hosts|
75
+ on(hosts) do
76
76
  execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
77
77
  execute *accessory.start
78
78
  end
@@ -83,8 +83,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
83
83
  desc "stop [NAME]", "Stop existing accessory container on host"
84
84
  def stop(name)
85
85
  mutating do
86
- with_accessory(name) do |accessory|
87
- on(accessory.hosts) do
86
+ with_accessory(name) do |accessory, hosts|
87
+ on(hosts) do
88
88
  execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
89
89
  execute *accessory.stop, raise_on_non_zero_exit: false
90
90
  end
@@ -107,8 +107,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
107
107
  if name == "all"
108
108
  KAMAL.accessory_names.each { |accessory_name| details(accessory_name) }
109
109
  else
110
- with_accessory(name) do |accessory|
111
- on(accessory.hosts) { puts capture_with_info(*accessory.info) }
110
+ with_accessory(name) do |accessory, hosts|
111
+ on(hosts) { puts capture_with_info(*accessory.info) }
112
112
  end
113
113
  end
114
114
  end
@@ -117,7 +117,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
117
117
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
118
118
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
119
119
  def exec(name, cmd)
120
- with_accessory(name) do |accessory|
120
+ with_accessory(name) do |accessory, hosts|
121
121
  case
122
122
  when options[:interactive] && options[:reuse]
123
123
  say "Launching interactive command with via SSH from existing container...", :magenta
@@ -129,14 +129,14 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
129
129
 
130
130
  when options[:reuse]
131
131
  say "Launching command from existing container...", :magenta
132
- on(accessory.hosts) do
132
+ on(hosts) do
133
133
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
134
134
  capture_with_info(*accessory.execute_in_existing_container(cmd))
135
135
  end
136
136
 
137
137
  else
138
138
  say "Launching command from new container...", :magenta
139
- on(accessory.hosts) do
139
+ on(hosts) do
140
140
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
141
141
  capture_with_info(*accessory.execute_in_new_container(cmd))
142
142
  end
@@ -150,12 +150,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
150
150
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
151
151
  option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
152
152
  def logs(name)
153
- with_accessory(name) do |accessory|
153
+ with_accessory(name) do |accessory, hosts|
154
154
  grep = options[:grep]
155
155
 
156
156
  if options[:follow]
157
157
  run_locally do
158
- info "Following logs on #{accessory.hosts}..."
158
+ info "Following logs on #{hosts}..."
159
159
  info accessory.follow_logs(grep: grep)
160
160
  exec accessory.follow_logs(grep: grep)
161
161
  end
@@ -163,7 +163,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
163
163
  since = options[:since]
164
164
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
165
165
 
166
- on(accessory.hosts) do
166
+ on(hosts) do
167
167
  puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep))
168
168
  end
169
169
  end
@@ -192,8 +192,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
192
192
  desc "remove_container [NAME]", "Remove accessory container from host", hide: true
193
193
  def remove_container(name)
194
194
  mutating do
195
- with_accessory(name) do |accessory|
196
- on(accessory.hosts) do
195
+ with_accessory(name) do |accessory, hosts|
196
+ on(hosts) do
197
197
  execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
198
198
  execute *accessory.remove_container
199
199
  end
@@ -204,8 +204,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
204
204
  desc "remove_image [NAME]", "Remove accessory image from host", hide: true
205
205
  def remove_image(name)
206
206
  mutating do
207
- with_accessory(name) do |accessory|
208
- on(accessory.hosts) do
207
+ with_accessory(name) do |accessory, hosts|
208
+ on(hosts) do
209
209
  execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
210
210
  execute *accessory.remove_image
211
211
  end
@@ -216,8 +216,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
216
216
  desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
217
217
  def remove_service_directory(name)
218
218
  mutating do
219
- with_accessory(name) do |accessory|
220
- on(accessory.hosts) do
219
+ with_accessory(name) do |accessory, hosts|
220
+ on(hosts) do
221
221
  execute *accessory.remove_service_directory
222
222
  end
223
223
  end
@@ -227,7 +227,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
227
227
  private
228
228
  def with_accessory(name)
229
229
  if accessory = KAMAL.accessory(name)
230
- yield accessory
230
+ yield accessory, accessory_hosts(accessory)
231
231
  else
232
232
  error_on_missing_accessory(name)
233
233
  end
@@ -240,4 +240,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
240
240
  "No accessory by the name of '#{name}'" +
241
241
  (options ? " (options: #{options.to_sentence})" : "")
242
242
  end
243
+
244
+ def accessory_hosts(accessory)
245
+ if KAMAL.specific_hosts&.any?
246
+ KAMAL.specific_hosts & accessory.hosts
247
+ else
248
+ accessory.hosts
249
+ end
250
+ end
243
251
  end
data/lib/kamal/cli/app.rb CHANGED
@@ -13,9 +13,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
13
13
 
14
14
  KAMAL.roles_on(host).each do |role|
15
15
  app = KAMAL.app(role: role)
16
- role_config = KAMAL.config.role(role)
17
16
 
18
- if role_config.assets?
17
+ if role.assets?
19
18
  execute *app.extract_assets
20
19
  old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
21
20
  execute *app.sync_asset_volumes(old_version: old_version)
@@ -27,7 +26,6 @@ class Kamal::Cli::App < Kamal::Cli::Base
27
26
  KAMAL.roles_on(host).each do |role|
28
27
  app = KAMAL.app(role: role)
29
28
  auditor = KAMAL.auditor(role: role)
30
- role_config = KAMAL.config.role(role)
31
29
 
32
30
  if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
33
31
  tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
@@ -38,7 +36,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
38
36
 
39
37
  old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
40
38
 
41
- execute *app.tie_cord(role_config.cord_host_file) if role_config.uses_cord?
39
+ execute *app.tie_cord(role.cord_host_file) if role.uses_cord?
42
40
 
43
41
  execute *auditor.record("Booted app version #{version}"), verbosity: :debug
44
42
 
@@ -47,7 +45,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
47
45
  Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
48
46
 
49
47
  if old_version.present?
50
- if role_config.uses_cord?
48
+ if role.uses_cord?
51
49
  cord = capture_with_info(*app.cord(version: old_version), raise_on_non_zero_exit: false).strip
52
50
  if cord.present?
53
51
  execute *app.cut_cord(cord)
@@ -57,7 +55,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
57
55
 
58
56
  execute *app.stop(version: old_version), raise_on_non_zero_exit: false
59
57
 
60
- execute *app.clean_up_assets if role_config.assets?
58
+ execute *app.clean_up_assets if role.assets?
61
59
  end
62
60
  end
63
61
  end
@@ -202,19 +200,20 @@ class Kamal::Cli::App < Kamal::Cli::Base
202
200
  # FIXME: Catch when app containers aren't running
203
201
 
204
202
  grep = options[:grep]
205
-
203
+ since = options[:since]
206
204
  if options[:follow]
205
+ lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
206
+
207
207
  run_locally do
208
208
  info "Following logs on #{KAMAL.primary_host}..."
209
209
 
210
210
  KAMAL.specific_roles ||= ["web"]
211
211
  role = KAMAL.roles_on(KAMAL.primary_host).first
212
212
 
213
- info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
214
- exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
213
+ info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
214
+ exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
215
215
  end
216
216
  else
217
- since = options[:since]
218
217
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
219
218
 
220
219
  on(KAMAL.hosts) do |host|
@@ -123,8 +123,9 @@ module Kamal::Cli
123
123
  yield
124
124
  rescue SSHKit::Runner::ExecuteError => e
125
125
  if e.message =~ /cannot create directory/
126
+ say "Deploy lock already in place!", :red
126
127
  on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
127
- raise LockError, "Deploy lock found"
128
+ raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
128
129
  else
129
130
  raise e
130
131
  end
data/lib/kamal/cli/env.rb CHANGED
@@ -8,9 +8,8 @@ class Kamal::Cli::Env < Kamal::Cli::Base
8
8
  execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug
9
9
 
10
10
  KAMAL.roles_on(host).each do |role|
11
- role_config = KAMAL.config.role(role)
12
11
  execute *KAMAL.app(role: role).make_env_directory
13
- upload! StringIO.new(role_config.env_file), role_config.host_env_file_path, mode: 400
12
+ upload! StringIO.new(role.env_file), role.host_env_file_path, mode: 400
14
13
  end
15
14
  end
16
15
 
@@ -36,7 +35,6 @@ class Kamal::Cli::Env < Kamal::Cli::Base
36
35
  execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug
37
36
 
38
37
  KAMAL.roles_on(host).each do |role|
39
- role_config = KAMAL.config.role(role)
40
38
  execute *KAMAL.app(role: role).remove_env_file
41
39
  end
42
40
  end
@@ -1,15 +1,18 @@
1
1
  class Kamal::Cli::Main < Kamal::Cli::Base
2
2
  desc "setup", "Setup all accessories, push the env, and deploy app to servers"
3
+ option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
3
4
  def setup
4
5
  print_runtime do
5
6
  mutating do
7
+ invoke_options = deploy_options
8
+
6
9
  say "Ensure Docker is installed...", :magenta
7
- invoke "kamal:cli:server:bootstrap"
10
+ invoke "kamal:cli:server:bootstrap", [], invoke_options
8
11
 
9
12
  say "Push env files...", :magenta
10
- invoke "kamal:cli:env:push"
13
+ invoke "kamal:cli:env:push", [], invoke_options
11
14
 
12
- invoke "kamal:cli:accessory:boot", [ "all" ]
15
+ invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options
13
16
  deploy
14
17
  end
15
18
  end
@@ -18,12 +18,16 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
18
18
  end
19
19
  end
20
20
 
21
- desc "containers", "Prune all stopped containers, except the last 5"
21
+ desc "containers", "Prune all stopped containers, except the last n (default 5)"
22
+ option :retain, type: :numeric, default: nil, desc: "Number of containers to retain"
22
23
  def containers
24
+ retain = options.fetch(:retain, KAMAL.config.retain_containers)
25
+ raise "retain must be at least 1" if retain < 1
26
+
23
27
  mutating do
24
28
  on(KAMAL.hosts) do
25
29
  execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
26
- execute *KAMAL.prune.app_containers
30
+ execute *KAMAL.prune.app_containers(retain: retain)
27
31
  execute *KAMAL.prune.healthcheck_containers
28
32
  end
29
33
  end
@@ -17,7 +17,9 @@ class Kamal::Cli::Server < Kamal::Cli::Base
17
17
  end
18
18
 
19
19
  if missing.any?
20
- raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/"
20
+ raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and either `wget` or `curl`. Install Docker manually: https://docs.docker.com/engine/install/"
21
21
  end
22
+
23
+ run_hook "docker-setup"
22
24
  end
23
25
  end
@@ -77,6 +77,10 @@ registry:
77
77
  # Bridge fingerprinted assets, like JS and CSS, between versions to avoid
78
78
  # hitting 404 on in-flight requests. Combines all files from new and old
79
79
  # version inside the asset_path.
80
+ #
81
+ # If your app is using the Sprockets gem, ensure it sets `config.assets.manifest`.
82
+ # See https://github.com/basecamp/kamal/issues/626 for details
83
+ #
80
84
  # asset_path: /rails/public/assets
81
85
 
82
86
  # Configure rolling deploys by setting a wait time between batches of restarts.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # A sample docker-setup hook
4
+ #
5
+ # Sets up a Docker network which can then be used by the application’s containers
6
+
7
+ ssh user@example.com docker network create kamal
@@ -20,7 +20,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
20
20
  on(hosts) do
21
21
  execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
22
22
  execute *KAMAL.registry.login
23
- execute *KAMAL.traefik.stop
23
+ execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
24
24
  execute *KAMAL.traefik.remove_container
25
25
  execute *KAMAL.traefik.run
26
26
  end
@@ -44,7 +44,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
44
44
  mutating do
45
45
  on(KAMAL.traefik_hosts) do
46
46
  execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
47
- execute *KAMAL.traefik.stop
47
+ execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
48
48
  end
49
49
  end
50
50
  end
@@ -53,7 +53,7 @@ class Kamal::Commander
53
53
 
54
54
  def primary_host
55
55
  # Given a list of specific roles, make an effort to match up with the primary_role
56
- specific_hosts&.first || specific_roles&.detect { |role| role.name == config.primary_role }&.primary_host || specific_roles&.first&.primary_host || config.primary_host
56
+ specific_hosts&.first || specific_roles&.detect { |role| role == config.primary_role }&.primary_host || specific_roles&.first&.primary_host || config.primary_host
57
57
  end
58
58
 
59
59
  def primary_role
@@ -73,7 +73,7 @@ class Kamal::Commander
73
73
  end
74
74
 
75
75
  def roles_on(host)
76
- roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
76
+ roles.select { |role| role.hosts.include?(host.to_s) }
77
77
  end
78
78
 
79
79
  def traefik_hosts
@@ -1,20 +1,20 @@
1
1
  module Kamal::Commands::App::Assets
2
2
  def extract_assets
3
- asset_container = "#{role_config.container_prefix}-assets"
3
+ asset_container = "#{role.container_prefix}-assets"
4
4
 
5
5
  combine \
6
- make_directory(role_config.asset_extracted_path),
6
+ make_directory(role.asset_extracted_path),
7
7
  [*docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true"],
8
8
  docker(:run, "--name", asset_container, "--detach", "--rm", config.latest_image, "sleep 1000000"),
9
- docker(:cp, "-L", "#{asset_container}:#{role_config.asset_path}/.", role_config.asset_extracted_path),
9
+ docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_path),
10
10
  docker(:stop, "-t 1", asset_container),
11
11
  by: "&&"
12
12
  end
13
13
 
14
14
  def sync_asset_volumes(old_version: nil)
15
- new_extracted_path, new_volume_path = role_config.asset_extracted_path(config.version), role_config.asset_volume.host_path
15
+ new_extracted_path, new_volume_path = role.asset_extracted_path(config.version), role.asset_volume.host_path
16
16
  if old_version.present?
17
- old_extracted_path, old_volume_path = role_config.asset_extracted_path(old_version), role_config.asset_volume(old_version).host_path
17
+ old_extracted_path, old_volume_path = role.asset_extracted_path(old_version), role.asset_volume(old_version).host_path
18
18
  end
19
19
 
20
20
  commands = [make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path)]
@@ -29,8 +29,8 @@ module Kamal::Commands::App::Assets
29
29
 
30
30
  def clean_up_assets
31
31
  chain \
32
- find_and_remove_older_siblings(role_config.asset_extracted_path),
33
- find_and_remove_older_siblings(role_config.asset_volume_path)
32
+ find_and_remove_older_siblings(role.asset_extracted_path),
33
+ find_and_remove_older_siblings(role.asset_volume_path)
34
34
  end
35
35
 
36
36
  private
@@ -39,7 +39,7 @@ module Kamal::Commands::App::Assets
39
39
  :find,
40
40
  Pathname.new(path).dirname.to_s,
41
41
  "-maxdepth 1",
42
- "-name", "'#{role_config.container_prefix}-*'",
42
+ "-name", "'#{role.container_prefix}-*'",
43
43
  "!", "-name", Pathname.new(path).basename.to_s,
44
44
  "-exec rm -rf \"{}\" +"
45
45
  ]
@@ -2,7 +2,7 @@ module Kamal::Commands::App::Cord
2
2
  def cord(version:)
3
3
  pipe \
4
4
  docker(:inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", container_name(version)),
5
- [:awk, "'$2 == \"#{role_config.cord_volume.container_path}\" {print $1}'"]
5
+ [:awk, "'$2 == \"#{role.cord_volume.container_path}\" {print $1}'"]
6
6
  end
7
7
 
8
8
  def tie_cord(cord)
@@ -12,8 +12,8 @@ module Kamal::Commands::App::Cord
12
12
  def cut_cord(cord)
13
13
  remove_directory(cord)
14
14
  end
15
-
16
- private
15
+
16
+ private
17
17
  def create_empty_file(file)
18
18
  chain \
19
19
  make_directory_for(file),
@@ -10,9 +10,9 @@ module Kamal::Commands::App::Execution
10
10
  docker :run,
11
11
  ("-it" if interactive),
12
12
  "--rm",
13
- *role_config&.env_args,
13
+ *role&.env_args,
14
14
  *config.volume_args,
15
- *role_config&.option_args,
15
+ *role&.option_args,
16
16
  config.absolute_image,
17
17
  *command
18
18
  end
@@ -6,11 +6,11 @@ module Kamal::Commands::App::Logging
6
6
  ("grep '#{grep}'" if grep)
7
7
  end
8
8
 
9
- def follow_logs(host:, grep: nil)
9
+ def follow_logs(host:, lines: nil, grep: nil)
10
10
  run_over_ssh \
11
11
  pipe(
12
12
  current_running_container_id,
13
- "xargs docker logs --timestamps --tail 10 --follow 2>&1",
13
+ "xargs docker logs --timestamps#{" --tail #{lines}" if lines} --follow 2>&1",
14
14
  (%(grep "#{grep}") if grep)
15
15
  ),
16
16
  host: host
@@ -3,12 +3,11 @@ class Kamal::Commands::App < Kamal::Commands::Base
3
3
 
4
4
  ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
5
5
 
6
- attr_reader :role, :role_config
6
+ attr_reader :role, :role
7
7
 
8
8
  def initialize(config, role: nil)
9
9
  super(config)
10
10
  @role = role
11
- @role_config = config.role(self.role)
12
11
  end
13
12
 
14
13
  def run(hostname: nil)
@@ -19,15 +18,15 @@ class Kamal::Commands::App < Kamal::Commands::Base
19
18
  *(["--hostname", hostname] if hostname),
20
19
  "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
21
20
  "-e", "KAMAL_VERSION=\"#{config.version}\"",
22
- *role_config.env_args,
23
- *role_config.health_check_args,
24
- *config.logging_args,
21
+ *role.env_args,
22
+ *role.health_check_args,
23
+ *role.logging_args,
25
24
  *config.volume_args,
26
- *role_config.asset_volume_args,
27
- *role_config.label_args,
28
- *role_config.option_args,
25
+ *role.asset_volume_args,
26
+ *role.label_args,
27
+ *role.option_args,
29
28
  config.absolute_image,
30
- role_config.cmd
29
+ role.cmd
31
30
  end
32
31
 
33
32
  def start
@@ -64,22 +63,22 @@ class Kamal::Commands::App < Kamal::Commands::Base
64
63
  def list_versions(*docker_args, statuses: nil)
65
64
  pipe \
66
65
  docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
67
- %(while read line; do echo ${line##{role_config.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
66
+ %(while read line; do echo ${line##{role.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
68
67
  end
69
68
 
70
69
 
71
70
  def make_env_directory
72
- make_directory role_config.host_env_directory
71
+ make_directory role.host_env_directory
73
72
  end
74
73
 
75
74
  def remove_env_file
76
- [ :rm, "-f", role_config.host_env_file_path ]
75
+ [ :rm, "-f", role.host_env_file_path ]
77
76
  end
78
77
 
79
78
 
80
79
  private
81
80
  def container_name(version = nil)
82
- [ role_config.container_prefix, version || config.version ].compact.join("-")
81
+ [ role.container_prefix, version || config.version ].compact.join("-")
83
82
  end
84
83
 
85
84
  def filter_args(statuses: nil)
@@ -62,10 +62,18 @@ module Kamal::Commands
62
62
  combine *commands, by: ">"
63
63
  end
64
64
 
65
+ def any(*commands)
66
+ combine *commands, by: "||"
67
+ end
68
+
65
69
  def xargs(command)
66
70
  [ :xargs, command ].flatten
67
71
  end
68
72
 
73
+ def shell(command)
74
+ [ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\''")}'" ]
75
+ end
76
+
69
77
  def docker(*args)
70
78
  args.compact.unshift :docker
71
79
  end
@@ -3,7 +3,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
3
3
  class BuilderError < StandardError; end
4
4
 
5
5
  delegate :argumentize, to: Kamal::Utils
6
- delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, to: :builder_config
6
+ delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config
7
7
 
8
8
  def clean
9
9
  docker :image, :rm, "--force", config.absolute_image
@@ -14,7 +14,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
14
14
  end
15
15
 
16
16
  def build_options
17
- [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile ]
17
+ [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_ssh ]
18
18
  end
19
19
 
20
20
  def build_context
@@ -24,7 +24,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
24
24
  def validate_image
25
25
  pipe \
26
26
  docker(:inspect, "-f", "'{{ .Config.Labels.service }}'", config.absolute_image),
27
- [:grep, "-x", config.service, "||", "(echo \"Image #{config.absolute_image} is missing the `service` label\" && exit 1)"]
27
+ any(
28
+ [:grep, "-x", config.service],
29
+ "(echo \"Image #{config.absolute_image} is missing the 'service' label\" && exit 1)"
30
+ )
28
31
  end
29
32
 
30
33
 
@@ -60,6 +63,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
60
63
  end
61
64
  end
62
65
 
66
+ def build_ssh
67
+ argumentize "--ssh", ssh if ssh.present?
68
+ end
69
+
63
70
  def builder_config
64
71
  config.builder
65
72
  end
@@ -10,7 +10,7 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
10
10
  def push
11
11
  docker :buildx, :build,
12
12
  "--push",
13
- "--platform", "linux/amd64,linux/arm64",
13
+ "--platform", platform_names,
14
14
  "--builder", builder_name,
15
15
  *build_options,
16
16
  build_context
@@ -26,4 +26,12 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
26
26
  def builder_name
27
27
  "kamal-#{config.service}-multiarch"
28
28
  end
29
+
30
+ def platform_names
31
+ if local_arch
32
+ "linux/#{local_arch}"
33
+ else
34
+ "linux/amd64,linux/arm64"
35
+ end
36
+ end
29
37
  end
@@ -1,7 +1,7 @@
1
1
  class Kamal::Commands::Docker < Kamal::Commands::Base
2
2
  # Install Docker using the https://github.com/docker/docker-install convenience script.
3
3
  def install
4
- pipe [ :curl, "-fsSL", "https://get.docker.com" ], :sh
4
+ pipe get_docker, :sh
5
5
  end
6
6
 
7
7
  # Checks the Docker client version. Fails if Docker is not installed.
@@ -18,4 +18,13 @@ class Kamal::Commands::Docker < Kamal::Commands::Base
18
18
  def superuser?
19
19
  [ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ]
20
20
  end
21
+
22
+ private
23
+ def get_docker
24
+ shell \
25
+ any \
26
+ [ :curl, "-fsSL", "https://get.docker.com" ],
27
+ [ :wget, "-O -", "https://get.docker.com" ],
28
+ [ :echo, "\"exit 1\"" ]
29
+ end
21
30
  end
@@ -13,10 +13,10 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
13
13
  "while read image tag; do docker rmi $tag; done"
14
14
  end
15
15
 
16
- def app_containers(keep_last: 5)
16
+ def app_containers(retain:)
17
17
  pipe \
18
18
  docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
19
- "tail -n +#{keep_last + 1}",
19
+ "tail -n +#{retain + 1}",
20
20
  "while read container_id; do docker rm $container_id; done"
21
21
  end
22
22
 
@@ -2,7 +2,10 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
2
2
  delegate :registry, to: :config
3
3
 
4
4
  def login
5
- docker :login, registry["server"], "-u", sensitive(lookup("username")), "-p", sensitive(lookup("password"))
5
+ docker :login,
6
+ registry["server"],
7
+ "-u", sensitive(Kamal::Utils.escape_shell_value(lookup("username"))),
8
+ "-p", sensitive(Kamal::Utils.escape_shell_value(lookup("password")))
6
9
  end
7
10
 
8
11
  def logout
@@ -1,7 +1,7 @@
1
1
  class Kamal::Commands::Traefik < Kamal::Commands::Base
2
2
  delegate :argumentize, :optionize, to: Kamal::Utils
3
3
 
4
- DEFAULT_IMAGE = "traefik:v2.9"
4
+ DEFAULT_IMAGE = "traefik:v2.10"
5
5
  CONTAINER_PORT = 80
6
6
  DEFAULT_ARGS = {
7
7
  'log.level' => 'DEBUG'
@@ -39,7 +39,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
39
39
  end
40
40
 
41
41
  def start_or_run
42
- combine start, run, by: "||"
42
+ any start, run
43
43
  end
44
44
 
45
45
  def info
@@ -8,7 +8,7 @@ class Kamal::Configuration::Accessory
8
8
  end
9
9
 
10
10
  def service_name
11
- "#{config.service}-#{name}"
11
+ specifics["service"] || "#{config.service}-#{name}"
12
12
  end
13
13
 
14
14
  def image
@@ -8,7 +8,7 @@ class Kamal::Configuration::Boot
8
8
  limit = @options["limit"]
9
9
 
10
10
  if limit.to_s.end_with?("%")
11
- @host_count * limit.to_i / 100
11
+ [@host_count * limit.to_i / 100, 1].max
12
12
  else
13
13
  limit
14
14
  end
@@ -81,6 +81,10 @@ class Kamal::Configuration::Builder
81
81
  end
82
82
  end
83
83
 
84
+ def ssh
85
+ @options["ssh"]
86
+ end
87
+
84
88
  private
85
89
  def valid?
86
90
  if @options["cache"] && @options["cache"]["type"]
@@ -3,9 +3,10 @@ class Kamal::Configuration::Role
3
3
  delegate :argumentize, :optionize, to: Kamal::Utils
4
4
 
5
5
  attr_accessor :name
6
+ alias to_s name
6
7
 
7
8
  def initialize(name, config:)
8
- @name, @config = name.inquiry, config
9
+ @name, @config = name.inquiry, config
9
10
  end
10
11
 
11
12
  def primary_host
@@ -36,6 +37,18 @@ class Kamal::Configuration::Role
36
37
  argumentize "--label", labels
37
38
  end
38
39
 
40
+ def logging_args
41
+ args = config.logging || {}
42
+ args.deep_merge!(specializations["logging"]) if specializations["logging"].present?
43
+
44
+ if args.any?
45
+ optionize({ "log-driver" => args["driver"] }.compact) +
46
+ argumentize("--log-opt", args["options"])
47
+ else
48
+ config.logging_args
49
+ end
50
+ end
51
+
39
52
 
40
53
  def env
41
54
  if config.env && config.env["secret"]
@@ -101,7 +114,7 @@ class Kamal::Configuration::Role
101
114
  end
102
115
 
103
116
  def primary?
104
- @config.primary_role == name
117
+ self == @config.primary_role
105
118
  end
106
119
 
107
120
 
@@ -6,7 +6,7 @@ require "erb"
6
6
  require "net/ssh/proxy/jump"
7
7
 
8
8
  class Kamal::Configuration
9
- delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
9
+ delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true
10
10
  delegate :argumentize, :optionize, to: Kamal::Utils
11
11
 
12
12
  attr_reader :destination, :raw_config
@@ -92,7 +92,19 @@ class Kamal::Configuration
92
92
  end
93
93
 
94
94
  def primary_host
95
- role(primary_role)&.primary_host
95
+ primary_role&.primary_host
96
+ end
97
+
98
+ def primary_role_name
99
+ raw_config.primary_role || "web"
100
+ end
101
+
102
+ def primary_role
103
+ role(primary_role_name)
104
+ end
105
+
106
+ def allow_empty_roles?
107
+ raw_config.allow_empty_roles
96
108
  end
97
109
 
98
110
  def traefik_roles
@@ -127,6 +139,10 @@ class Kamal::Configuration
127
139
  raw_config.require_destination
128
140
  end
129
141
 
142
+ def retain_containers
143
+ raw_config.retain_containers || 5
144
+ end
145
+
130
146
 
131
147
  def volume_args
132
148
  if raw_config.volumes.present?
@@ -137,9 +153,9 @@ class Kamal::Configuration
137
153
  end
138
154
 
139
155
  def logging_args
140
- if raw_config.logging.present?
141
- optionize({ "log-driver" => raw_config.logging["driver"] }.compact) +
142
- argumentize("--log-opt", raw_config.logging["options"])
156
+ if logging.present?
157
+ optionize({ "log-driver" => logging["driver"] }.compact) +
158
+ argumentize("--log-opt", logging["options"])
143
159
  else
144
160
  argumentize("--log-opt", { "max-size" => "10m" })
145
161
  end
@@ -208,17 +224,9 @@ class Kamal::Configuration
208
224
  raw_config.asset_path
209
225
  end
210
226
 
211
- def primary_role
212
- raw_config.primary_role || "web"
213
- end
214
-
215
- def allow_empty_roles?
216
- raw_config.allow_empty_roles
217
- end
218
-
219
227
 
220
228
  def valid?
221
- ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version
229
+ ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid && ensure_valid_service_name
222
230
  end
223
231
 
224
232
  def to_h
@@ -264,12 +272,12 @@ class Kamal::Configuration
264
272
  raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
265
273
  end
266
274
 
267
- unless role_names.include?(primary_role)
268
- raise ArgumentError, "The primary_role #{primary_role} isn't defined"
275
+ unless role_names.include?(primary_role_name)
276
+ raise ArgumentError, "The primary_role #{primary_role_name} isn't defined"
269
277
  end
270
278
 
271
- if role(primary_role).hosts.empty?
272
- raise ArgumentError, "No servers specified for the #{primary_role} primary_role"
279
+ if primary_role.hosts.empty?
280
+ raise ArgumentError, "No servers specified for the #{primary_role.name} primary_role"
273
281
  end
274
282
 
275
283
  unless allow_empty_roles?
@@ -283,6 +291,12 @@ class Kamal::Configuration
283
291
  true
284
292
  end
285
293
 
294
+ def ensure_valid_service_name
295
+ raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/
296
+
297
+ true
298
+ end
299
+
286
300
  def ensure_valid_kamal_version
287
301
  if minimum_version && Gem::Version.new(minimum_version) > Gem::Version.new(Kamal::VERSION)
288
302
  raise ArgumentError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
@@ -291,6 +305,12 @@ class Kamal::Configuration
291
305
  true
292
306
  end
293
307
 
308
+ def ensure_retain_containers_valid
309
+ raise ArgumentError, "Must retain at least 1 container" if retain_containers < 1
310
+
311
+ true
312
+ end
313
+
294
314
 
295
315
  def role_names
296
316
  raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
@@ -1,5 +1,6 @@
1
1
  require "sshkit"
2
2
  require "sshkit/dsl"
3
+ require "net/scp"
3
4
  require "active_support/core_ext/hash/deep_merge"
4
5
  require "json"
5
6
 
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "1.3.1"
2
+ VERSION = "1.4.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-10 00:00:00.000000000 Z
11
+ date: 2024-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -217,6 +217,7 @@ files:
217
217
  - lib/kamal/cli/registry.rb
218
218
  - lib/kamal/cli/server.rb
219
219
  - lib/kamal/cli/templates/deploy.yml
220
+ - lib/kamal/cli/templates/sample_hooks/docker-setup.sample
220
221
  - lib/kamal/cli/templates/sample_hooks/post-deploy.sample
221
222
  - lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample
222
223
  - lib/kamal/cli/templates/sample_hooks/pre-build.sample
@@ -286,7 +287,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
286
287
  - !ruby/object:Gem::Version
287
288
  version: '0'
288
289
  requirements: []
289
- rubygems_version: 3.5.1
290
+ rubygems_version: 3.5.6
290
291
  signing_key:
291
292
  specification_version: 4
292
293
  summary: Deploy web apps in containers to servers running Docker with zero downtime.