kamal 1.3.1 → 1.5.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +38 -29
  3. data/lib/kamal/cli/app/boot.rb +67 -0
  4. data/lib/kamal/cli/app/prepare_assets.rb +24 -0
  5. data/lib/kamal/cli/app.rb +25 -67
  6. data/lib/kamal/cli/base.rb +23 -8
  7. data/lib/kamal/cli/env.rb +3 -5
  8. data/lib/kamal/cli/main.rb +7 -4
  9. data/lib/kamal/cli/prune.rb +6 -2
  10. data/lib/kamal/cli/server.rb +3 -1
  11. data/lib/kamal/cli/templates/deploy.yml +5 -1
  12. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +7 -0
  13. data/lib/kamal/cli/traefik.rb +16 -13
  14. data/lib/kamal/commander/specifics.rb +49 -0
  15. data/lib/kamal/commander.rb +9 -33
  16. data/lib/kamal/commands/accessory.rb +2 -2
  17. data/lib/kamal/commands/app/assets.rb +12 -12
  18. data/lib/kamal/commands/app/cord.rb +4 -4
  19. data/lib/kamal/commands/app/execution.rb +10 -8
  20. data/lib/kamal/commands/app/images.rb +1 -1
  21. data/lib/kamal/commands/app/logging.rb +2 -2
  22. data/lib/kamal/commands/app.rb +38 -18
  23. data/lib/kamal/commands/auditor.rb +1 -1
  24. data/lib/kamal/commands/base.rb +12 -0
  25. data/lib/kamal/commands/builder/base.rb +22 -5
  26. data/lib/kamal/commands/builder/multiarch.rb +17 -9
  27. data/lib/kamal/commands/builder/native/cached.rb +7 -6
  28. data/lib/kamal/commands/builder/native/remote.rb +9 -9
  29. data/lib/kamal/commands/builder/native.rb +8 -7
  30. data/lib/kamal/commands/docker.rb +10 -1
  31. data/lib/kamal/commands/healthcheck.rb +0 -1
  32. data/lib/kamal/commands/hook.rb +1 -1
  33. data/lib/kamal/commands/lock.rb +19 -9
  34. data/lib/kamal/commands/prune.rb +4 -4
  35. data/lib/kamal/commands/registry.rb +4 -1
  36. data/lib/kamal/commands/server.rb +1 -1
  37. data/lib/kamal/commands/traefik.rb +10 -16
  38. data/lib/kamal/configuration/accessory.rb +10 -20
  39. data/lib/kamal/configuration/boot.rb +1 -1
  40. data/lib/kamal/configuration/builder.rb +11 -3
  41. data/lib/kamal/configuration/env.rb +40 -0
  42. data/lib/kamal/configuration/role.rb +23 -40
  43. data/lib/kamal/configuration.rb +53 -21
  44. data/lib/kamal/env_file.rb +12 -15
  45. data/lib/kamal/sshkit_with_ext.rb +1 -0
  46. data/lib/kamal/utils.rb +7 -3
  47. data/lib/kamal/version.rb +1 -1
  48. data/lib/kamal.rb +1 -1
  49. metadata +8 -3
@@ -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 [ :curl, "-fsSL", "https://get.docker.com" ], :sh
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
@@ -1,5 +1,4 @@
1
1
  class Kamal::Commands::Healthcheck < Kamal::Commands::Base
2
-
3
2
  def run
4
3
  primary = config.role(config.primary_role)
5
4
 
@@ -9,6 +9,6 @@ class Kamal::Commands::Hook < Kamal::Commands::Base
9
9
 
10
10
  private
11
11
  def hook_file(hook)
12
- "#{config.hooks_path}/#{hook}"
12
+ File.join(config.hooks_path, hook)
13
13
  end
14
14
  end
@@ -5,14 +5,14 @@ require "base64"
5
5
  class Kamal::Commands::Lock < Kamal::Commands::Base
6
6
  def acquire(message, version)
7
7
  combine \
8
- [:mkdir, lock_dir],
8
+ [ :mkdir, lock_dir ],
9
9
  write_lock_details(message, version)
10
10
  end
11
11
 
