kamal 0.16.1 → 1.1.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/kamal/cli/app.rb +44 -13
  4. data/lib/kamal/cli/base.rb +15 -2
  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 +18 -4
  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 +18 -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/docker.rb +1 -1
  28. data/lib/kamal/commands/healthcheck.rb +15 -12
  29. data/lib/kamal/commands/lock.rb +2 -2
  30. data/lib/kamal/commands/prune.rb +11 -3
  31. data/lib/kamal/commands/server.rb +5 -0
  32. data/lib/kamal/commands/traefik.rb +21 -7
  33. data/lib/kamal/configuration/accessory.rb +14 -2
  34. data/lib/kamal/configuration/role.rb +112 -19
  35. data/lib/kamal/configuration/ssh.rb +1 -1
  36. data/lib/kamal/configuration/volume.rb +22 -0
  37. data/lib/kamal/configuration.rb +73 -44
  38. data/lib/kamal/env_file.rb +41 -0
  39. data/lib/kamal/git.rb +19 -0
  40. data/lib/kamal/utils/sensitive.rb +1 -0
  41. data/lib/kamal/utils.rb +0 -39
  42. data/lib/kamal/version.rb +1 -1
  43. metadata +15 -4
  44. data/lib/kamal/utils/healthcheck_poller.rb +0 -39
@@ -0,0 +1,23 @@
1
+ module Kamal::Commands::App::Containers
2
+ def list_containers
3
+ docker :container, :ls, "--all", *filter_args
4
+ end
5
+
6
+ def list_container_names
7
+ [ *list_containers, "--format", "'{{ .Names }}'" ]
8
+ end
9
+
10
+ def remove_container(version:)
11
+ pipe \
12
+ container_id_for(container_name: container_name(version)),
13
+ xargs(docker(:container, :rm))
14
+ end
15
+
16
+ def rename_container(version:, new_version:)
17
+ docker :rename, container_name(version), container_name(new_version)
18
+ end
19
+
20
+ def remove_containers
21
+ docker :container, :prune, "--force", *filter_args
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ module Kamal::Commands::App::Cord
2
+ def cord(version:)
3
+ pipe \
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}'"]
6
+ end
7
+
8
+ def tie_cord(cord)
9
+ create_empty_file(cord)
10
+ end
11
+
12
+ def cut_cord(cord)
13
+ remove_directory(cord)
14
+ end
15
+
16
+ private
17
+ def create_empty_file(file)
18
+ chain \
19
+ make_directory_for(file),
20
+ [:touch, file]
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ module Kamal::Commands::App::Execution
2
+ def execute_in_existing_container(*command, interactive: false)
3
+ docker :exec,
4
+ ("-it" if interactive),
5
+ container_name,
6
+ *command
7
+ end
8
+
9
+ def execute_in_new_container(*command, interactive: false)
10
+ docker :run,
11
+ ("-it" if interactive),
12
+ "--rm",
13
+ *role_config&.env_args,
14
+ *config.volume_args,
15
+ *role_config&.option_args,
16
+ config.absolute_image,
17
+ *command
18
+ end
19
+
20
+ def execute_in_existing_container_over_ssh(*command, host:)
21
+ run_over_ssh execute_in_existing_container(*command, interactive: true), host: host
22
+ end
23
+
24
+ def execute_in_new_container_over_ssh(*command, host:)
25
+ run_over_ssh execute_in_new_container(*command, interactive: true), host: host
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ module Kamal::Commands::App::Images
2
+ def list_images
3
+ docker :image, :ls, config.repository
4
+ end
5
+
6
+ def remove_images
7
+ docker :image, :prune, "--all", "--force", *filter_args
8
+ end
9
+
10
+ def tag_current_image_as_latest
11
+ docker :tag, config.absolute_image, config.latest_image
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module Kamal::Commands::App::Logging
2
+ def logs(since: nil, lines: nil, grep: nil)
3
+ pipe \
4
+ current_running_container_id,
5
+ "xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
6
+ ("grep '#{grep}'" if grep)
7
+ end
8
+
9
+ def follow_logs(host:, grep: nil)
10
+ run_over_ssh \
11
+ pipe(
12
+ current_running_container_id,
13
+ "xargs docker logs --timestamps --tail 10 --follow 2>&1",
14
+ (%(grep "#{grep}") if grep)
15
+ ),
16
+ host: host
17
+ end
18
+ end
@@ -1,34 +1,33 @@
1
1
  class Kamal::Commands::App < Kamal::Commands::Base
2
+ include Assets, Containers, Cord, Execution, Images, Logging
3
+
2
4
  ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
