kamal 0.16.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/kamal/cli/app.rb +44 -13
  4. data/lib/kamal/cli/base.rb +15 -2
  5. data/lib/kamal/cli/build.rb +18 -1
  6. data/lib/kamal/cli/env.rb +56 -0
  7. data/lib/kamal/cli/healthcheck/poller.rb +64 -0
  8. data/lib/kamal/cli/healthcheck.rb +2 -2
  9. data/lib/kamal/cli/lock.rb +12 -3
  10. data/lib/kamal/cli/main.rb +18 -4
  11. data/lib/kamal/cli/prune.rb +3 -2
  12. data/lib/kamal/cli/server.rb +2 -0
  13. data/lib/kamal/cli/templates/deploy.yml +12 -1
  14. data/lib/kamal/commander.rb +21 -8
  15. data/lib/kamal/commands/accessory.rb +8 -8
  16. data/lib/kamal/commands/app/assets.rb +51 -0
  17. data/lib/kamal/commands/app/containers.rb +23 -0
  18. data/lib/kamal/commands/app/cord.rb +22 -0
  19. data/lib/kamal/commands/app/execution.rb +27 -0
  20. data/lib/kamal/commands/app/images.rb +13 -0
  21. data/lib/kamal/commands/app/logging.rb +18 -0
  22. data/lib/kamal/commands/app.rb +18 -91
  23. data/lib/kamal/commands/auditor.rb +3 -1
  24. data/lib/kamal/commands/base.rb +12 -0
  25. data/lib/kamal/commands/builder/base.rb +6 -0
  26. data/lib/kamal/commands/builder.rb +1 -1
  27. data/lib/kamal/commands/docker.rb +1 -1
  28. data/lib/kamal/commands/healthcheck.rb +15 -12
  29. data/lib/kamal/commands/lock.rb +2 -2
  30. data/lib/kamal/commands/prune.rb +11 -3
  31. data/lib/kamal/commands/server.rb +5 -0
  32. data/lib/kamal/commands/traefik.rb +21 -7
  33. data/lib/kamal/configuration/accessory.rb +14 -2
  34. data/lib/kamal/configuration/role.rb +112 -19
  35. data/lib/kamal/configuration/ssh.rb +1 -1
  36. data/lib/kamal/configuration/volume.rb +22 -0
  37. data/lib/kamal/configuration.rb +73 -44
  38. data/lib/kamal/env_file.rb +41 -0
  39. data/lib/kamal/git.rb +19 -0
  40. data/lib/kamal/utils/sensitive.rb +1 -0
  41. data/lib/kamal/utils.rb +0 -39
  42. data/lib/kamal/version.rb +1 -1
  43. metadata +15 -4
  44. data/lib/kamal/utils/healthcheck_poller.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45021fa39194b2d23787149f4dfc15b62a5b657488aa6001c8ec0a826b916753
4
- data.tar.gz: 5464df4ac1066064668cb302fd7c41f8c9d3640460b3b6b7c5d612ace997e46c
3
+ metadata.gz: 2470f58e93ad7f181c9a422a0eef08c741190cb897c962b83aeda9effd4862f5
4
+ data.tar.gz: df8ddd4bf5dcf6687fab747bcfb11689dbe58b846ab6e392e99694ce8bfdff88
5
5
  SHA512:
6
- metadata.gz: 3942dc1ecdfb9eb3634c8d1e26b8efd5b5aaa9ca2cd9ff2fe78fa521c99c8cffd3dc39a4754ffca2e9042fb7d7d33355b3b1abc2b201a94b984417d55b8962d4
7
- data.tar.gz: d3c5a0971f5586b678ea96bdfbb0ff49b20a614200e58433c77597c051a9e0b15ec8bf05f143afe56b12bbcd7816d95f5e64597335809888e457641d0d145946
6
+ metadata.gz: 7cef4c1ea22ffe3a0f80dbd2985cb3f3941e70dfcb35509a4ec7210124e517b0b7336ddd571370c32868d9d99442b9cc1186eb6a0cdefaa5db1b0475f117faae
7
+ data.tar.gz: 1f6d43624775bb9f1320692c3b4e5d061528b92763886f963cf184978e2405e478c5c6a55347316c69416f512bb5f70c970999d56e2e5c6937b6b83613338106
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Kamal: Deploy web apps anywhere
2
2
 
