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
@@ -11,20 +11,23 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
11
11
 
12
12
  desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
13
13
  option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
14
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
14
15
  def reboot
15
- mutating do
16
- host_groups = options[:rolling] ? KAMAL.traefik_hosts : [KAMAL.traefik_hosts]
17
- host_groups.each do |hosts|
18
- host_list = Array(hosts).join(",")
19
- run_hook "pre-traefik-reboot", hosts: host_list
20
- on(hosts) do
21
- execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
22
- execute *KAMAL.registry.login
23
- execute *KAMAL.traefik.stop
24
- execute *KAMAL.traefik.remove_container
25
- execute *KAMAL.traefik.run
16
+ confirming "This will cause a brief outage on each host. Are you sure?" do
17
+ mutating do
18
+ host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ]
19
+ host_groups.each do |hosts|
20
+ host_list = Array(hosts).join(",")
21
+ run_hook "pre-traefik-reboot", hosts: host_list
22
+ on(hosts) do
23
+ execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
24
+ execute *KAMAL.registry.login
25
+ execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
26
+ execute *KAMAL.traefik.remove_container
27
+ execute *KAMAL.traefik.run
28
+ end
29
+ run_hook "post-traefik-reboot", hosts: host_list
26
30
  end
27
- run_hook "post-traefik-reboot", hosts: host_list
28
31
  end
29
32
  end
30
33
  end
@@ -44,7 +47,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
44
47
  mutating do
45
48
  on(KAMAL.traefik_hosts) do
46
49
  execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
47
- execute *KAMAL.traefik.stop
50
+ execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
48
51
  end
49
52
  end
50
53
  end
@@ -0,0 +1,49 @@
1
+ class Kamal::Commander::Specifics
2
+ attr_reader :primary_host, :primary_role, :hosts, :roles
3
+ delegate :stable_sort!, to: Kamal::Utils
4
+
5
+ def initialize(config, specific_hosts, specific_roles)
6
+ @config, @specific_hosts, @specific_roles = config, specific_hosts, specific_roles
7
+
8
+ @roles, @hosts = specified_roles, specified_hosts
9
+
10
+ @primary_host = specific_hosts&.first || primary_specific_role&.primary_host || config.primary_host
11
+ @primary_role = primary_or_first_role(roles_on(primary_host))
12
+
13
+ stable_sort!(roles) { |role| role == primary_role ? 0 : 1 }
14
+ stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
15
+ end
16
+
17
+ def roles_on(host)
18
+ roles.select { |role| role.hosts.include?(host.to_s) }
19
+ end
20
+
21
+ def traefik_hosts
22
+ specific_hosts || config.traefik_hosts
23
+ end
24
+
25
+ def accessory_hosts
26
+ specific_hosts || config.accessories.flat_map(&:hosts)
27
+ end
28
+
29
+ private
30
+ attr_reader :config, :specific_hosts, :specific_roles
31
+
32
+ def primary_specific_role
33
+ primary_or_first_role(specific_roles) if specific_roles.present?
34
+ end
35
+
36
+ def primary_or_first_role(roles)
37
+ roles.detect { |role| role == config.primary_role } || roles.first
38
+ end
39
+
40
+ def specified_roles
41
+ (specific_roles || config.roles) \
42
+ .select { |role| ((specific_hosts || config.all_hosts) & role.hosts).any? }
43
+ end
44
+
45
+ def specified_hosts
46
+ (specific_hosts || config.all_hosts) \
47
+ .select { |host| (specific_roles || config.roles).flat_map(&:hosts).include?(host) }
48
+ end
49
+ end
@@ -3,11 +3,13 @@ require "active_support/core_ext/module/delegation"
3
3
 
4
4
  class Kamal::Commander
5
5
  attr_accessor :verbosity, :holding_lock, :hold_lock_on_error
6
+ delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :accessory_hosts, to: :specifics
6
7
 
7
8
  def initialize
8
9
  self.verbosity = :info
