kamal 0.16.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -1013
- data/lib/kamal/cli/app.rb +38 -11
- data/lib/kamal/cli/base.rb +8 -0
- data/lib/kamal/cli/build.rb +18 -1
- data/lib/kamal/cli/env.rb +56 -0
- data/lib/kamal/cli/healthcheck/poller.rb +64 -0
- data/lib/kamal/cli/healthcheck.rb +2 -2
- data/lib/kamal/cli/lock.rb +12 -3
- data/lib/kamal/cli/main.rb +14 -3
- data/lib/kamal/cli/prune.rb +3 -2
- data/lib/kamal/cli/server.rb +2 -0
- data/lib/kamal/cli/templates/deploy.yml +12 -1
- data/lib/kamal/commander.rb +21 -8
- data/lib/kamal/commands/accessory.rb +8 -8
- data/lib/kamal/commands/app/assets.rb +51 -0
- data/lib/kamal/commands/app/containers.rb +23 -0
- data/lib/kamal/commands/app/cord.rb +22 -0
- data/lib/kamal/commands/app/execution.rb +27 -0
- data/lib/kamal/commands/app/images.rb +13 -0
- data/lib/kamal/commands/app/logging.rb +18 -0
- data/lib/kamal/commands/app.rb +17 -91
- data/lib/kamal/commands/auditor.rb +3 -1
- data/lib/kamal/commands/base.rb +12 -0
- data/lib/kamal/commands/builder/base.rb +6 -0
- data/lib/kamal/commands/builder.rb +3 -1
- data/lib/kamal/commands/healthcheck.rb +15 -12
- data/lib/kamal/commands/lock.rb +2 -2
- data/lib/kamal/commands/prune.rb +11 -3
- data/lib/kamal/commands/server.rb +5 -0
- data/lib/kamal/commands/traefik.rb +26 -8
- data/lib/kamal/configuration/accessory.rb +14 -2
- data/lib/kamal/configuration/role.rb +112 -19
- data/lib/kamal/configuration/ssh.rb +1 -1
- data/lib/kamal/configuration/volume.rb +22 -0
- data/lib/kamal/configuration.rb +79 -43
- data/lib/kamal/env_file.rb +41 -0
- data/lib/kamal/git.rb +19 -0
- data/lib/kamal/utils.rb +0 -39
- data/lib/kamal/version.rb +1 -1
- metadata +16 -5
- data/lib/kamal/utils/healthcheck_poller.rb +0 -39
data/lib/kamal/configuration.rb
CHANGED
@@ -7,10 +7,9 @@ require "net/ssh/proxy/jump"
|
|
7
7
|
|
8
8
|
class Kamal::Configuration
|
9
9
|
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
|
10
|
-
delegate :argumentize, :
|
10
|
+
delegate :argumentize, :optionize, to: Kamal::Utils
|
11
11
|
|
12
|
-
|
13
|
-
attr_accessor :raw_config
|
12
|
+
attr_reader :destination, :raw_config
|
14
13
|
|
15
14
|
class << self
|
16
15
|
def create_from(config_file:, destination: nil, version: nil)
|
@@ -54,7 +53,18 @@ class Kamal::Configuration
|
|
54
53
|
end
|
55
54
|
|
56
55
|
def abbreviated_version
|
57
|
-
|
56
|
+
if version
|
57
|
+
# Don't abbreviate <sha>_uncommitted_<etc>
|
58
|
+
if version.include?("_")
|
59
|
+
version
|
60
|
+
else
|
61
|
+
version[0...7]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def minimum_version
|
67
|
+
raw_config.minimum_version
|
58
68
|
end
|
59
69
|
|
60
70
|
|
@@ -87,10 +97,6 @@ class Kamal::Configuration
|
|
87
97
|
roles.select(&:running_traefik?).flat_map(&:hosts).uniq
|
88
98
|
end
|
89
99
|
|
90
|
-
def boot
|
91
|
-
Kamal::Configuration::Boot.new(config: self)
|
92
|
-
end
|
93
|
-
|
94
100
|
|
95
101
|
def repository
|
96
102
|
[ raw_config.registry["server"], image ].compact.join("/")
|
@@ -108,15 +114,11 @@ class Kamal::Configuration
|
|
108
114
|
"#{service}-#{version}"
|
109
115
|
end
|
110
116
|
|
111
|
-
|
112
|
-
|
113
|
-
if raw_config.env.present?
|
114
|
-
argumentize_env_with_secrets(raw_config.env)
|
115
|
-
else
|
116
|
-
[]
|
117
|
-
end
|
117
|
+
def require_destination?
|
118
|
+
raw_config.require_destination
|
118
119
|
end
|
119
120
|
|
121
|
+
|
120
122
|
def volume_args
|
121
123
|
if raw_config.volumes.present?
|
122
124
|
argumentize "--volume", raw_config.volumes
|
@@ -135,6 +137,18 @@ class Kamal::Configuration
|
|
135
137
|
end
|
136
138
|
|
137
139
|
|
140
|
+
def boot
|
141
|
+
Kamal::Configuration::Boot.new(config: self)
|
142
|
+
end
|
143
|
+
|
144
|
+
def builder
|
145
|
+
Kamal::Configuration::Builder.new(config: self)
|
146
|
+
end
|
147
|
+
|
148
|
+
def traefik
|
149
|
+
raw_config.traefik || {}
|
150
|
+
end
|
151
|
+
|
138
152
|
def ssh
|
139
153
|
Kamal::Configuration::Ssh.new(config: self)
|
140
154
|
end
|
@@ -145,21 +159,57 @@ class Kamal::Configuration
|
|
145
159
|
|
146
160
|
|
147
161
|
def healthcheck
|
148
|
-
{ "path" => "/up", "port" => 3000, "max_attempts" => 7 }.merge(raw_config.healthcheck || {})
|
162
|
+
{ "path" => "/up", "port" => 3000, "max_attempts" => 7, "exposed_port" => 3999, "cord" => "/tmp/kamal-cord", "log_lines" => 50 }.merge(raw_config.healthcheck || {})
|
163
|
+
end
|
164
|
+
|
165
|
+
def healthcheck_service
|
166
|
+
[ "healthcheck", service, destination ].compact.join("-")
|
149
167
|
end
|
150
168
|
|
151
169
|
def readiness_delay
|
152
170
|
raw_config.readiness_delay || 7
|
153
171
|
end
|
154
172
|
|
155
|
-
def
|
156
|
-
|
173
|
+
def run_id
|
174
|
+
@run_id ||= SecureRandom.hex(16)
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
def run_directory
|
179
|
+
raw_config.run_directory || ".kamal"
|
180
|
+
end
|
181
|
+
|
182
|
+
def run_directory_as_docker_volume
|
183
|
+
if Pathname.new(run_directory).absolute?
|
184
|
+
run_directory
|
185
|
+
else
|
186
|
+
File.join "$(pwd)", run_directory
|
187
|
+
end
|
157
188
|
end
|
158
189
|
|
190
|
+
def hooks_path
|
191
|
+
raw_config.hooks_path || ".kamal/hooks"
|
192
|
+
end
|
193
|
+
|
194
|
+
def host_env_directory
|
195
|
+
"#{run_directory}/env"
|
196
|
+
end
|
197
|
+
|
198
|
+
def asset_path
|
199
|
+
raw_config.asset_path
|
200
|
+
end
|
201
|
+
|
202
|
+
|
159
203
|
def valid?
|
160
|
-
ensure_required_keys_present && ensure_valid_kamal_version
|
204
|
+
ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version
|
161
205
|
end
|
162
206
|
|
207
|
+
# Will raise KeyError if any secret ENVs are missing
|
208
|
+
def ensure_env_available
|
209
|
+
roles.collect(&:env_file).each(&:to_s)
|
210
|
+
|
211
|
+
true
|
212
|
+
end
|
163
213
|
|
164
214
|
def to_h
|
165
215
|
{
|
@@ -170,7 +220,6 @@ class Kamal::Configuration
|
|
170
220
|
repository: repository,
|
171
221
|
absolute_image: absolute_image,
|
172
222
|
service_with_version: service_with_version,
|
173
|
-
env_args: env_args,
|
174
223
|
volume_args: volume_args,
|
175
224
|
ssh_options: ssh.to_h,
|
176
225
|
sshkit: sshkit.to_h,
|
@@ -181,28 +230,17 @@ class Kamal::Configuration
|
|
181
230
|
}.compact
|
182
231
|
end
|
183
232
|
|
184
|
-
def traefik
|
185
|
-
raw_config.traefik || {}
|
186
|
-
end
|
187
|
-
|
188
|
-
def hooks_path
|
189
|
-
raw_config.hooks_path || ".kamal/hooks"
|
190
|
-
end
|
191
|
-
|
192
|
-
def builder
|
193
|
-
Kamal::Configuration::Builder.new(config: self)
|
194
|
-
end
|
195
|
-
|
196
|
-
# Will raise KeyError if any secret ENVs are missing
|
197
|
-
def ensure_env_available
|
198
|
-
env_args
|
199
|
-
roles.each(&:env_args)
|
200
|
-
|
201
|
-
true
|
202
|
-
end
|
203
233
|
|
204
234
|
private
|
205
235
|
# Will raise ArgumentError if any required config keys are missing
|
236
|
+
def ensure_destination_if_required
|
237
|
+
if require_destination? && destination.nil?
|
238
|
+
raise ArgumentError, "You must specify a destination"
|
239
|
+
end
|
240
|
+
|
241
|
+
true
|
242
|
+
end
|
243
|
+
|
206
244
|
def ensure_required_keys_present
|
207
245
|
%i[ service image registry servers ].each do |key|
|
208
246
|
raise ArgumentError, "Missing required configuration for #{key}" unless raw_config[key].present?
|
@@ -240,10 +278,8 @@ class Kamal::Configuration
|
|
240
278
|
|
241
279
|
def git_version
|
242
280
|
@git_version ||=
|
243
|
-
if
|
244
|
-
|
245
|
-
|
246
|
-
"#{`git rev-parse HEAD`.strip}#{uncommitted_suffix}"
|
281
|
+
if Kamal::Git.used?
|
282
|
+
[ Kamal::Git.revision, Kamal::Git.uncommitted_changes.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : "" ].join
|
247
283
|
else
|
248
284
|
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
|
249
285
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Encode an env hash as a string where secret values have been looked up and all values escaped for Docker.
|
2
|
+
class Kamal::EnvFile
|
3
|
+
def initialize(env)
|
4
|
+
@env = env
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
env_file = StringIO.new.tap do |contents|
|
9
|
+
if (secrets = @env["secret"]).present?
|
10
|
+
@env.fetch("secret", @env)&.each do |key|
|
11
|
+
contents << docker_env_file_line(key, ENV.fetch(key))
|
12
|
+
end
|
13
|
+
|
14
|
+
@env["clear"]&.each do |key, value|
|
15
|
+
contents << docker_env_file_line(key, value)
|
16
|
+
end
|
17
|
+
else
|
18
|
+
@env.fetch("clear", @env)&.each do |key, value|
|
19
|
+
contents << docker_env_file_line(key, value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end.string
|
23
|
+
|
24
|
+
# Ensure the file has some contents to avoid the SSHKIT empty file warning
|
25
|
+
env_file.presence || "\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
alias to_str to_s
|
29
|
+
|
30
|
+
private
|
31
|
+
def docker_env_file_line(key, value)
|
32
|
+
"#{key.to_s}=#{escape_docker_env_file_value(value)}\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Escape a value to make it safe to dump in a docker file.
|
36
|
+
def escape_docker_env_file_value(value)
|
37
|
+
# Doublequotes are treated literally in docker env files
|
38
|
+
# so remove leading and trailing ones and unescape any others
|
39
|
+
value.to_s.dump[1..-2].gsub(/\\"/, "\"")
|
40
|
+
end
|
41
|
+
end
|
data/lib/kamal/git.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Kamal::Git
|
2
|
+
extend self
|
3
|
+
|
4
|
+
def used?
|
5
|
+
system("git rev-parse")
|
6
|
+
end
|
7
|
+
|
8
|
+
def user_name
|
9
|
+
`git config user.name`.strip
|
10
|
+
end
|
11
|
+
|
12
|
+
def revision
|
13
|
+
`git rev-parse HEAD`.strip
|
14
|
+
end
|
15
|
+
|
16
|
+
def uncommitted_changes
|
17
|
+
`git status --porcelain`.strip
|
18
|
+
end
|
19
|
+
end
|
data/lib/kamal/utils.rb
CHANGED
@@ -16,16 +16,6 @@ module Kamal::Utils
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
# Return a list of shell arguments using the same named argument against the passed attributes,
|
20
|
-
# but redacts and expands secrets.
|
21
|
-
def argumentize_env_with_secrets(env)
|
22
|
-
if (secrets = env["secret"]).present?
|
23
|
-
argumentize("-e", secrets.to_h { |key| [ key, ENV.fetch(key) ] }, sensitive: true) + argumentize("-e", env["clear"])
|
24
|
-
else
|
25
|
-
argumentize "-e", env.fetch("clear", env)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
19
|
# Returns a list of shell-dashed option arguments. If the value is true, it's treated like a value-less option.
|
30
20
|
def optionize(args, with: nil)
|
31
21
|
options = if with
|
@@ -62,39 +52,10 @@ module Kamal::Utils
|
|
62
52
|
end
|
63
53
|
end
|
64
54
|
|
65
|
-
def unredacted(value)
|
66
|
-
case
|
67
|
-
when value.respond_to?(:unredacted)
|
68
|
-
value.unredacted
|
69
|
-
when value.respond_to?(:transform_values)
|
70
|
-
value.transform_values { |value| unredacted value }
|
71
|
-
when value.respond_to?(:map)
|
72
|
-
value.map { |element| unredacted element }
|
73
|
-
else
|
74
|
-
value
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
55
|
# Escape a value to make it safe for shell use.
|
79
56
|
def escape_shell_value(value)
|
80
57
|
value.to_s.dump
|
81
58
|
.gsub(/`/, '\\\\`')
|
82
59
|
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
|
83
60
|
end
|
84
|
-
|
85
|
-
# Abbreviate a git revhash for concise display
|
86
|
-
def abbreviate_version(version)
|
87
|
-
if version
|
88
|
-
# Don't abbreviate <sha>_uncommitted_<etc>
|
89
|
-
if version.include?("_")
|
90
|
-
version
|
91
|
-
else
|
92
|
-
version[0...7]
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def uncommitted_changes
|
98
|
-
`git status --porcelain`.strip
|
99
|
-
end
|
100
61
|
end
|
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: 0.
|
4
|
+
version: 1.0.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: 2023-
|
11
|
+
date: 2023-09-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -194,7 +194,9 @@ files:
|
|
194
194
|
- lib/kamal/cli/app.rb
|
195
195
|
- lib/kamal/cli/base.rb
|
196
196
|
- lib/kamal/cli/build.rb
|
197
|
+
- lib/kamal/cli/env.rb
|
197
198
|
- lib/kamal/cli/healthcheck.rb
|
199
|
+
- lib/kamal/cli/healthcheck/poller.rb
|
198
200
|
- lib/kamal/cli/lock.rb
|
199
201
|
- lib/kamal/cli/main.rb
|
200
202
|
- lib/kamal/cli/prune.rb
|
@@ -211,6 +213,12 @@ files:
|
|
211
213
|
- lib/kamal/commands.rb
|
212
214
|
- lib/kamal/commands/accessory.rb
|
213
215
|
- lib/kamal/commands/app.rb
|
216
|
+
- lib/kamal/commands/app/assets.rb
|
217
|
+
- lib/kamal/commands/app/containers.rb
|
218
|
+
- lib/kamal/commands/app/cord.rb
|
219
|
+
- lib/kamal/commands/app/execution.rb
|
220
|
+
- lib/kamal/commands/app/images.rb
|
221
|
+
- lib/kamal/commands/app/logging.rb
|
214
222
|
- lib/kamal/commands/auditor.rb
|
215
223
|
- lib/kamal/commands/base.rb
|
216
224
|
- lib/kamal/commands/builder.rb
|
@@ -226,6 +234,7 @@ files:
|
|
226
234
|
- lib/kamal/commands/lock.rb
|
227
235
|
- lib/kamal/commands/prune.rb
|
228
236
|
- lib/kamal/commands/registry.rb
|
237
|
+
- lib/kamal/commands/server.rb
|
229
238
|
- lib/kamal/commands/traefik.rb
|
230
239
|
- lib/kamal/configuration.rb
|
231
240
|
- lib/kamal/configuration/accessory.rb
|
@@ -234,13 +243,15 @@ files:
|
|
234
243
|
- lib/kamal/configuration/role.rb
|
235
244
|
- lib/kamal/configuration/ssh.rb
|
236
245
|
- lib/kamal/configuration/sshkit.rb
|
246
|
+
- lib/kamal/configuration/volume.rb
|
247
|
+
- lib/kamal/env_file.rb
|
248
|
+
- lib/kamal/git.rb
|
237
249
|
- lib/kamal/sshkit_with_ext.rb
|
238
250
|
- lib/kamal/tags.rb
|
239
251
|
- lib/kamal/utils.rb
|
240
|
-
- lib/kamal/utils/healthcheck_poller.rb
|
241
252
|
- lib/kamal/utils/sensitive.rb
|
242
253
|
- lib/kamal/version.rb
|
243
|
-
homepage: https://github.com/
|
254
|
+
homepage: https://github.com/basecamp/kamal
|
244
255
|
licenses:
|
245
256
|
- MIT
|
246
257
|
metadata: {}
|
@@ -259,7 +270,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
259
270
|
- !ruby/object:Gem::Version
|
260
271
|
version: '0'
|
261
272
|
requirements: []
|
262
|
-
rubygems_version: 3.4.
|
273
|
+
rubygems_version: 3.4.19
|
263
274
|
signing_key:
|
264
275
|
specification_version: 4
|
265
276
|
summary: Deploy web apps in containers to servers running Docker with zero downtime.
|
@@ -1,39 +0,0 @@
|
|
1
|
-
class Kamal::Utils::HealthcheckPoller
|
2
|
-
TRAEFIK_HEALTHY_DELAY = 2
|
3
|
-
|
4
|
-
class HealthcheckError < StandardError; end
|
5
|
-
|
6
|
-
class << self
|
7
|
-
def wait_for_healthy(pause_after_ready: false, &block)
|
8
|
-
attempt = 1
|
9
|
-
max_attempts = KAMAL.config.healthcheck["max_attempts"]
|
10
|
-
|
11
|
-
begin
|
12
|
-
case status = block.call
|
13
|
-
when "healthy"
|
14
|
-
sleep TRAEFIK_HEALTHY_DELAY if pause_after_ready
|
15
|
-
when "running" # No health check configured
|
16
|
-
sleep KAMAL.config.readiness_delay if pause_after_ready
|
17
|
-
else
|
18
|
-
raise HealthcheckError, "container not ready (#{status})"
|
19
|
-
end
|
20
|
-
rescue HealthcheckError => e
|
21
|
-
if attempt <= max_attempts
|
22
|
-
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
|
23
|
-
sleep attempt
|
24
|
-
attempt += 1
|
25
|
-
retry
|
26
|
-
else
|
27
|
-
raise
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
info "Container is healthy!"
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
def info(message)
|
36
|
-
SSHKit.config.output.info(message)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|