3
- From bare metal to cloud VMs using Docker, deploy web apps anywhere with zero downtime. Kamal uses the dynamic reverse-proxy Traefik to hold requests, while the new app container is started and the old one is stopped working seamlessly across multiple hosts, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker.
3
+ From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal has the dynamic reverse-proxy Traefik hold requests while a new app container is started and the old one is stopped. Works seamlessly across multiple hosts, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker.
4
4
 
5
5
  ➡️ See [kamal-deploy.org](https://kamal-deploy.org) for documentation on [installation](https://kamal-deploy.org/docs/installation), [configuration](https://kamal-deploy.org/docs/configuration), and [commands](https://kamal-deploy.org/docs/commands).
6
6
 
data/lib/kamal/cli/app.rb CHANGED
@@ -9,31 +9,56 @@ class Kamal::Cli::App < Kamal::Cli::Base
9
9
 
10
10
  on(KAMAL.hosts) do
11
11
  execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
12
- execute *KAMAL.app.tag_current_as_latest
12
+ execute *KAMAL.app.tag_current_image_as_latest
13
+
14
+ KAMAL.roles_on(host).each do |role|
15
+ app = KAMAL.app(role: role)
16
+ role_config = KAMAL.config.role(role)
17
+
18
+ if role_config.assets?
19
+ execute *app.extract_assets
20
+ old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
21
+ execute *app.sync_asset_volumes(old_version: old_version)
22
+ end
23
+ end
13
24
  end
14
25
 
15
26
  on(KAMAL.hosts, **KAMAL.boot_strategy) do |host|
16
- roles = KAMAL.roles_on(host)
17
-
18
- roles.each do |role|
27
+ KAMAL.roles_on(host).each do |role|
19
28
  app = KAMAL.app(role: role)
20
29
  auditor = KAMAL.auditor(role: role)
30
+ role_config = KAMAL.config.role(role)
21
31
 
22
- if capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).present?
32
+ if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
23
33
  tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
24
34
  info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
25
35
  execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
26
36
  execute *app.rename_container(version: version, new_version: tmp_version)
27
37
  end
28
38
 
39
+ old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
40
+
41
+ execute *app.tie_cord(role_config.cord_host_file) if role_config.uses_cord?
42
+
29
43
  execute *auditor.record("Booted app version #{version}"), verbosity: :debug
30
44
 
31
- old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
32
- execute *app.start_or_run(hostname: "#{host}-#{SecureRandom.hex(6)}")
45
+ execute *app.run(hostname: "#{host}-#{SecureRandom.hex(6)}")
46
+
47
+ Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
48
+
49
+ if old_version.present?
50
+ if role_config.uses_cord?
51
+ cord = capture_with_info(*app.cord(version: old_version), raise_on_non_zero_exit: false).strip
52
+ if cord.present?
53
+ execute *app.cut_cord(cord)
54
+ Kamal::Cli::Healthcheck::Poller.wait_for_unhealthy(pause_after_ready: true) { capture_with_info(*app.status(version: old_version)) }
55
+ end
56
+ end
33
57
 
34
- Kamal::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
58
+ execute *app.stop(version: old_version), raise_on_non_zero_exit: false
35
59
 
36
- execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
60
+ execute *app.clean_up_assets if role_config.assets?
61
+ end
37
62
  end
38
63
  end
39
64
  end
@@ -90,14 +115,16 @@ class Kamal::Cli::App < Kamal::Cli::Base
90
115
  say "Get current version of running container...", :magenta unless options[:version]
91
116
  using_version(options[:version] || current_running_version) do |version|
92
117
  say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
93
- run_locally { exec KAMAL.app(role: "web").execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host) }
118
+ run_locally { exec KAMAL.app(role: KAMAL.primary_role).execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host) }
94
119
  end
