kamal 0.16.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -1013
  3. data/lib/kamal/cli/app.rb +38 -11
  4. data/lib/kamal/cli/base.rb +8 -0
  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 +14 -3
  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 +17 -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 +3 -1
  27. data/lib/kamal/commands/healthcheck.rb +15 -12
  28. data/lib/kamal/commands/lock.rb +2 -2
  29. data/lib/kamal/commands/prune.rb +11 -3
  30. data/lib/kamal/commands/server.rb +5 -0
  31. data/lib/kamal/commands/traefik.rb +26 -8
  32. data/lib/kamal/configuration/accessory.rb +14 -2
  33. data/lib/kamal/configuration/role.rb +112 -19
  34. data/lib/kamal/configuration/ssh.rb +1 -1
  35. data/lib/kamal/configuration/volume.rb +22 -0
  36. data/lib/kamal/configuration.rb +79 -43
  37. data/lib/kamal/env_file.rb +41 -0
  38. data/lib/kamal/git.rb +19 -0
  39. data/lib/kamal/utils.rb +0 -39
  40. data/lib/kamal/version.rb +1 -1
  41. metadata +16 -5
  42. data/lib/kamal/utils/healthcheck_poller.rb +0 -39
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)) }
33
48
 
34
- Kamal::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
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
35
57
 
36
- execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
58
+ execute *app.stop(version: old_version), raise_on_non_zero_exit: false
59
+
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.primary_host.roles.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]
@@ -79,6 +79,8 @@ module Kamal::Cli
79
79
 
80
80
  run_hook "pre-connect"
81
81
 
82
+ ensure_run_directory
83
+
82
84
  acquire_lock
83
85
 
84
86
  begin
@@ -167,5 +169,11 @@ module Kamal::Cli
167
169
  def first_invocation
168
170
  instance_variable_get("@_invocations").first
169
171
  end
172
+
173
+ def ensure_run_directory
174
+ on(KAMAL.hosts) do
175
+ execute(*KAMAL.server.ensure_run_directory)
176
+ end
177
+ end
170
178
  end
171
179
  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
@@ -175,6 +180,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
175
180
  end
176
181
 
177
182
  File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
183
+
184
+ load_envs # reload new file
185
+ invoke "kamal:cli:env:push", options
178
186
  end
179
187
 
180
188
  desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
@@ -204,6 +212,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
204
212
  desc "build", "Build application image"
205
213
  subcommand "build", Kamal::Cli::Build
206
214
 
215
+ desc "env", "Manage environment files"
216
+ subcommand "env", Kamal::Cli::Env
217
+
207
218
  desc "healthcheck", "Healthcheck application"
208
219
  subcommand "healthcheck", Kamal::Cli::Healthcheck
209
220
 
@@ -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
@@ -0,0 +1,23 @@
1
+ module Kamal::Commands::App::Containers
2
+ def list_containers
3
+ docker :container, :ls, "--all", *filter_args
4
+ end
5
+
6
+ def list_container_names
7
+ [ *list_containers, "--format", "'{{ .Names }}'" ]
8
+ end
9
+
10
+ def remove_container(version:)
11
+ pipe \
12
+ container_id_for(container_name: container_name(version)),
13
+ xargs(docker(:container, :rm))
14
+ end
15
+
16
+ def rename_container(version:, new_version:)
17
+ docker :rename, container_name(version), container_name(new_version)
18
+ end
19
+
20
+ def remove_containers
21
+ docker :container, :prune, "--force", *filter_args
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ module Kamal::Commands::App::Cord
2
+ def cord(version:)
3
+ pipe \
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}'"]
6
+ end
7
+
8
+ def tie_cord(cord)
9
+ create_empty_file(cord)
10
+ end
11
+
12
+ def cut_cord(cord)
13
+ remove_directory(cord)
14
+ end
15
+
16
+ private
17
+ def create_empty_file(file)
18
+ chain \
19
+ make_directory_for(file),
20
+ [:touch, file]
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ module Kamal::Commands::App::Execution
2
+ def execute_in_existing_container(*command, interactive: false)
3
+ docker :exec,
4
+ ("-it" if interactive),
5
+ container_name,
6
+ *command
7
+ end
8
+
9
+ def execute_in_new_container(*command, interactive: false)
10
+ docker :run,
11
+ ("-it" if interactive),
12
+ "--rm",
13
+ *role_config&.env_args,
14
+ *config.volume_args,
15
+ *role_config&.option_args,
16
+ config.absolute_image,
17
+ *command
18
+ end
19
+
20
+ def execute_in_existing_container_over_ssh(*command, host:)
21
+ run_over_ssh execute_in_existing_container(*command, interactive: true), host: host
22
+ end
23
+
24
+ def execute_in_new_container_over_ssh(*command, host:)
25
+ run_over_ssh execute_in_new_container(*command, interactive: true), host: host
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ module Kamal::Commands::App::Images
2
+ def list_images
3
+ docker :image, :ls, config.repository
4
+ end
5
+
6
+ def remove_images
7
+ docker :image, :prune, "--all", "--force", *filter_args
8
+ end
9
+
10
+ def tag_current_image_as_latest
11
+ docker :tag, config.absolute_image, config.latest_image
12
+ end
13
+ end