3
5
 
4
- attr_reader :role
6
+ attr_reader :role, :role_config
5
7
 
6
8
  def initialize(config, role: nil)
7
9
  super(config)
8
10
  @role = role
9
- end
10
-
11
- def start_or_run(hostname: nil)
12
- combine start, run(hostname: hostname), by: "||"
11
+ @role_config = config.role(self.role)
13
12
  end
14
13
 
15
14
  def run(hostname: nil)
16
- role = config.role(self.role)
17
-
18
15
  docker :run,
19
16
  "--detach",
20
17
  "--restart unless-stopped",
21
18
  "--name", container_name,
22
19
  *(["--hostname", hostname] if hostname),
23
20
  "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
24
- *role.env_args,
25
- *role.health_check_args,
21
+ "-e", "KAMAL_VERSION=\"#{config.version}\"",
22
+ *role_config.env_args,
23
+ *role_config.health_check_args,
26
24
  *config.logging_args,
27
25
  *config.volume_args,
28
- *role.label_args,
29
- *role.option_args,
26
+ *role_config.asset_volume_args,
27
+ *role_config.label_args,
28
+ *role_config.option_args,
30
29
  config.absolute_image,
31
- role.cmd
30
+ role_config.cmd
32
31
  end
33
32
 
34
33
  def start
@@ -50,53 +49,6 @@ class Kamal::Commands::App < Kamal::Commands::Base
50
49
  end
51
50
 
52
51
 
53
- def logs(since: nil, lines: nil, grep: nil)
54
- pipe \
55
- current_running_container_id,
56
- "xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
57
- ("grep '#{grep}'" if grep)
58
- end
59
-
60
- def follow_logs(host:, grep: nil)
61
- run_over_ssh \
62
- pipe(
63
- current_running_container_id,
64
- "xargs docker logs --timestamps --tail 10 --follow 2>&1",
65
- (%(grep "#{grep}") if grep)
66
- ),
67
- host: host
68
- end
69
-
70
-
71
- def execute_in_existing_container(*command, interactive: false)
72
- docker :exec,
73
- ("-it" if interactive),
74
- container_name,
75
- *command
76
- end
77
-
78
- def execute_in_new_container(*command, interactive: false)
79
- role = config.role(self.role)
80
-
81
- docker :run,
82
- ("-it" if interactive),
83
- "--rm",
84
- *config.env_args,
85
- *config.volume_args,
86
- *role&.option_args,
87
- config.absolute_image,
88
- *command
89
- end
90
-
91
- def execute_in_existing_container_over_ssh(*command, host:)
92
- run_over_ssh execute_in_existing_container(*command, interactive: true), host: host
93
- end
94
-
95
- def execute_in_new_container_over_ssh(*command, host:)
96
- run_over_ssh execute_in_new_container(*command, interactive: true), host: host
97
- end
98
-
99
-
100
52
  def current_running_container_id
101
53
  docker :ps, "--quiet", *filter_args(statuses: ACTIVE_DOCKER_STATUSES), "--latest"
102
54
  end
@@ -112,47 +64,22 @@ class Kamal::Commands::App < Kamal::Commands::Base
112
64
  def list_versions(*docker_args, statuses: nil)
113
65
  pipe \
114
66
  docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
