kamal 0.16.1 → 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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  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 +1 -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 +21 -7
  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 +15 -4
  42. data/lib/kamal/utils/healthcheck_poller.rb +0 -39
@@ -0,0 +1,22 @@
1
+ class Kamal::Configuration::Volume
2
+ attr_reader :host_path, :container_path
3
+ delegate :argumentize, to: Kamal::Utils
4
+
5
+ def initialize(host_path:, container_path:)
6
+ @host_path = host_path
7
+ @container_path = container_path
8
+ end
9
+
10
+ def docker_args
11
+ argumentize "--volume", "#{host_path_for_docker_volume}:#{container_path}"
12
+ end
13
+
14
+ private
15
+ def host_path_for_docker_volume
16
+ if Pathname.new(host_path).absolute?
17
+ host_path
18
+ else
19
+ File.join "$(pwd)", host_path
20
+ end
21
+ end
22
+ end
@@ -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.1"
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.1
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-24 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,10 +243,12 @@ 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
254
  homepage: https://github.com/basecamp/kamal
@@ -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