9
10
  self.holding_lock = false
10
11
  self.hold_lock_on_error = false
12
+ @specifics = nil
11
13
  end
12
14
 
13
15
  def config
@@ -24,10 +26,12 @@ class Kamal::Commander
24
26
  attr_reader :specific_roles, :specific_hosts
25
27
 
26
28
  def specific_primary!
29
+ @specifics = nil
27
30
  self.specific_hosts = [ config.primary_host ]
28
31
  end
29
32
 
30
33
  def specific_roles=(role_names)
34
+ @specifics = nil
31
35
  if role_names.present?
32
36
  @specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles)
33
37
 
@@ -40,6 +44,7 @@ class Kamal::Commander
40
44
  end
41
45
 
42
46
  def specific_hosts=(hosts)
47
+ @specifics = nil
43
48
  if hosts.present?
44
49
  @specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
45
50
 
@@ -51,39 +56,6 @@ class Kamal::Commander
51
56
  end
52
57
  end
53
58
 
54
- def primary_host
55
- # Given a list of specific roles, make an effort to match up with the primary_role
56
- specific_hosts&.first || specific_roles&.detect { |role| role.name == config.primary_role }&.primary_host || specific_roles&.first&.primary_host || config.primary_host
57
- end
58
-
59
- def primary_role
60
- roles_on(primary_host).first
61
- end
62
-
63
- def roles
64
- (specific_roles || config.roles).select do |role|
65
- ((specific_hosts || config.all_hosts) & role.hosts).any?
66
- end
67
- end
68
-
69
- def hosts
70
- (specific_hosts || config.all_hosts).select do |host|
71
- (specific_roles || config.roles).flat_map(&:hosts).include?(host)
72
- end
73
- end
74
-
75
- def roles_on(host)
76
- roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
77
- end
78
-
79
- def traefik_hosts
80
- specific_hosts || config.traefik_hosts
81
- end
82
-
83
- def accessory_hosts
84
- specific_hosts || config.accessories.flat_map(&:hosts)
85
- end
86
-
87
59
  def accessory_names
88
60
  config.accessories&.collect(&:name) || []
89
61
  end
@@ -181,4 +153,8 @@ class Kamal::Commander
181
153
  SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
182
154
  SSHKit.config.output_verbosity = verbosity
183
155
  end
156
+
157
+ def specifics
158
+ @specifics ||= Kamal::Commander::Specifics.new(config, specific_hosts, specific_roles)
159
+ end
184
160
  end
@@ -99,11 +99,11 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
99
99
  end
100
100
 
101
101
  def make_env_directory
102
- make_directory accessory_config.host_env_directory
102
+ make_directory accessory_config.env.secrets_directory
103
103
  end
104
104
 
105
105
  def remove_env_file
106
- [:rm, "-f", accessory_config.host_env_file_path]
106
+ [ :rm, "-f", accessory_config.env.secrets_file ]
107
107
  end
108
108
 
109
109
  private
@@ -1,23 +1,23 @@
1
1
  module Kamal::Commands::App::Assets
2
2
  def extract_assets
3
- asset_container = "#{role_config.container_prefix}-assets"
3
+ asset_container = "#{role.container_prefix}-assets"
4
4
 
5
5
  combine \