95
120
 
96
121
  when options[:interactive]
97
122
  say "Get most recent version available as an image...", :magenta unless options[:version]
98
123
  using_version(version_or_latest) do |version|
99
124
  say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
100
- run_locally { exec KAMAL.app(role: KAMAL.roles_on(KAMAL.primary_host).first).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host) }
125
+ run_locally do
126
+ exec KAMAL.app(role: KAMAL.primary_role).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host)
127
+ end
101
128
  end
102
129
 
103
130
  when options[:reuse]
@@ -120,8 +147,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
120
147
  using_version(version_or_latest) do |version|
121
148
  say "Launching command with version #{version} from new container...", :magenta
122
149
  on(KAMAL.hosts) do |host|
123
- execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
124
- puts_by_host host, capture_with_info(*KAMAL.app.execute_in_new_container(cmd))
150
+ roles = KAMAL.roles_on(host)
151
+
152
+ roles.each do |role|
153
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
154
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_new_container(cmd))
155
+ end
125
156
  end
126
157
  end
127
158
  end
@@ -24,6 +24,7 @@ module Kamal::Cli
24
24
 
25
25
  def initialize(*)
26
26
  super
27
+ @original_env = ENV.to_h.dup
27
28
  load_envs
28
29
  initialize_commander(options_with_subcommand_class_options)
29
30
  end
@@ -37,6 +38,12 @@ module Kamal::Cli
37
38
  end
38
39
  end
39
40
 
41
+ def reload_envs
42
+ ENV.clear
43
+ ENV.update(@original_env)
44
+ load_envs
45
+ end
46
+
40
47
  def options_with_subcommand_class_options
41
48
  options.merge(@_initializer.last[:class_options] || {})
42
49
  end
@@ -75,10 +82,10 @@ module Kamal::Cli
75
82
  def mutating
76
83
  return yield if KAMAL.holding_lock?
77
84
 
78
- KAMAL.config.ensure_env_available
79
-
80
85
  run_hook "pre-connect"
81
86
 
87
+ ensure_run_directory
88
+
82
89
  acquire_lock
83
90
 
84
91
  begin
@@ -167,5 +174,11 @@ module Kamal::Cli
167
174
  def first_invocation
168
175
  instance_variable_get("@_invocations").first
169
176
  end
177
+
178
+ def ensure_run_directory
179
+ on(KAMAL.hosts) do
180
+ execute(*KAMAL.server.ensure_run_directory)
181
+ end
182
+ end
170
183
  end
171
184
  end
@@ -1,3 +1,5 @@
1
+ require "uri"
2
+
1
3
  class Kamal::Cli::Build < Kamal::Cli::Base
2
4
  class BuildError < StandardError; end
3
5
 
@@ -17,7 +19,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
17
19
  verify_local_dependencies
18
20
  run_hook "pre-build"
19
21
 
20
- if (uncommitted_changes = Kamal::Utils.uncommitted_changes).present?
22
+ if (uncommitted_changes = Kamal::Git.uncommitted_changes).present?
21
23
  say "The following paths have uncommitted changes:\n #{uncommitted_changes}", :yellow
22
24
  end
23
25
 
@@ -48,6 +50,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
48
50
  execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
49
51
  execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
50
52
  execute *KAMAL.builder.pull
53
+ execute *KAMAL.builder.validate_image
51
54
  end
52
55
  end
53
56
  end
@@ -55,6 +58,10 @@ class Kamal::Cli::Build < Kamal::Cli::Base
55
58
  desc "create", "Create a build setup"
56
59
  def create
57
60
  mutating do
61
+ if (remote_host = KAMAL.config.builder.remote_host)
62
+ connect_to_remote_host(remote_host)
63
+ end
64
+
58
65
  run_locally do
59
66
  begin
60
67
  debug "Using builder: #{KAMAL.builder.name}"
@@ -103,4 +110,14 @@ class Kamal::Cli::Build < Kamal::Cli::Base
103
110
  end
