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 +4 -4
- data/lib/kamal/cli/accessory.rb +35 -27
- data/lib/kamal/cli/app.rb +9 -10
- data/lib/kamal/cli/base.rb +2 -1
- data/lib/kamal/cli/env.rb +1 -3
- data/lib/kamal/cli/main.rb +6 -3
- data/lib/kamal/cli/prune.rb +6 -2
- data/lib/kamal/cli/server.rb +3 -1
- data/lib/kamal/cli/templates/deploy.yml +4 -0
- data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +7 -0
- data/lib/kamal/cli/traefik.rb +2 -2
- data/lib/kamal/commander.rb +2 -2
- data/lib/kamal/commands/app/assets.rb +8 -8
- data/lib/kamal/commands/app/cord.rb +3 -3
- data/lib/kamal/commands/app/execution.rb +2 -2
- data/lib/kamal/commands/app/logging.rb +2 -2
- data/lib/kamal/commands/app.rb +12 -13
- data/lib/kamal/commands/base.rb +8 -0
- data/lib/kamal/commands/builder/base.rb +10 -3
- data/lib/kamal/commands/builder/multiarch.rb +9 -1
- data/lib/kamal/commands/docker.rb +10 -1
- data/lib/kamal/commands/prune.rb +2 -2
- data/lib/kamal/commands/registry.rb +4 -1
- data/lib/kamal/commands/traefik.rb +2 -2
- data/lib/kamal/configuration/accessory.rb +1 -1
- data/lib/kamal/configuration/boot.rb +1 -1
- data/lib/kamal/configuration/builder.rb +4 -0
- data/lib/kamal/configuration/role.rb +15 -2
- data/lib/kamal/configuration.rb +38 -18
- data/lib/kamal/sshkit_with_ext.rb +1 -0
- data/lib/kamal/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df82a171edf966865ca45112056965b93b50a700c4ae190cebe791992c41a825
|
4
|
+
data.tar.gz: 9c5290566c477597460910a80a6e0694285e6d9779b6897bc6c1c10f5bd23b7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c2995cb335465be583037f0a23e2a5d3ce128b47e162b69d7666dbc3e313b542c1d10abef9fa2bb542bdb679003dc8993c3037c4738a9e8544fb346755e8ced0
|
7
|
+
data.tar.gz: a0b7d5500f7ad5408409e0f655274e19a833a8158d2013e79a70e7209d87f5c350b715071d8981f9119f9b3d8c66033bfcd23b5290d3d3880511b3c98c7cff83
|
data/lib/kamal/cli/accessory.rb
CHANGED
@@ -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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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 #{
|
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(
|
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(
|
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(
|
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(
|
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
|
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(
|
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
|
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
|
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|
|
data/lib/kamal/cli/base.rb
CHANGED
@@ -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(
|
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
|
data/lib/kamal/cli/main.rb
CHANGED
@@ -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
|
data/lib/kamal/cli/prune.rb
CHANGED
@@ -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
|
data/lib/kamal/cli/server.rb
CHANGED
@@ -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
|
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.
|
data/lib/kamal/cli/traefik.rb
CHANGED
@@ -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
|
data/lib/kamal/commander.rb
CHANGED
@@ -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
|
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) }
|
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 = "#{
|
3
|
+
asset_container = "#{role.container_prefix}-assets"
|
4
4
|
|
5
5
|
combine \
|
6
|
-
make_directory(
|
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}:#{
|
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 =
|
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 =
|
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(
|
33
|
-
find_and_remove_older_siblings(
|
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", "'#{
|
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 == \"#{
|
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
|
-
*
|
13
|
+
*role&.env_args,
|
14
14
|
*config.volume_args,
|
15
|
-
*
|
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
|
13
|
+
"xargs docker logs --timestamps#{" --tail #{lines}" if lines} --follow 2>&1",
|
14
14
|
(%(grep "#{grep}") if grep)
|
15
15
|
),
|
16
16
|
host: host
|
data/lib/kamal/commands/app.rb
CHANGED
@@ -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, :
|
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
|
-
*
|
23
|
-
*
|
24
|
-
*
|
21
|
+
*role.env_args,
|
22
|
+
*role.health_check_args,
|
23
|
+
*role.logging_args,
|
25
24
|
*config.volume_args,
|
26
|
-
*
|
27
|
-
*
|
28
|
-
*
|
25
|
+
*role.asset_volume_args,
|
26
|
+
*role.label_args,
|
27
|
+
*role.option_args,
|
29
28
|
config.absolute_image,
|
30
|
-
|
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##{
|
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
|
71
|
+
make_directory role.host_env_directory
|
73
72
|
end
|
74
73
|
|
75
74
|
def remove_env_file
|
76
|
-
[ :rm, "-f",
|
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
|
-
[
|
81
|
+
[ role.container_prefix, version || config.version ].compact.join("-")
|
83
82
|
end
|
84
83
|
|
85
84
|
def filter_args(statuses: nil)
|
data/lib/kamal/commands/base.rb
CHANGED
@@ -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
|
-
|
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",
|
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
|
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
|
data/lib/kamal/commands/prune.rb
CHANGED
@@ -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(
|
16
|
+
def app_containers(retain:)
|
17
17
|
pipe \
|
18
18
|
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
|
19
|
-
"tail -n +#{
|
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,
|
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.
|
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
|
-
|
42
|
+
any start, run
|
43
43
|
end
|
44
44
|
|
45
45
|
def info
|
@@ -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
|
-
|
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
|
117
|
+
self == @config.primary_role
|
105
118
|
end
|
106
119
|
|
107
120
|
|
data/lib/kamal/configuration.rb
CHANGED
@@ -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
|
-
|
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
|
141
|
-
optionize({ "log-driver" =>
|
142
|
-
argumentize("--log-opt",
|
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?(
|
268
|
-
raise ArgumentError, "The primary_role #{
|
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
|
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
|
data/lib/kamal/version.rb
CHANGED
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.
|
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-
|
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.
|
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.
|