115
- %(while read line; do echo ${line##{service_role_dest}-}; done) # Extract SHA from "service-role-dest-SHA"
116
- end
117
-
118
- def list_containers
119
- docker :container, :ls, "--all", *filter_args
120
- end
121
-
122
- def list_container_names
123
- [ *list_containers, "--format", "'{{ .Names }}'" ]
67
+ %(while read line; do echo ${line##{role_config.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
124
68
  end
125
69
 
126
- def remove_container(version:)
127
- pipe \
128
- container_id_for(container_name: container_name(version)),
129
- xargs(docker(:container, :rm))
130
- end
131
-
132
- def rename_container(version:, new_version:)
133
- docker :rename, container_name(version), container_name(new_version)
134
- end
135
-
136
- def remove_containers
137
- docker :container, :prune, "--force", *filter_args
138
- end
139
-
140
- def list_images
141
- docker :image, :ls, config.repository
142
- end
143
70
 
144
- def remove_images
145
- docker :image, :prune, "--all", "--force", *filter_args
71
+ def make_env_directory
72
+ make_directory role_config.host_env_directory
146
73
  end
147
74
 
148
- def tag_current_as_latest
149
- docker :tag, config.absolute_image, config.latest_image
75
+ def remove_env_file
76
+ [ :rm, "-f", role_config.host_env_file_path ]
150
77
  end
151
78
 
152
79
 
153
80
  private
154
81
  def container_name(version = nil)
155
- [ config.service, role, config.destination, version || config.version ].compact.join("-")
82
+ [ role_config.container_prefix, version || config.version ].compact.join("-")
156
83
  end
157
84
 
158
85
  def filter_args(statuses: nil)
@@ -160,7 +87,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
160
87
  end
161
88
 
162
89
  def service_role_dest
163
- [config.service, role, config.destination].compact.join("-")
90
+ [ config.service, role, config.destination ].compact.join("-")
164
91
  end
165
92
 
166
93
  def filters(statuses: nil)
@@ -19,7 +19,9 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base
19
19
 
20
20
  private
21
21
  def audit_log_file
22
- [ "kamal", config.service, config.destination, "audit.log" ].compact.join("-")
22
+ file = [ config.service, config.destination, "audit.log" ].compact.join("-")
23
+
24
+ "#{config.run_directory}/#{file}"
23
25
  end
24
26
 
25
27
  def audit_tags(**details)
@@ -26,6 +26,18 @@ module Kamal::Commands
26
26
  docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet"
27
27
  end
28
28
 
29
+ def make_directory_for(remote_file)
30
+ make_directory Pathname.new(remote_file).dirname.to_s
31
+ end
32
+
33
+ def make_directory(path)
34
+ [ :mkdir, "-p", path ]
35
+ end
36
+
37
+ def remove_directory(path)
38
+ [ :rm, "-r", path ]
39
+ end
40
+
29
41
  private
30
42
  def combine(*commands, by: "&&")
31
43
  commands
@@ -21,6 +21,12 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
21
21
  config.builder.context
22
22
  end
23
23
 
24
+ def validate_image
25
+ pipe \
26
+ 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)"]
28
+ end
29
+
24
30
 
25
31
  private
26
32
  def build_tags
@@ -1,7 +1,7 @@
1
1
  require "active_support/core_ext/string/filters"
2
2
 
3
3
  class Kamal::Commands::Builder < Kamal::Commands::Base
4
- delegate :create, :remove, :push, :clean, :pull, :info, to: :target
4
+ delegate :create, :remove, :push, :clean, :pull, :info, :validate_image, to: :target
5
5
 
6
6
  def name
7
7
  target.class.to_s.remove("Kamal::Commands::Builder::").underscore.inquiry
@@ -16,6 +16,6 @@ class Kamal::Commands::Docker < Kamal::Commands::Base
16
16
 
17
17
  # Do we have superuser access to install Docker and start system services?
18
18
  def superuser?
19
- [ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
19
+ [ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ]
20
20
  end
21
21
  end
@@ -1,5 +1,4 @@
1
1
  class Kamal::Commands::Healthcheck < Kamal::Commands::Base
2
- EXPOSED_PORT = 3999
3
2
 
4
3
  def run
5
4
  web = config.role(:web)
@@ -7,11 +6,11 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
7
6
  docker :run,
8
7
  "--detach",
9
8
  "--name", container_name_with_version,
10
- "--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
11
- "--label", "service=#{container_name}",
12
- "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
9
+ "--publish", "#{exposed_port}:#{config.healthcheck["port"]}",
10
+ "--label", "service=#{config.healthcheck_service}",
11
+ "-e", "KAMAL_CONTAINER_NAME=\"#{config.healthcheck_service}\"",
13
12
  *web.env_args,
14
- *web.health_check_args,
13
+ *web.health_check_args(cord: false),
15
14
  *config.volume_args,
16
15
  *web.option_args,
17
16
  config.absolute_image,
@@ -27,7 +26,7 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
27
26
  end
28
27
 
29
28
  def logs
30
- pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1"))
29
+ pipe container_id, xargs(docker(:logs, "--tail", log_lines, "2>&1"))
31
30
  end
32
31
 
33
32
  def stop
@@ -39,12 +38,8 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
39
38
  end
40
39
 
41
40
  private
42
- def container_name
43
- [ "healthcheck", config.service, config.destination ].compact.join("-")
44
- end
45
-
46
41
  def container_name_with_version
47
- "#{container_name}-#{config.version}"
42
+ "#{config.healthcheck_service}-#{config.version}"
48
43
  end
49
44
 
50
45
  def container_id
@@ -52,6 +47,14 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
52
47
  end
53
48
 
54
49
  def health_url
55
- "http://localhost:#{EXPOSED_PORT}#{config.healthcheck["path"]}"
50
+ "http://localhost:#{exposed_port}#{config.healthcheck["path"]}"
51
+ end
52
+
53
+ def exposed_port
54
+ config.healthcheck["exposed_port"]
55
+ end
56
+
57
+ def log_lines
58
+ config.healthcheck["log_lines"]
56
59
  end
57
60
  end
@@ -40,7 +40,7 @@ class Kamal::Commands::Lock < Kamal::Commands::Base
40
40
  end
41
41
 
42
42
  def lock_dir
43
- "kamal_lock-#{config.service}"
43
+ "#{config.run_directory}/lock-#{config.service}"
44
44
  end
45
45
 
46
46
  def lock_details_file
@@ -56,7 +56,7 @@ class Kamal::Commands::Lock < Kamal::Commands::Base
56
56
  end
57
57
 
58
58
  def locked_by
59
- `git config user.name`.strip
59
+ Kamal::Git.user_name
60
60
  rescue Errno::ENOENT
61
61
  "Unknown"
62
62
  end
@@ -3,7 +3,7 @@ require "active_support/core_ext/numeric/time"
3
3
 
4
4
  class Kamal::Commands::Prune < Kamal::Commands::Base
5
5
  def dangling_images
6
- docker :image, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
6
+ docker :image, :prune, "--force", "--filter", "label=service=#{config.service}"
7
7
  end
8
8
 
9
9
  def tagged_images
@@ -13,13 +13,17 @@ 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 containers(keep_last: 5)
16
+ def app_containers(keep_last: 5)
17
17
  pipe \
18
18
  docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
19
19
  "tail -n +#{keep_last + 1}",
20
20
  "while read container_id; do docker rm $container_id; done"
21
21
  end
22
22
 
23
+ def healthcheck_containers
24
+ docker :container, :prune, "--force", *healthcheck_service_filter
25
+ end
26
+
23
27
  private
24
28
  def stopped_containers_filters
25
29
  [ "created", "exited", "dead" ].flat_map { |status| ["--filter", "status=#{status}"] }
@@ -35,4 +39,8 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
35
39
  def service_filter
36
40
  [ "--filter", "label=service=#{config.service}" ]
37
41
  end
38
- end
42
+
43
+ def healthcheck_service_filter
44
+ [ "--filter", "label=service=#{config.healthcheck_service}" ]
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ class Kamal::Commands::Server < Kamal::Commands::Base
2
+ def ensure_run_directory
3
+ [:mkdir, "-p", config.run_directory]
4
+ end
5
+ end
@@ -1,5 +1,5 @@
1
1
  class Kamal::Commands::Traefik < Kamal::Commands::Base
2
- delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
2
+ delegate :argumentize, :optionize, to: Kamal::Utils
3
3
 
4
4
  DEFAULT_IMAGE = "traefik:v2.9"
5
5
  CONTAINER_PORT = 80
@@ -63,6 +63,22 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
63
63
  "#{host_port}:#{CONTAINER_PORT}"
64
64
  end
65
65
 
66
+ def env_file
67
+ Kamal::EnvFile.new(config.traefik.fetch("env", {}))
68
+ end
69
+
70
+ def host_env_file_path
71
+ File.join host_env_directory, "traefik.env"
72
+ end
73
+
74
+ def make_env_directory
75
+ make_directory(host_env_directory)
76
+ end
77
+
78
+ def remove_env_file
79
+ [:rm, "-f", host_env_file_path]
80
+ end
81
+
66
82
  private
67
83
  def publish_args
68
84
  argumentize "--publish", port unless config.traefik["publish"] == false
@@ -73,13 +89,11 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
73
89
  end
74
90
 
75
91
  def env_args
76
- env_config = config.traefik["env"] || {}
92
+ argumentize "--env-file", host_env_file_path
93
+ end
77
94
 
78
- if env_config.present?
79
- argumentize_env_with_secrets(env_config)
80
- else
81
- []
82
- end
95
+ def host_env_directory
96
+ File.join config.host_env_directory, "traefik"
83
97
  end
84
98
 
85
99
  def labels
@@ -1,5 +1,5 @@
1
1
  class Kamal::Configuration::Accessory
2
- delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
2
+ delegate :argumentize, :optionize, to: Kamal::Utils
3
3
 
4
4
  attr_accessor :name, :specifics
5
5
 
@@ -45,8 +45,20 @@ class Kamal::Configuration::Accessory
45
45
  specifics["env"] || {}
46
46
  end
47
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"
58
+ end
59
+
48
60
  def env_args
49
- argumentize_env_with_secrets env
61
+ argumentize "--env-file", host_env_file_path
50
62
  end
51
63
 
52
64
  def files