104
111
  end
105
112
  end
113
+
114
+ def connect_to_remote_host(remote_host)
115
+ remote_uri = URI.parse(remote_host)
116
+ if remote_uri.scheme == "ssh"
117
+ options = { user: remote_uri.user, port: remote_uri.port }.compact
118
+ on(remote_uri.host, options) do
119
+ execute "true"
120
+ end
121
+ end
122
+ end
106
123
  end
@@ -0,0 +1,56 @@
1
+ require "tempfile"
2
+
3
+ class Kamal::Cli::Env < Kamal::Cli::Base
4
+ desc "push", "Push the env file to the remote hosts"
5
+ def push
6
+ mutating do
7
+ on(KAMAL.hosts) do
8
+ execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug
9
+
10
+ KAMAL.roles_on(host).each do |role|
11
+ role_config = KAMAL.config.role(role)
12
+ execute *KAMAL.app(role: role).make_env_directory
13
+ upload! StringIO.new(role_config.env_file), role_config.host_env_file_path, mode: 400
14
+ end
15
+ end
16
+
17
+ on(KAMAL.traefik_hosts) do
18
+ execute *KAMAL.traefik.make_env_directory
19
+ upload! StringIO.new(KAMAL.traefik.env_file), KAMAL.traefik.host_env_file_path, mode: 400
20
+ end
21
+
22
+ on(KAMAL.accessory_hosts) do
23
+ KAMAL.accessories_on(host).each do |accessory|
24
+ accessory_config = KAMAL.config.accessory(accessory)
25
+ execute *KAMAL.accessory(accessory).make_env_directory
26
+ upload! StringIO.new(accessory_config.env_file), accessory_config.host_env_file_path, mode: 400
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ desc "delete", "Delete the env file from the remote hosts"
33
+ def delete
34
+ mutating do
35
+ on(KAMAL.hosts) do
36
+ execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug
37
+
38
+ KAMAL.roles_on(host).each do |role|
39
+ role_config = KAMAL.config.role(role)
40
+ execute *KAMAL.app(role: role).remove_env_file
41
+ end
42
+ end
43
+
44
+ on(KAMAL.traefik_hosts) do
45
+ execute *KAMAL.traefik.remove_env_file
46
+ end
47
+
48
+ on(KAMAL.accessory_hosts) do
49
+ KAMAL.accessories_on(host).each do |accessory|
50
+ accessory_config = KAMAL.config.accessory(accessory)
51
+ execute *KAMAL.accessory(accessory).remove_env_file
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,64 @@
1
+ module Kamal::Cli::Healthcheck::Poller
2
+ extend self
3
+
4
+ TRAEFIK_UPDATE_DELAY = 5
5
+
6
+ class HealthcheckError < StandardError; end
7
+
8
+ def wait_for_healthy(pause_after_ready: false, &block)
9
+ attempt = 1
10
+ max_attempts = KAMAL.config.healthcheck["max_attempts"]
11
+
12
+ begin
13
+ case status = block.call
14
+ when "healthy"
15
+ sleep TRAEFIK_UPDATE_DELAY if pause_after_ready
16
+ when "running" # No health check configured
17
+ sleep KAMAL.config.readiness_delay if pause_after_ready
18
+ else
19
+ raise HealthcheckError, "container not ready (#{status})"
20
+ end
21
+ rescue HealthcheckError => e
22
+ if attempt <= max_attempts
23
+ info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
24
+ sleep attempt
25
+ attempt += 1
26
+ retry
27
+ else
28
+ raise
29
+ end
30
+ end
31
+
32
+ info "Container is healthy!"
33
+ end
34
+
35
+ def wait_for_unhealthy(pause_after_ready: false, &block)
36
+ attempt = 1
37
+ max_attempts = KAMAL.config.healthcheck["max_attempts"]
38
+
39
+ begin
40
+ case status = block.call
41
+ when "unhealthy"
42
+ sleep TRAEFIK_UPDATE_DELAY if pause_after_ready
43
+ else
44
+ raise HealthcheckError, "container not unhealthy (#{status})"
45
+ end
46
+ rescue HealthcheckError => e
47
+ if attempt <= max_attempts
48
+ info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
49
+ sleep attempt
50
+ attempt += 1
51
+ retry
52
+ else
53
+ raise
54
+ end
55
+ end
56
+
57
+ info "Container is unhealthy!"
58
+ end
59
+
60
+ private
61
+ def info(message)
62
+ SSHKit.config.output.info(message)
63
+ end
64
+ end
@@ -6,8 +6,8 @@ class Kamal::Cli::Healthcheck < Kamal::Cli::Base
6
6
  on(KAMAL.primary_host) do
