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
@@ -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, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
10
+ delegate :argumentize, :optionize, to: Kamal::Utils
11
11
 
12
- attr_accessor :destination
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
- Kamal::Utils.abbreviate_version(version)
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
- def env_args
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 minimum_version
156
- raw_config.minimum_version
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 system("git rev-parse")
244
- uncommitted_suffix = Kamal::Utils.uncommitted_changes.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : ""
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
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "0.16.0"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.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-08-22 00:00:00.000000000 Z
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/rails/kamal
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.18
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