kamal 0.16.1 → 1.1.0

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