7
7
  begin
8
8
  execute *KAMAL.healthcheck.run
9
- Kamal::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*KAMAL.healthcheck.status) }
10
- rescue Kamal::Utils::HealthcheckPoller::HealthcheckError => e
9
+ Poller.wait_for_healthy { capture_with_info(*KAMAL.healthcheck.status) }
10
+ rescue Poller::HealthcheckError => e
11
11
  error capture_with_info(*KAMAL.healthcheck.logs)
12
12
  error capture_with_pretty_json(*KAMAL.healthcheck.container_health_log)
13
13
  raise
@@ -2,7 +2,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
2
2
  desc "status", "Report lock status"
3
3
  def status
4
4
  handle_missing_lock do
5
- on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
5
+ on(KAMAL.primary_host) do
6
+ execute *KAMAL.server.ensure_run_directory
7
+ puts capture_with_debug(*KAMAL.lock.status)
8
+ end
6
9
  end
7
10
  end
8
11
 
@@ -11,7 +14,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
11
14
  def acquire
12
15
  message = options[:message]
13
16
  raise_if_locked do
14
- on(KAMAL.primary_host) { execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug }
17
+ on(KAMAL.primary_host) do
18
+ execute *KAMAL.server.ensure_run_directory
19
+ execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
20
+ end
15
21
  say "Acquired the deploy lock"
16
22
  end
17
23
  end
@@ -19,7 +25,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
19
25
  desc "release", "Release the deploy lock"
20
26
  def release
21
27
  handle_missing_lock do
22
- on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
28
+ on(KAMAL.primary_host) do
29
+ execute *KAMAL.server.ensure_run_directory
30
+ execute *KAMAL.lock.release, verbosity: :debug
31
+ end
23
32
  say "Released the deploy lock"
24
33
  end
25
34
  end
@@ -1,9 +1,14 @@
1
1
  class Kamal::Cli::Main < Kamal::Cli::Base
2
- desc "setup", "Setup all accessories and deploy app to servers"
2
+ desc "setup", "Setup all accessories, push the env, and deploy app to servers"
3
3
  def setup
4
4
  print_runtime do
5
5
  mutating do
6
+ say "Ensure Docker is installed...", :magenta
6
7
  invoke "kamal:cli:server:bootstrap"
8
+
9
+ say "Push env files...", :magenta
10
+ invoke "kamal:cli:env:push"
11
+
7
12
  invoke "kamal:cli:accessory:boot", [ "all" ]
8
13
  deploy
9
14
  end
@@ -37,7 +42,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
37
42
  invoke "kamal:cli:healthcheck:perform", [], invoke_options
38
43
 
39
44
  say "Detect stale containers...", :magenta
40
- invoke "kamal:cli:app:stale_containers", [], invoke_options
45
+ invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
41
46
 
42
47
  invoke "kamal:cli:app:boot", [], invoke_options
43
48
 
@@ -70,7 +75,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
70
75
  invoke "kamal:cli:healthcheck:perform", [], invoke_options
71
76
 
72
77
  say "Detect stale containers...", :magenta
73
- invoke "kamal:cli:app:stale_containers", [], invoke_options
78
+ invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
74
79
 
75
80
  invoke "kamal:cli:app:boot", [], invoke_options
76
81
  end
@@ -165,6 +170,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
165
170
  end
166
171
 
