kamal 1.3.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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