12
12
  def release
13
13
  combine \
14
- [:rm, lock_details_file],
15
- [:rm, "-r", lock_dir]
14
+ [ :rm, lock_details_file ],
15
+ [ :rm, "-r", lock_dir ]
16
16
  end
17
17
 
18
18
  def status
@@ -21,31 +21,41 @@ class Kamal::Commands::Lock < Kamal::Commands::Base
21
21
  read_lock_details
22
22
  end
23
23
 
24
+ def ensure_locks_directory
25
+ [ :mkdir, "-p", locks_dir ]
26
+ end
27
+
24
28
  private
25
29
  def write_lock_details(message, version)
26
30
  write \
27
- [:echo, "\"#{Base64.encode64(lock_details(message, version))}\""],
31
+ [ :echo, "\"#{Base64.encode64(lock_details(message, version))}\"" ],
28
32
  lock_details_file
29
33
  end
30
34
 
31
35
  def read_lock_details
32
36
  pipe \
33
- [:cat, lock_details_file],
34
- [:base64, "-d"]
37
+ [ :cat, lock_details_file ],
38
+ [ :base64, "-d" ]
35
39
  end
36
40
 
37
41
  def stat_lock_dir
38
42
  write \
39
- [:stat, lock_dir],
43
+ [ :stat, lock_dir ],
40
44
  "/dev/null"
41
45
  end
42
46
 
47
+ def locks_dir
48
+ File.join(config.run_directory, "locks")
49
+ end
50
+
43
51
  def lock_dir
44
- "#{config.run_directory}/lock-#{config.service}"
52
+ dir_name = [ config.service, config.destination ].compact.join("-")
53
+
54
+ File.join(locks_dir, dir_name)
45
55
  end
46
56
 
47
57
  def lock_details_file
48
- [lock_dir, :details].join("/")
58
+ File.join(lock_dir, "details")
49
59
  end
50
60
 
51
61
  def lock_details(message, version)
@@ -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(keep_last: 5)
16
+ def app_containers(retain:)
17
17
  pipe \
18
18
  docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
19
- "tail -n +#{keep_last + 1}",
19
+ "tail -n +#{retain + 1}",
20
20
  "while read container_id; do docker rm $container_id; done"
21
21
  end
22
22
 
@@ -26,7 +26,7 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
26
26
 
27
27
  private
28
28
  def stopped_containers_filters
29
- [ "created", "exited", "dead" ].flat_map { |status| ["--filter", "status=#{status}"] }
29
+ [ "created", "exited", "dead" ].flat_map { |status| [ "--filter", "status=#{status}" ] }
30
30
  end
31
31
 
32
32
  def active_image_list
@@ -43,4 +43,4 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
43
43
  def healthcheck_service_filter
44
44
  [ "--filter", "label=service=#{config.healthcheck_service}" ]
45
45
  end
46
- end
46
+ end
@@ -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, registry["server"], "-u", sensitive(lookup("username")), "-p", sensitive(lookup("password"))
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,5 +1,5 @@
1
1
  class Kamal::Commands::Server < Kamal::Commands::Base
2
2
  def ensure_run_directory
3
- [:mkdir, "-p", config.run_directory]
3
+ [ :mkdir, "-p", config.run_directory ]
4
4
  end
5
5
  end
@@ -1,10 +1,10 @@
1
1
  class Kamal::Commands::Traefik < Kamal::Commands::Base
2
2
  delegate :argumentize, :optionize, to: Kamal::Utils
3
3
 
4
- DEFAULT_IMAGE = "traefik:v2.9"
4
+ DEFAULT_IMAGE = "traefik:v2.10"
5
5
  CONTAINER_PORT = 80
6
6
  DEFAULT_ARGS = {
7
- 'log.level' => 'DEBUG'
7
+ "log.level" => "DEBUG"
8
8
  }