6
- make_directory(role_config.asset_extracted_path),
7
- [*docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true"],
8
- docker(:run, "--name", asset_container, "--detach", "--rm", config.latest_image, "sleep 1000000"),
9
- docker(:cp, "-L", "#{asset_container}:#{role_config.asset_path}/.", role_config.asset_extracted_path),
6
+ make_directory(role.asset_extracted_path),
7
+ [ *docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true" ],
8
+ docker(:run, "--name", asset_container, "--detach", "--rm", config.absolute_image, "sleep 1000000"),
9
+ docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_path),
10
10
  docker(:stop, "-t 1", asset_container),
11
11
  by: "&&"
12
12
  end
13
13
 
14
14
  def sync_asset_volumes(old_version: nil)
15
- new_extracted_path, new_volume_path = role_config.asset_extracted_path(config.version), role_config.asset_volume.host_path
15
+ new_extracted_path, new_volume_path = role.asset_extracted_path(config.version), role.asset_volume.host_path
16
16
  if old_version.present?
17
- old_extracted_path, old_volume_path = role_config.asset_extracted_path(old_version), role_config.asset_volume(old_version).host_path
17
+ old_extracted_path, old_volume_path = role.asset_extracted_path(old_version), role.asset_volume(old_version).host_path
18
18
  end
19
19
 
20
- commands = [make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path)]
20
+ commands = [ make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path) ]
21
21
 
22
22
  if old_version.present?
23
23
  commands << copy_contents(new_extracted_path, old_volume_path, continue_on_error: true)
@@ -29,8 +29,8 @@ module Kamal::Commands::App::Assets
29
29
 
30
30
  def clean_up_assets
31
31
  chain \
32
- find_and_remove_older_siblings(role_config.asset_extracted_path),
33
- find_and_remove_older_siblings(role_config.asset_volume_path)
32
+ find_and_remove_older_siblings(role.asset_extracted_path),
33
+ find_and_remove_older_siblings(role.asset_volume_path)
34
34
  end
35
35
 
36
36
  private
@@ -39,13 +39,13 @@ module Kamal::Commands::App::Assets
39
39
  :find,
40
40
  Pathname.new(path).dirname.to_s,
41
41
  "-maxdepth 1",
42
- "-name", "'#{role_config.container_prefix}-*'",
42
+ "-name", "'#{role.container_prefix}-*'",
43
43
  "!", "-name", Pathname.new(path).basename.to_s,
44
44
  "-exec rm -rf \"{}\" +"
45
45
  ]
46
46
  end
47
47
 
48
48
  def copy_contents(source, destination, continue_on_error: false)
49
- [ :cp, "-rnT", "#{source}", destination, *("|| true" if continue_on_error)]
49
+ [ :cp, "-rnT", "#{source}", destination, *("|| true" if continue_on_error) ]
50
50
  end
51
51
  end
@@ -2,7 +2,7 @@ module Kamal::Commands::App::Cord
2
2
  def cord(version:)
3
3
  pipe \