167
172
  desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
173
+ option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip .env file push"
168
174
  def envify
169
175
  if destination = options[:destination]
170
176
  env_template_path = ".env.#{destination}.erb"
@@ -174,7 +180,12 @@ class Kamal::Cli::Main < Kamal::Cli::Base
174
180
  env_path = ".env"
175
181
  end
176
182
 
177
- File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
183
+ File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
184
+
185
+ unless options[:skip_push]
186
+ reload_envs
187
+ invoke "kamal:cli:env:push", options
188
+ end
178
189
  end
179
190
 
180
191
  desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
@@ -204,6 +215,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
204
215
  desc "build", "Build application image"
205
216
  subcommand "build", Kamal::Cli::Build
206
217
 
218
+ desc "env", "Manage environment files"
219
+ subcommand "env", Kamal::Cli::Env
220
+
207
221
  desc "healthcheck", "Healthcheck application"
208
222
  subcommand "healthcheck", Kamal::Cli::Healthcheck
209
223
 
@@ -7,7 +7,7 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
7
7
  end
8
8
  end
9
9
 
10
- desc "images", "Prune dangling images"
10
+ desc "images", "Prune unused images"
11
11
  def images
12
12
  mutating do
13
13
  on(KAMAL.hosts) do
@@ -23,7 +23,8 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
23
23
  mutating do
24
24
  on(KAMAL.hosts) do
25
25
  execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
26
- execute *KAMAL.prune.containers
26
+ execute *KAMAL.prune.app_containers
27
+ execute *KAMAL.prune.healthcheck_containers
27
28
  end
28
29
  end
29
30
  end
@@ -12,6 +12,8 @@ class Kamal::Cli::Server < Kamal::Cli::Base
12
12
  missing << host
13
13
  end
14
14
  end
15
+
16
+ execute(*KAMAL.server.ensure_run_directory)
15
17
  end
16
18
 
17
19
  if missing.any?
@@ -19,6 +19,7 @@ registry:
19
19
  - KAMAL_REGISTRY_PASSWORD
20
20
 
21
21
  # Inject ENV variables into containers (secrets come from .env).
22
+ # Remember to run `kamal env push` after making changes!
22
23
  # env:
23
24
  # clear:
24
25
  # DB_HOST: 192.168.0.2
@@ -52,7 +53,7 @@ registry:
52
53
  # - MYSQL_ROOT_PASSWORD
53
54
  # files:
54
55
  # - config/mysql/production.cnf:/etc/mysql/my.cnf
55
- # - db/production.sql.erb:/docker-entrypoint-initdb.d/setup.sql
56
+ # - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
56
57
  # directories:
57
58
  # - data:/var/lib/mysql
58
59
  # redis:
@@ -72,3 +73,13 @@ registry:
72
73
  # healthcheck:
73
74
  # path: /healthz
74
75
  # port: 4000
76
+
77
+ # Bridge fingerprinted assets, like JS and CSS, between versions to avoid
78
+ # hitting 404 on in-flight requests. Combines all files from new and old
79
+ # version inside the asset_path.
80
+ # asset_path: /rails/public/assets
81
+
82
+ # Configure rolling deploys by setting a wait time between batches of restarts.
83
+ # boot:
84
+ # limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
85
+ # wait: 2
@@ -39,6 +39,10 @@ class Kamal::Commander
39
39
  specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_web_host
40
40
  end
41
41
 
42
+ def primary_role
43
+ roles_on(primary_host).first
44
+ end
45
+
42
46
  def roles
43
47
  (specific_roles || config.roles).select do |role|
44
48
  ((specific_hosts || config.all_hosts) & role.hosts).any?
@@ -51,14 +55,6 @@ class Kamal::Commander
51
55
  end
52
56
  end
53
57
 
54
- def boot_strategy
55
- if config.boot.limit.present?
56
- { in: :groups, limit: config.boot.limit, wait: config.boot.wait }
57
- else
58
- {}
59
- end
60
- end
61
-
62
58
  def roles_on(host)