9
9
  DEFAULT_LABELS = {
10
10
  # These ensure we serve a 502 rather than a 404 if no containers are available
@@ -39,7 +39,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
39
39
  end
40
40
 
41
41
  def start_or_run
42
- combine start, run, by: "||"
42
+ any start, run
43
43
  end
44
44
 
45
45
  def info
@@ -71,20 +71,18 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
71
71
  "#{host_port}:#{CONTAINER_PORT}"
72
72
  end
73
73
 
74
- def env_file
75
- Kamal::EnvFile.new(config.traefik.fetch("env", {}))
76
- end
77
-
78
- def host_env_file_path
79
- File.join host_env_directory, "traefik.env"
74
+ def env
75
+ Kamal::Configuration::Env.from_config \
76
+ config: config.traefik.fetch("env", {}),
77
+ secrets_file: File.join(config.host_env_directory, "traefik", "traefik.env")
80
78
  end
81
79
 
82
80
  def make_env_directory
83
- make_directory(host_env_directory)
81
+ make_directory(env.secrets_directory)
84
82
  end
85
83
 
86
84
  def remove_env_file
87
- [:rm, "-f", host_env_file_path]
85
+ [ :rm, "-f", env.secrets_file ]
88
86
  end
89
87
 
90
88
  private
@@ -97,11 +95,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
97
95
  end
98
96
 
99
97
  def env_args
100
- argumentize "--env-file", host_env_file_path
101
- end
102
-
103
- def host_env_directory
104
- File.join config.host_env_directory, "traefik"
98
+ env.args
105
99
  end
106
100
 
107
101
  def labels
@@ -8,7 +8,7 @@ class Kamal::Configuration::Accessory
8
8
  end
9
9
 
10
10
  def service_name
11
- "#{config.service}-#{name}"
11
+ specifics["service"] || "#{config.service}-#{name}"
12
12
  end
13
13
 
14
14
  def image
@@ -16,7 +16,7 @@ class Kamal::Configuration::Accessory
16
16
  end
17
17
 
18
18
  def hosts
19
- if (specifics.keys & ["host", "hosts", "roles"]).size != 1
19
+ if (specifics.keys & [ "host", "hosts", "roles" ]).size != 1
20
20
  raise ArgumentError, "Specify one of `host`, `hosts` or `roles` for accessory `#{name}`"
21
21
  end
22
22
 
@@ -42,23 +42,13 @@ class Kamal::Configuration::Accessory
42
42
  end
43
43
 
44
44
  def env
45
- specifics["env"] || {}
46
- end
47
-
48
- def env_file
49
- Kamal::EnvFile.new(env)
50
- end
51
-
52
- def host_env_directory
53
- File.join config.host_env_directory, "accessories"
54
- end
55
-
56
- def host_env_file_path
57
- File.join host_env_directory, "#{service_name}.env"
45
+ Kamal::Configuration::Env.from_config \
46
+ config: specifics.fetch("env", {}),
47
+ secrets_file: File.join(config.host_env_directory, "accessories", "#{service_name}.env")
58
48
  end
59
49
 
60
50
  def env_args
61
- argumentize "--env-file", host_env_file_path
51
+ env.args
62
52
  end
63
53
 
64
54
  def files
@@ -111,10 +101,10 @@ class Kamal::Configuration::Accessory
111
101
  end
112
102
 
113
103
  def with_clear_env_loaded
114
- (env["clear"] || env).each { |k, v| ENV[k] = v }
104
+ env.clear.each { |k, v| ENV[k] = v }
115
105
  yield
116
106
  ensure
117
- (env["clear"] || env).each { |k, v| ENV.delete(k) }
107
+ env.clear.each { |k, v| ENV.delete(k) }
118
108
  end
119
109
 
120
110
  def read_dynamic_file(local_file)
@@ -144,7 +134,7 @@ class Kamal::Configuration::Accessory
144
134
  end
145
135
 
146
136
  def expand_host_path(host_path)
147
- absolute_path?(host_path) ? host_path : "#{service_data_directory}/#{host_path}"
137
+ absolute_path?(host_path) ? host_path : File.join(service_data_directory, host_path)
148
138
  end
149
139
 
150
140
  def absolute_path?(path)
@@ -159,7 +149,7 @@ class Kamal::Configuration::Accessory
159
149
  if specifics.key?("host")
160
150
  host = specifics["host"]
161
151
  if host
162
- [host]
152
+ [ host ]
163
153
  else
164
154
  raise ArgumentError, "Missing host for accessory `#{name}`"
165
155
  end
@@ -8,7 +8,7 @@ class Kamal::Configuration::Boot
8
8
  limit = @options["limit"]
9
9
 
10
10
  if limit.to_s.end_with?("%")
11
- @host_count * limit.to_i / 100
11
+ [ @host_count * limit.to_i / 100, 1 ].max
12
12
  else
13
13
  limit
14
14
  end
@@ -40,7 +40,7 @@ class Kamal::Configuration::Builder
40
40
  end
41
41
 
42
42
  def context
43
- @options["context"] || "."
43
+ @options["context"] || (git_archive? ? "-" : ".")
44
44
  end
45
45
 
46
46
  def local_arch
@@ -81,10 +81,18 @@ class Kamal::Configuration::Builder
81
81
  end
82
82
  end
83
83
 
84
+ def ssh
85
+ @options["ssh"]
86
+ end
87
+
88
+ def git_archive?
89
+ Kamal::Git.used? && @options["context"].nil?
90
+ end
91
+
84
92
  private
85
93
  def valid?
86
94
  if @options["cache"] && @options["cache"]["type"]
87
- raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless ["gha", "registry"].include?(@options["cache"]["type"])
95
+ raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"])
88
96
  end
89
97
  end
90
98
 
@@ -105,7 +113,7 @@ class Kamal::Configuration::Builder
105
113
  end
106
114
 
107
115
  def cache_to_config_for_gha
108
- [ "type=gha", @options["cache"]&.fetch("options", nil)].compact.join(",")
116
+ [ "type=gha", @options["cache"]&.fetch("options", nil) ].compact.join(",")
109
117
  end
110
118
 
111
119
  def cache_to_config_for_registry
@@ -0,0 +1,40 @@
1
+ class Kamal::Configuration::Env
2
+ attr_reader :secrets_keys, :clear, :secrets_file
3
+ delegate :argumentize, to: Kamal::Utils
4
+
5
+ def self.from_config(config:, secrets_file: nil)
6
+ secrets_keys = config.fetch("secret", [])
7
+ clear = config.fetch("clear", config.key?("secret") ? {} : config)
8
+
9
+ new clear: clear, secrets_keys: secrets_keys, secrets_file: secrets_file
10
+ end
11
+
12
+ def initialize(clear:, secrets_keys:, secrets_file:)
13
+ @clear = clear
14
+ @secrets_keys = secrets_keys
15
+ @secrets_file = secrets_file
16
+ end
17
+
18
+ def args
19
+ [ "--env-file", secrets_file, *argumentize("--env", clear) ]
20
+ end
21
+
22
+ def secrets_io
23
+ StringIO.new(Kamal::EnvFile.new(secrets).to_s)
24
+ end
25
+
26
+ def secrets
27
+ @secrets ||= secrets_keys.to_h { |key| [ key, ENV.fetch(key) ] }
28
+ end
29
+
30
+ def secrets_directory
31
+ File.dirname(secrets_file)
32
+ end
33
+
34
+ def merge(other)
35
+ self.class.new \
36
+ clear: @clear.merge(other.clear),
37
+ secrets_keys: @secrets_keys | other.secrets_keys,
38
+ secrets_file: secrets_file
39
+ end
40
+ end
@@ -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
- @name, @config = name.inquiry, config
9
+ @name, @config = name.inquiry, config
9
10
  end
10
11
 
11
12
  def primary_host
@@ -36,29 +37,25 @@ 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?
39
43
 
40
- def env
41
- if config.env && config.env["secret"]
42
- merged_env_with_secrets
44
+ if args.any?
45
+ optionize({ "log-driver" => args["driver"] }.compact) +
46
+ argumentize("--log-opt", args["options"])
43
47
  else
44
- merged_env
48
+ config.logging_args
45
49
  end
46
50
  end
47
51
 
48
- def env_file
49
- Kamal::EnvFile.new(env)
50
- end
51
-
52
- def host_env_directory
53
- File.join config.host_env_directory, "roles"
54
- end
55
52
 
56
- def host_env_file_path
57
- File.join host_env_directory, "#{[config.service, name, config.destination].compact.join("-")}.env"
53
+ def env
54
+ @env ||= base_env.merge(specialized_env)
58
55
  end
59
56
 
60
57
  def env_args
61
- argumentize "--env-file", host_env_file_path
58
+ env.args
62
59
  end
63
60
 
64
61
  def asset_volume_args
@@ -101,7 +98,7 @@ class Kamal::Configuration::Role
101
98
  end
102
99
 
103
100
  def primary?
104
- @config.primary_role == name
101
+ self == @config.primary_role
105
102
  end
106
103
 
107
104
 
@@ -110,13 +107,13 @@ class Kamal::Configuration::Role
110
107
  end
111
108
 
112
109
  def cord_host_directory
113
- File.join config.run_directory_as_docker_volume, "cords", [container_prefix, config.run_id].join("-")
110
+ File.join config.run_directory_as_docker_volume, "cords", [ container_prefix, config.run_id ].join("-")
114
111
  end
115
112
 
116
113
  def cord_volume
117
114
  if (cord = health_check_options["cord"])
118
115
  @cord_volume ||= Kamal::Configuration::Volume.new \
119
- host_path: File.join(config.run_directory, "cords", [container_prefix, config.run_id].join("-")),
116
+ host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")),
120
117
  container_path: cord
121
118
  end
122
119
  end
@@ -179,11 +176,7 @@ class Kamal::Configuration::Role
179
176
  end
180
177
 
181
178
  def default_labels
182
- if config.destination
183
- { "service" => config.service, "role" => name, "destination" => config.destination }
184
- else
185
- { "service" => config.service, "role" => name }
186
- end
179
+ { "service" => config.service, "role" => name, "destination" => config.destination }
187
180
  end
188
181
 
189
182
  def traefik_labels
@@ -204,7 +197,7 @@ class Kamal::Configuration::Role
204
197
  end
205
198
 
206
199
  def traefik_service
207
- [ config.service, name, config.destination ].compact.join("-")
200
+ container_prefix
208
201
  end
209
202
 
210
203
  def custom_labels
@@ -216,31 +209,21 @@ class Kamal::Configuration::Role
216
209
 
217
210
  def specializations
218
211
  if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
219
- { }
212
+ {}
220
213
  else
221
214
  config.servers[name].except("hosts")
222
215
  end
223
216
  end
224
217
 
225
218
  def specialized_env
226
- specializations["env"] || {}
227
- end
228
-
229
- def merged_env
230
- config.env&.merge(specialized_env) || {}
219
+ Kamal::Configuration::Env.from_config config: specializations.fetch("env", {})
231
220
  end
232
221
 
233
222
  # Secrets are stored in an array, which won't merge by default, so have to do it by hand.
234
- def merged_env_with_secrets
235
- merged_env.tap do |new_env|
236
- new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"])
237
-
238
- # If there's no secret/clear split, everything is clear
239
- clear_app_env = config.env["secret"] ? Array(config.env["clear"]) : Array(config.env["clear"] || config.env)
240
- clear_role_env = specialized_env["secret"] ? Array(specialized_env["clear"]) : Array(specialized_env["clear"] || specialized_env)
241
-
242
- new_env["clear"] = clear_app_env.to_h.merge(clear_role_env.to_h)
243
- end
223
+ def base_env
224
+ Kamal::Configuration::Env.from_config \
225
+ config: config.env,
226
+ secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env")
244
227
  end
245
228
 
246
229
  def http_health_check(port:, path:)
@@ -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, :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
@@ -88,11 +88,23 @@ class Kamal::Configuration
88
88
 
89
89
 
90
90
  def all_hosts
91
- roles.flat_map(&:hosts).uniq
91
+ (roles + accessories).flat_map(&:hosts).uniq
92
92
  end
93
93
 
94
94
  def primary_host
95
- role(primary_role)&.primary_host
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
@@ -116,7 +128,11 @@ class Kamal::Configuration
116
128
  end
117
129
 
118
130
  def latest_image
119
- "#{repository}:latest"
131
+ "#{repository}:#{latest_tag}"
132
+ end
133
+
134
+ def latest_tag
135
+ [ "latest", *destination ].join("-")
120
136
  end
121
137
 
122
138
  def service_with_version
@@ -127,6 +143,10 @@ class Kamal::Configuration
127
143
  raw_config.require_destination
128
144
  end
129
145
 
146
+ def retain_containers
147
+ raw_config.retain_containers || 5
148
+ end
149
+
130
150
 
131
151
  def volume_args
132
152
  if raw_config.volumes.present?
@@ -137,9 +157,9 @@ class Kamal::Configuration
137
157
  end
138
158
 
139
159
  def logging_args
140
- if raw_config.logging.present?
141
- optionize({ "log-driver" => raw_config.logging["driver"] }.compact) +
142
- argumentize("--log-opt", raw_config.logging["options"])
160
+ if logging.present?
161
+ optionize({ "log-driver" => logging["driver"] }.compact) +
162
+ argumentize("--log-opt", logging["options"])
143
163
  else
144
164
  argumentize("--log-opt", { "max-size" => "10m" })
145
165
  end
@@ -200,25 +220,22 @@ class Kamal::Configuration
200
220
  raw_config.hooks_path || ".kamal/hooks"
201
221
  end
202
222
 
203
- def host_env_directory
204
- "#{run_directory}/env"
205
- end
206
-
207
223
  def asset_path
208
224
  raw_config.asset_path
209
225
  end
210
226
 
211
- def primary_role
212
- raw_config.primary_role || "web"
227
+
228
+ def host_env_directory
229
+ File.join(run_directory, "env")
213
230
  end
214
231
 
215
- def allow_empty_roles?
216
- raw_config.allow_empty_roles
232
+ def env
233
+ raw_config.env || {}
217
234
  end
218
235
 
219
236
 
220
237
  def valid?
221
- ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version
238
+ ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid && ensure_valid_service_name
222
239
  end
223
240
 
224
241
  def to_h
@@ -264,12 +281,12 @@ class Kamal::Configuration
264
281
  raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
265
282
  end
266
283
 
267
- unless role_names.include?(primary_role)
268
- raise ArgumentError, "The primary_role #{primary_role} isn't defined"
284
+ unless role_names.include?(primary_role_name)
285
+ raise ArgumentError, "The primary_role #{primary_role_name} isn't defined"
269
286
  end
270
287
 
271
- if role(primary_role).hosts.empty?
272
- raise ArgumentError, "No servers specified for the #{primary_role} primary_role"
288
+ if primary_role.hosts.empty?
289
+ raise ArgumentError, "No servers specified for the #{primary_role.name} primary_role"
273
290
  end
274
291
 
275
292
  unless allow_empty_roles?
@@ -283,6 +300,12 @@ class Kamal::Configuration
283
300
  true
284
301
  end
285
302
 
303
+ def ensure_valid_service_name
304
+ raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/i
305
+
306
+ true
307
+ end
308
+
286
309
  def ensure_valid_kamal_version
287
310
  if minimum_version && Gem::Version.new(minimum_version) > Gem::Version.new(Kamal::VERSION)
288
311
  raise ArgumentError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
@@ -291,6 +314,12 @@ class Kamal::Configuration
291
314
  true
292
315
  end
293
316
 
317
+ def ensure_retain_containers_valid
318
+ raise ArgumentError, "Must retain at least 1 container" if retain_containers < 1
319
+
320
+ true
321
+ end
322
+
294
323
 
295
324
  def role_names
296
325
  raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
@@ -299,7 +328,10 @@ class Kamal::Configuration
299
328
  def git_version
300
329
  @git_version ||=
301
330
  if Kamal::Git.used?
302
- [ Kamal::Git.revision, Kamal::Git.uncommitted_changes.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : "" ].join
331
+ if Kamal::Git.uncommitted_changes.present? && !builder.git_archive?
332
+ uncommitted_suffix = "_uncommitted_#{SecureRandom.hex(8)}"
333
+ end
334
+ [ Kamal::Git.revision, uncommitted_suffix ].compact.join
303
335
  else
304
336
  raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
305
337
  end