4
4
  docker(:inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", container_name(version)),
5
- [:awk, "'$2 == \"#{role_config.cord_volume.container_path}\" {print $1}'"]
5
+ [ :awk, "'$2 == \"#{role.cord_volume.container_path}\" {print $1}'" ]
6
6
  end
7
7
 
8
8
  def tie_cord(cord)
@@ -12,11 +12,11 @@ module Kamal::Commands::App::Cord
12
12
  def cut_cord(cord)
13
13
  remove_directory(cord)
14
14
  end
15
-
16
- private
15
+
16
+ private
17
17
  def create_empty_file(file)
18
18
  chain \
19
19
  make_directory_for(file),
20
- [:touch, file]
20
+ [ :touch, file ]
21
21
  end
22
22
  end
@@ -1,27 +1,29 @@
1
1
  module Kamal::Commands::App::Execution
2
- def execute_in_existing_container(*command, interactive: false)
2
+ def execute_in_existing_container(*command, interactive: false, env:)
3
3
  docker :exec,
4
4
  ("-it" if interactive),
5
+ *argumentize("--env", env),
5
6
  container_name,
6
7
  *command
7
8
  end
8
9
 
9
- def execute_in_new_container(*command, interactive: false)
10
+ def execute_in_new_container(*command, interactive: false, env:)
10
11
  docker :run,
11
12
  ("-it" if interactive),
12
13
  "--rm",
13
- *role_config&.env_args,
14
+ *role&.env_args,
15
+ *argumentize("--env", env),
14
16
  *config.volume_args,
15
- *role_config&.option_args,
17
+ *role&.option_args,
16
18
  config.absolute_image,
17
19
  *command
18
20
  end
19
21
 
20
- def execute_in_existing_container_over_ssh(*command, host:)
21
- run_over_ssh execute_in_existing_container(*command, interactive: true), host: host
22
+ def execute_in_existing_container_over_ssh(*command, host:, env:)
23
+ run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host
22
24
  end
23
25
 
24
- def execute_in_new_container_over_ssh(*command, host:)
25
- run_over_ssh execute_in_new_container(*command, interactive: true), host: host
26
+ def execute_in_new_container_over_ssh(*command, host:, env:)
27
+ run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host
26
28
  end
27
29
  end
@@ -7,7 +7,7 @@ module Kamal::Commands::App::Images
7
7
  docker :image, :prune, "--all", "--force", *filter_args
8
8
  end
9
9
 
10
- def tag_current_image_as_latest
10
+ def tag_latest_image
11
11
  docker :tag, config.absolute_image, config.latest_image
12
12
  end
13
13
  end
@@ -6,11 +6,11 @@ module Kamal::Commands::App::Logging
6
6
  ("grep '#{grep}'" if grep)
7
7
  end
8
8
 
9
- def follow_logs(host:, grep: nil)
9
+ def follow_logs(host:, lines: nil, grep: nil)
10
10
  run_over_ssh \
11
11
  pipe(
12
12
  current_running_container_id,
13
- "xargs docker logs --timestamps --tail 10 --follow 2>&1",
13
+ "xargs docker logs --timestamps#{" --tail #{lines}" if lines} --follow 2>&1",
14
14
  (%(grep "#{grep}") if grep)
15
15
  ),
16
16
  host: host
@@ -3,12 +3,11 @@ class Kamal::Commands::App < Kamal::Commands::Base
3
3
 
4
4
  ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
5
5
 
6
- attr_reader :role, :role_config
6
+ attr_reader :role, :role
7
7
 
8
8
  def initialize(config, role: nil)
9
9
  super(config)
10
10
  @role = role
11
- @role_config = config.role(self.role)
12
11
  end
13
12
 
14
13
  def run(hostname: nil)
@@ -16,18 +15,18 @@ class Kamal::Commands::App < Kamal::Commands::Base
16
15
  "--detach",
17
16
  "--restart unless-stopped",
18
17
  "--name", container_name,
19
- *(["--hostname", hostname] if hostname),
18
+ *([ "--hostname", hostname ] if hostname),
20
19
  "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
21
20
  "-e", "KAMAL_VERSION=\"#{config.version}\"",
22
- *role_config.env_args,
23
- *role_config.health_check_args,
24
- *config.logging_args,
21
+ *role.env_args,
22
+ *role.health_check_args,
23
+ *role.logging_args,
25
24
  *config.volume_args,
26
- *role_config.asset_volume_args,
27
- *role_config.label_args,
28
- *role_config.option_args,
25
+ *role.asset_volume_args,
26
+ *role.label_args,
27
+ *role.option_args,
29
28
  config.absolute_image,
30
- role_config.cmd
29
+ role.cmd
31
30
  end
32
31
 
33
32
  def start
@@ -50,7 +49,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
50
49
 
51
50
 
52
51
  def current_running_container_id
53
- docker :ps, "--quiet", *filter_args(statuses: ACTIVE_DOCKER_STATUSES), "--latest"
52
+ current_running_container(format: "--quiet")
54
53
  end
55
54
 
56
55
  def container_id_for_version(version, only_running: false)
@@ -58,36 +57,57 @@ class Kamal::Commands::App < Kamal::Commands::Base
58
57
  end
59
58
 
60
59
  def current_running_version
61
- list_versions("--latest", statuses: ACTIVE_DOCKER_STATUSES)
60
+ pipe \
61
+ current_running_container(format: "--format '{{.Names}}'"),
62
+ extract_version_from_name
62
63
  end
63
64
 
64
65
  def list_versions(*docker_args, statuses: nil)
65
66
  pipe \
66
67
  docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
67
- %(while read line; do echo ${line##{role_config.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
68
+ extract_version_from_name
68
69
  end
69
70
 
70
71
 
71
72
  def make_env_directory
72
- make_directory role_config.host_env_directory
73
+ make_directory role.env.secrets_directory
73
74
  end
74
75
 
75
76
  def remove_env_file
76
- [ :rm, "-f", role_config.host_env_file_path ]
77
+ [ :rm, "-f", role.env.secrets_file ]
77
78
  end
78
79
 
79
80
 
80
81
  private
81
82
  def container_name(version = nil)
82
- [ role_config.container_prefix, version || config.version ].compact.join("-")
83
+ [ role.container_prefix, version || config.version ].compact.join("-")
84
+ end
85
+
86
+ def latest_image_id
87
+ docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'"
88
+ end
89
+
90
+ def current_running_container(format:)
91
+ pipe \
92
+ shell(chain(latest_image_container(format: format), latest_container(format: format))),
93
+ [ :head, "-1" ]
94
+ end
95
+
96
+ def latest_image_container(format:)
97
+ latest_container format: format, filters: [ "ancestor=$(#{latest_image_id.join(" ")})" ]
98
+ end
99
+
100
+ def latest_container(format:, filters: nil)
101
+ docker :ps, "--latest", *format, *filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
83
102
  end
84
103
 
85
104
  def filter_args(statuses: nil)
86
105
  argumentize "--filter", filters(statuses: statuses)
87
106
  end
88
107
 
89
- def service_role_dest
90
- [ config.service, role, config.destination ].compact.join("-")
108
+ def extract_version_from_name
109
+ # Extract SHA from "service-role-dest-SHA"
110
+ %(while read line; do echo ${line##{role.container_prefix}-}; done)
91
111
  end
92
112
 
93
113
  def filters(statuses: nil)
@@ -21,7 +21,7 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base
21
21
  def audit_log_file
22
22
  file = [ config.service, config.destination, "audit.log" ].compact.join("-")
23
23
 
24
- "#{config.run_directory}/#{file}"
24
+ File.join(config.run_directory, file)
25
25
  end
26
26
 
27
27
  def audit_tags(**details)
@@ -62,14 +62,26 @@ module Kamal::Commands
62
62
  combine *commands, by: ">"
63
63
  end
64
64
 
65
+ def any(*commands)
66
+ combine *commands, by: "||"
67
+ end
68
+
65
69
  def xargs(command)
66
70
  [ :xargs, command ].flatten
67
71
  end
68
72
 
73
+ def shell(command)
74
+ [ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\\\''")}'" ]
75
+ end
76
+
69
77
  def docker(*args)
70
78
  args.compact.unshift :docker
71
79
  end
72
80
 
81
+ def git(*args)
82
+ args.compact.unshift :git
83
+ end
84
+
73
85
  def tags(**details)
74
86
  Kamal::Tags.from_config(config, **details)
75
87
  end
@@ -3,7 +3,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
3
3
  class BuilderError < StandardError; end
4
4
 
5
5
  delegate :argumentize, to: Kamal::Utils
6
- delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, to: :builder_config
6
+ delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, :git_archive?, to: :builder_config
7
7
 
8
8
  def clean
9
9
  docker :image, :rm, "--force", config.absolute_image
@@ -13,8 +13,18 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
13
13
  docker :pull, config.absolute_image
14
14
  end
15
15
 
16
+ def push
17
+ if git_archive?
18
+ pipe \
19
+ git(:archive, "--format=tar", :HEAD),
20
+ build_and_push
21
+ else
22
+ build_and_push
23
+ end
24
+ end
25
+
16
26
  def build_options
17
- [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile ]
27
+ [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_ssh ]
18
28
  end
19
29
 
20
30
  def build_context
@@ -24,7 +34,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
24
34
  def validate_image
25
35
  pipe \
26
36
  docker(:inspect, "-f", "'{{ .Config.Labels.service }}'", config.absolute_image),
27
- [:grep, "-x", config.service, "||", "(echo \"Image #{config.absolute_image} is missing the `service` label\" && exit 1)"]
37
+ any(
38
+ [ :grep, "-x", config.service ],
39
+ "(echo \"Image #{config.absolute_image} is missing the 'service' label\" && exit 1)"
40
+ )
28
41
  end
29
42
 
30
43
 
@@ -35,8 +48,8 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
35
48
 
36
49
  def build_cache
37
50
  if cache_to && cache_from
38
- ["--cache-to", cache_to,
39
- "--cache-from", cache_from]
51
+ [ "--cache-to", cache_to,
52
+ "--cache-from", cache_from ]
40
53
  end
41
54
  end
42
55
 
@@ -60,6 +73,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
60
73
  end
61
74
  end
62
75
 
76
+ def build_ssh
77
+ argumentize "--ssh", ssh if ssh.present?
78
+ end
79
+
63
80
  def builder_config
64
81
  config.builder
65
82
  end
@@ -7,15 +7,6 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
7
7
  docker :buildx, :rm, builder_name
8
8
  end
9
9
 
10
- def push
11
- docker :buildx, :build,
12
- "--push",
13
- "--platform", "linux/amd64,linux/arm64",
14
- "--builder", builder_name,
15
- *build_options,
16
- build_context
17
- end
18
-
19
10
  def info
20
11
  combine \
21
12
  docker(:context, :ls),
@@ -26,4 +17,21 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
26
17
  def builder_name
27
18
  "kamal-#{config.service}-multiarch"
28
19
  end
20
+
21
+ def platform_names
22
+ if local_arch
23
+ "linux/#{local_arch}"
24
+ else
25
+ "linux/amd64,linux/arm64"
26
+ end
27
+ end
28
+
29
+ def build_and_push
30
+ docker :buildx, :build,
31
+ "--push",
32
+ "--platform", platform_names,
33
+ "--builder", builder_name,
34
+ *build_options,
35
+ build_context
36
+ end
29
37
  end
@@ -7,10 +7,11 @@ class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Nativ
7
7
  docker :buildx, :rm, builder_name
8
8
  end
9
9
 
10
- def push
11
- docker :buildx, :build,
12
- "--push",
13
- *build_options,
14
- build_context
15
- end
10
+ private
11
+ def build_and_push
12
+ docker :buildx, :build,
13
+ "--push",
14
+ *build_options,
15
+ build_context
16
+ end
16
17
  end
@@ -11,15 +11,6 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ
11
11
  remove_buildx
12
12
  end
13
13
 
14
- def push
15
- docker :buildx, :build,
16
- "--push",
17
- "--platform", platform,
18
- "--builder", builder_name,
19
- *build_options,
20
- build_context
21
- end
22
-
23
14
  def info
24
15
  chain \
25
16
  docker(:context, :ls),
@@ -56,4 +47,13 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ
56
47
  def remove_buildx
57
48
  docker :buildx, :rm, builder_name
58
49
  end
50
+
51
+ def build_and_push
52
+ docker :buildx, :build,
53
+ "--push",
54
+ "--platform", platform,
55
+ "--builder", builder_name,
56
+ *build_options,
57
+ build_context
58
+ end
59
59
  end
@@ -7,14 +7,15 @@ class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base
7
7
  # No-op on native without cache
8
8
  end
9
9
 
10
- def push
11
- combine \
12
- docker(:build, *build_options, build_context),
13
- docker(:push, config.absolute_image),
14
- docker(:push, config.latest_image)
15
- end
16
-
17
10
  def info
18
11
  # No-op on native
19
12
  end
13
+
14
+ private
15
+ def build_and_push
16
+ combine \
17
+ docker(:build, *build_options, build_context),
18
+ docker(:push, config.absolute_image),
19
+ docker(:push, config.latest_image)
20
+ end
20
21
  end