63
59
  roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
64
60
  end
@@ -75,6 +71,10 @@ class Kamal::Commander
75
71
  config.accessories&.collect(&:name) || []
76
72
  end
77
73
 
74
+ def accessories_on(host)
75
+ config.accessories.select { |accessory| accessory.hosts.include?(host.to_s) }.map(&:name)
76
+ end
77
+
78
78
 
79
79
  def app(role: nil)
80
80
  Kamal::Commands::App.new(config, role: role)
@@ -116,10 +116,15 @@ class Kamal::Commander
116
116
  @registry ||= Kamal::Commands::Registry.new(config)
117
117
  end
118
118
 
119
+ def server
120
+ @server ||= Kamal::Commands::Server.new(config)
121
+ end
122
+
119
123
  def traefik
120
124
  @traefik ||= Kamal::Commands::Traefik.new(config)
121
125
  end
122
126
 
127
+
123
128
  def with_verbosity(level)
124
129
  old_level = self.verbosity
125
130
 
@@ -132,6 +137,14 @@ class Kamal::Commander
132
137
  SSHKit.config.output_verbosity = old_level
133
138
  end
134
139
 
140
+ def boot_strategy
141
+ if config.boot.limit.present?
142
+ { in: :groups, limit: config.boot.limit, wait: config.boot.wait }
143
+ else
144
+ {}
145
+ end
146
+ end
147
+
135
148
  def holding_lock?
136
149
  self.holding_lock
137
150
  end
@@ -86,14 +86,6 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
86
86
  end
87
87
  end
88
88
 
89
- def make_directory_for(remote_file)
90
- make_directory Pathname.new(remote_file).dirname.to_s
91
- end
92
-
93
- def make_directory(path)
94
- [ :mkdir, "-p", path ]
95
- end
96
-
97
89
  def remove_service_directory
98
90
  [ :rm, "-rf", service_name ]
99
91
  end
@@ -106,6 +98,14 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
106
98
  docker :image, :rm, "--force", image
107
99
  end
108
100
 
101
+ def make_env_directory
102
+ make_directory accessory_config.host_env_directory
103
+ end
104
+
105
+ def remove_env_file
106
+ [:rm, "-f", accessory_config.host_env_file_path]
107
+ end
108
+
109
109
  private
110
110
  def service_filter
111
111
  [ "--filter", "label=service=#{service_name}" ]
@@ -0,0 +1,51 @@
1
+ module Kamal::Commands::App::Assets
2
+ def extract_assets
3
+ asset_container = "#{role_config.container_prefix}-assets"
4
+
5
+ combine \
6
+ make_directory(role_config.asset_extracted_path),
7
+ [*docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true"],
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),
10
+ docker(:stop, "-t 1", asset_container),
11
+ by: "&&"
12
+ end
13
+
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
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
18
+ end
19
+
20
+ commands = [make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path)]
21
+
22
+ if old_version.present?
23
+ commands << copy_contents(new_extracted_path, old_volume_path, continue_on_error: true)
24
+ commands << copy_contents(old_extracted_path, new_volume_path, continue_on_error: true)
25
+ end
26
+
27
+ chain *commands
28
+ end
29
+
30
+ def clean_up_assets
31
+ chain \
32
+ find_and_remove_older_siblings(role_config.asset_extracted_path),
33
+ find_and_remove_older_siblings(role_config.asset_volume_path)
34
+ end
35
+
36
+ private
37
+ def find_and_remove_older_siblings(path)
38
+ [
39
+ :find,
40
+ Pathname.new(path).dirname.to_s,
41
+ "-maxdepth 1",
42
+ "-name", "'#{role_config.container_prefix}-*'",
43
+ "!", "-name", Pathname.new(path).basename.to_s,
44
+ "-exec rm -rf \"{}\" +"
45
+ ]
46
+ end
47
+
48
+ def copy_contents(source, destination, continue_on_error: false)
49
+ [ :cp, "-rnT", "#{source}", destination, *("|| true" if continue_on_error)]
50
+ end
51
+ end