kamal 0.16.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1021 -0
- data/bin/kamal +18 -0
- data/lib/kamal/cli/accessory.rb +239 -0
- data/lib/kamal/cli/app.rb +296 -0
- data/lib/kamal/cli/base.rb +171 -0
- data/lib/kamal/cli/build.rb +106 -0
- data/lib/kamal/cli/healthcheck.rb +20 -0
- data/lib/kamal/cli/lock.rb +37 -0
- data/lib/kamal/cli/main.rb +249 -0
- data/lib/kamal/cli/prune.rb +30 -0
- data/lib/kamal/cli/registry.rb +18 -0
- data/lib/kamal/cli/server.rb +21 -0
- data/lib/kamal/cli/templates/deploy.yml +74 -0
- data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +109 -0
- data/lib/kamal/cli/templates/template.env +2 -0
- data/lib/kamal/cli/traefik.rb +111 -0
- data/lib/kamal/cli.rb +7 -0
- data/lib/kamal/commander.rb +154 -0
- data/lib/kamal/commands/accessory.rb +113 -0
- data/lib/kamal/commands/app.rb +175 -0
- data/lib/kamal/commands/auditor.rb +28 -0
- data/lib/kamal/commands/base.rb +65 -0
- data/lib/kamal/commands/builder/base.rb +60 -0
- data/lib/kamal/commands/builder/multiarch/remote.rb +51 -0
- data/lib/kamal/commands/builder/multiarch.rb +29 -0
- data/lib/kamal/commands/builder/native/cached.rb +16 -0
- data/lib/kamal/commands/builder/native/remote.rb +59 -0
- data/lib/kamal/commands/builder/native.rb +20 -0
- data/lib/kamal/commands/builder.rb +62 -0
- data/lib/kamal/commands/docker.rb +21 -0
- data/lib/kamal/commands/healthcheck.rb +57 -0
- data/lib/kamal/commands/hook.rb +14 -0
- data/lib/kamal/commands/lock.rb +63 -0
- data/lib/kamal/commands/prune.rb +38 -0
- data/lib/kamal/commands/registry.rb +20 -0
- data/lib/kamal/commands/traefik.rb +104 -0
- data/lib/kamal/commands.rb +2 -0
- data/lib/kamal/configuration/accessory.rb +169 -0
- data/lib/kamal/configuration/boot.rb +20 -0
- data/lib/kamal/configuration/builder.rb +114 -0
- data/lib/kamal/configuration/role.rb +155 -0
- data/lib/kamal/configuration/ssh.rb +38 -0
- data/lib/kamal/configuration/sshkit.rb +20 -0
- data/lib/kamal/configuration.rb +251 -0
- data/lib/kamal/sshkit_with_ext.rb +104 -0
- data/lib/kamal/tags.rb +39 -0
- data/lib/kamal/utils/healthcheck_poller.rb +39 -0
- data/lib/kamal/utils/sensitive.rb +19 -0
- data/lib/kamal/utils.rb +100 -0
- data/lib/kamal/version.rb +3 -0
- data/lib/kamal.rb +10 -0
- metadata +266 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
require "active_support/duration"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
class Kamal::Commands::Lock < Kamal::Commands::Base
|
5
|
+
def acquire(message, version)
|
6
|
+
combine \
|
7
|
+
[:mkdir, lock_dir],
|
8
|
+
write_lock_details(message, version)
|
9
|
+
end
|
10
|
+
|
11
|
+
def release
|
12
|
+
combine \
|
13
|
+
[:rm, lock_details_file],
|
14
|
+
[:rm, "-r", lock_dir]
|
15
|
+
end
|
16
|
+
|
17
|
+
def status
|
18
|
+
combine \
|
19
|
+
stat_lock_dir,
|
20
|
+
read_lock_details
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def write_lock_details(message, version)
|
25
|
+
write \
|
26
|
+
[:echo, "\"#{Base64.encode64(lock_details(message, version))}\""],
|
27
|
+
lock_details_file
|
28
|
+
end
|
29
|
+
|
30
|
+
def read_lock_details
|
31
|
+
pipe \
|
32
|
+
[:cat, lock_details_file],
|
33
|
+
[:base64, "-d"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def stat_lock_dir
|
37
|
+
write \
|
38
|
+
[:stat, lock_dir],
|
39
|
+
"/dev/null"
|
40
|
+
end
|
41
|
+
|
42
|
+
def lock_dir
|
43
|
+
"kamal_lock-#{config.service}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def lock_details_file
|
47
|
+
[lock_dir, :details].join("/")
|
48
|
+
end
|
49
|
+
|
50
|
+
def lock_details(message, version)
|
51
|
+
<<~DETAILS.strip
|
52
|
+
Locked by: #{locked_by} at #{Time.now.utc.iso8601}
|
53
|
+
Version: #{version}
|
54
|
+
Message: #{message}
|
55
|
+
DETAILS
|
56
|
+
end
|
57
|
+
|
58
|
+
def locked_by
|
59
|
+
`git config user.name`.strip
|
60
|
+
rescue Errno::ENOENT
|
61
|
+
"Unknown"
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "active_support/duration"
|
2
|
+
require "active_support/core_ext/numeric/time"
|
3
|
+
|
4
|
+
class Kamal::Commands::Prune < Kamal::Commands::Base
|
5
|
+
def dangling_images
|
6
|
+
docker :image, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
|
7
|
+
end
|
8
|
+
|
9
|
+
def tagged_images
|
10
|
+
pipe \
|
11
|
+
docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"),
|
12
|
+
"grep -v -w \"#{active_image_list}\"",
|
13
|
+
"while read image tag; do docker rmi $tag; done"
|
14
|
+
end
|
15
|
+
|
16
|
+
def containers(keep_last: 5)
|
17
|
+
pipe \
|
18
|
+
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
|
19
|
+
"tail -n +#{keep_last + 1}",
|
20
|
+
"while read container_id; do docker rm $container_id; done"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def stopped_containers_filters
|
25
|
+
[ "created", "exited", "dead" ].flat_map { |status| ["--filter", "status=#{status}"] }
|
26
|
+
end
|
27
|
+
|
28
|
+
def active_image_list
|
29
|
+
# Pull the images that are used by any containers
|
30
|
+
# Append repo:latest - to avoid deleting the latest tag
|
31
|
+
# Append repo:<none> - to avoid deleting dangling images that are in use. Unused dangling images are deleted separately
|
32
|
+
"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=#{config.service} | tr -d '\\n')#{config.latest_image}\\|#{config.repository}:<none>"
|
33
|
+
end
|
34
|
+
|
35
|
+
def service_filter
|
36
|
+
[ "--filter", "label=service=#{config.service}" ]
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Kamal::Commands::Registry < Kamal::Commands::Base
|
2
|
+
delegate :registry, to: :config
|
3
|
+
|
4
|
+
def login
|
5
|
+
docker :login, registry["server"], "-u", sensitive(lookup("username")), "-p", sensitive(lookup("password"))
|
6
|
+
end
|
7
|
+
|
8
|
+
def logout
|
9
|
+
docker :logout, registry["server"]
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def lookup(key)
|
14
|
+
if registry[key].is_a?(Array)
|
15
|
+
ENV.fetch(registry[key].first).dup
|
16
|
+
else
|
17
|
+
registry[key]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
class Kamal::Commands::Traefik < Kamal::Commands::Base
|
2
|
+
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
3
|
+
|
4
|
+
DEFAULT_IMAGE = "traefik:v2.9"
|
5
|
+
CONTAINER_PORT = 80
|
6
|
+
DEFAULT_ARGS = {
|
7
|
+
'log.level' => 'DEBUG'
|
8
|
+
}
|
9
|
+
|
10
|
+
def run
|
11
|
+
docker :run, "--name traefik",
|
12
|
+
"--detach",
|
13
|
+
"--restart", "unless-stopped",
|
14
|
+
"--publish", port,
|
15
|
+
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
16
|
+
*env_args,
|
17
|
+
*config.logging_args,
|
18
|
+
*label_args,
|
19
|
+
*docker_options_args,
|
20
|
+
image,
|
21
|
+
"--providers.docker",
|
22
|
+
*cmd_option_args
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
docker :container, :start, "traefik"
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop
|
30
|
+
docker :container, :stop, "traefik"
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_or_run
|
34
|
+
combine start, run, by: "||"
|
35
|
+
end
|
36
|
+
|
37
|
+
def info
|
38
|
+
docker :ps, "--filter", "name=^traefik$"
|
39
|
+
end
|
40
|
+
|
41
|
+
def logs(since: nil, lines: nil, grep: nil)
|
42
|
+
pipe \
|
43
|
+
docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
|
44
|
+
("grep '#{grep}'" if grep)
|
45
|
+
end
|
46
|
+
|
47
|
+
def follow_logs(host:, grep: nil)
|
48
|
+
run_over_ssh pipe(
|
49
|
+
docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"),
|
50
|
+
(%(grep "#{grep}") if grep)
|
51
|
+
).join(" "), host: host
|
52
|
+
end
|
53
|
+
|
54
|
+
def remove_container
|
55
|
+
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
|
56
|
+
end
|
57
|
+
|
58
|
+
def remove_image
|
59
|
+
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
|
60
|
+
end
|
61
|
+
|
62
|
+
def port
|
63
|
+
"#{host_port}:#{CONTAINER_PORT}"
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def label_args
|
68
|
+
argumentize "--label", labels
|
69
|
+
end
|
70
|
+
|
71
|
+
def env_args
|
72
|
+
env_config = config.traefik["env"] || {}
|
73
|
+
|
74
|
+
if env_config.present?
|
75
|
+
argumentize_env_with_secrets(env_config)
|
76
|
+
else
|
77
|
+
[]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def labels
|
82
|
+
config.traefik["labels"] || []
|
83
|
+
end
|
84
|
+
|
85
|
+
def image
|
86
|
+
config.traefik.fetch("image") { DEFAULT_IMAGE }
|
87
|
+
end
|
88
|
+
|
89
|
+
def docker_options_args
|
90
|
+
optionize(config.traefik["options"] || {})
|
91
|
+
end
|
92
|
+
|
93
|
+
def cmd_option_args
|
94
|
+
if args = config.traefik["args"]
|
95
|
+
optionize DEFAULT_ARGS.merge(args), with: "="
|
96
|
+
else
|
97
|
+
optionize DEFAULT_ARGS, with: "="
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def host_port
|
102
|
+
config.traefik["host_port"] || CONTAINER_PORT
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
class Kamal::Configuration::Accessory
|
2
|
+
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
3
|
+
|
4
|
+
attr_accessor :name, :specifics
|
5
|
+
|
6
|
+
def initialize(name, config:)
|
7
|
+
@name, @config, @specifics = name.inquiry, config, config.raw_config["accessories"][name]
|
8
|
+
end
|
9
|
+
|
10
|
+
def service_name
|
11
|
+
"#{config.service}-#{name}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def image
|
15
|
+
specifics["image"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def hosts
|
19
|
+
if (specifics.keys & ["host", "hosts", "roles"]).size != 1
|
20
|
+
raise ArgumentError, "Specify one of `host`, `hosts` or `roles` for accessory `#{name}`"
|
21
|
+
end
|
22
|
+
|
23
|
+
hosts_from_host || hosts_from_hosts || hosts_from_roles
|
24
|
+
end
|
25
|
+
|
26
|
+
def port
|
27
|
+
if port = specifics["port"]&.to_s
|
28
|
+
port.include?(":") ? port : "#{port}:#{port}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def publish_args
|
33
|
+
argumentize "--publish", port if port
|
34
|
+
end
|
35
|
+
|
36
|
+
def labels
|
37
|
+
default_labels.merge(specifics["labels"] || {})
|
38
|
+
end
|
39
|
+
|
40
|
+
def label_args
|
41
|
+
argumentize "--label", labels
|
42
|
+
end
|
43
|
+
|
44
|
+
def env
|
45
|
+
specifics["env"] || {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def env_args
|
49
|
+
argumentize_env_with_secrets env
|
50
|
+
end
|
51
|
+
|
52
|
+
def files
|
53
|
+
specifics["files"]&.to_h do |local_to_remote_mapping|
|
54
|
+
local_file, remote_file = local_to_remote_mapping.split(":")
|
55
|
+
[ expand_local_file(local_file), expand_remote_file(remote_file) ]
|
56
|
+
end || {}
|
57
|
+
end
|
58
|
+
|
59
|
+
def directories
|
60
|
+
specifics["directories"]&.to_h do |host_to_container_mapping|
|
61
|
+
host_relative_path, container_path = host_to_container_mapping.split(":")
|
62
|
+
[ expand_host_path(host_relative_path), container_path ]
|
63
|
+
end || {}
|
64
|
+
end
|
65
|
+
|
66
|
+
def volumes
|
67
|
+
specific_volumes + remote_files_as_volumes + remote_directories_as_volumes
|
68
|
+
end
|
69
|
+
|
70
|
+
def volume_args
|
71
|
+
argumentize "--volume", volumes
|
72
|
+
end
|
73
|
+
|
74
|
+
def option_args
|
75
|
+
if args = specifics["options"]
|
76
|
+
optionize args
|
77
|
+
else
|
78
|
+
[]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def cmd
|
83
|
+
specifics["cmd"]
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
attr_accessor :config
|
88
|
+
|
89
|
+
def default_labels
|
90
|
+
{ "service" => service_name }
|
91
|
+
end
|
92
|
+
|
93
|
+
def expand_local_file(local_file)
|
94
|
+
if local_file.end_with?("erb")
|
95
|
+
with_clear_env_loaded { read_dynamic_file(local_file) }
|
96
|
+
else
|
97
|
+
Pathname.new(File.expand_path(local_file)).to_s
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def with_clear_env_loaded
|
102
|
+
(env["clear"] || env).each { |k, v| ENV[k] = v }
|
103
|
+
yield
|
104
|
+
ensure
|
105
|
+
(env["clear"] || env).each { |k, v| ENV.delete(k) }
|
106
|
+
end
|
107
|
+
|
108
|
+
def read_dynamic_file(local_file)
|
109
|
+
StringIO.new(ERB.new(IO.read(local_file)).result)
|
110
|
+
end
|
111
|
+
|
112
|
+
def expand_remote_file(remote_file)
|
113
|
+
service_name + remote_file
|
114
|
+
end
|
115
|
+
|
116
|
+
def specific_volumes
|
117
|
+
specifics["volumes"] || []
|
118
|
+
end
|
119
|
+
|
120
|
+
def remote_files_as_volumes
|
121
|
+
specifics["files"]&.collect do |local_to_remote_mapping|
|
122
|
+
_, remote_file = local_to_remote_mapping.split(":")
|
123
|
+
"#{service_data_directory + remote_file}:#{remote_file}"
|
124
|
+
end || []
|
125
|
+
end
|
126
|
+
|
127
|
+
def remote_directories_as_volumes
|
128
|
+
specifics["directories"]&.collect do |host_to_container_mapping|
|
129
|
+
host_relative_path, container_path = host_to_container_mapping.split(":")
|
130
|
+
[ expand_host_path(host_relative_path), container_path ].join(":")
|
131
|
+
end || []
|
132
|
+
end
|
133
|
+
|
134
|
+
def expand_host_path(host_relative_path)
|
135
|
+
"#{service_data_directory}/#{host_relative_path}"
|
136
|
+
end
|
137
|
+
|
138
|
+
def service_data_directory
|
139
|
+
"$PWD/#{service_name}"
|
140
|
+
end
|
141
|
+
|
142
|
+
def hosts_from_host
|
143
|
+
if specifics.key?("host")
|
144
|
+
host = specifics["host"]
|
145
|
+
if host
|
146
|
+
[host]
|
147
|
+
else
|
148
|
+
raise ArgumentError, "Missing host for accessory `#{name}`"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def hosts_from_hosts
|
154
|
+
if specifics.key?("hosts")
|
155
|
+
hosts = specifics["hosts"]
|
156
|
+
if hosts.is_a?(Array)
|
157
|
+
hosts
|
158
|
+
else
|
159
|
+
raise ArgumentError, "Hosts should be an Array for accessory `#{name}`"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def hosts_from_roles
|
165
|
+
if specifics.key?("roles")
|
166
|
+
specifics["roles"].flat_map { |role| config.role(role).hosts }
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Kamal::Configuration::Boot
|
2
|
+
def initialize(config:)
|
3
|
+
@options = config.raw_config.boot || {}
|
4
|
+
@host_count = config.all_hosts.count
|
5
|
+
end
|
6
|
+
|
7
|
+
def limit
|
8
|
+
limit = @options["limit"]
|
9
|
+
|
10
|
+
if limit.to_s.end_with?("%")
|
11
|
+
@host_count * limit.to_i / 100
|
12
|
+
else
|
13
|
+
limit
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def wait
|
18
|
+
@options["wait"]
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
class Kamal::Configuration::Builder
|
2
|
+
def initialize(config:)
|
3
|
+
@options = config.raw_config.builder || {}
|
4
|
+
@image = config.image
|
5
|
+
@server = config.registry["server"]
|
6
|
+
|
7
|
+
valid?
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_h
|
11
|
+
@options
|
12
|
+
end
|
13
|
+
|
14
|
+
def multiarch?
|
15
|
+
@options["multiarch"] != false
|
16
|
+
end
|
17
|
+
|
18
|
+
def local?
|
19
|
+
!!@options["local"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def remote?
|
23
|
+
!!@options["remote"]
|
24
|
+
end
|
25
|
+
|
26
|
+
def cached?
|
27
|
+
!!@options["cache"]
|
28
|
+
end
|
29
|
+
|
30
|
+
def args
|
31
|
+
@options["args"] || {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def secrets
|
35
|
+
@options["secrets"] || []
|
36
|
+
end
|
37
|
+
|
38
|
+
def dockerfile
|
39
|
+
@options["dockerfile"] || "Dockerfile"
|
40
|
+
end
|
41
|
+
|
42
|
+
def context
|
43
|
+
@options["context"] || "."
|
44
|
+
end
|
45
|
+
|
46
|
+
def local_arch
|
47
|
+
@options["local"]["arch"] if local?
|
48
|
+
end
|
49
|
+
|
50
|
+
def local_host
|
51
|
+
@options["local"]["host"] if local?
|
52
|
+
end
|
53
|
+
|
54
|
+
def remote_arch
|
55
|
+
@options["remote"]["arch"] if remote?
|
56
|
+
end
|
57
|
+
|
58
|
+
def remote_host
|
59
|
+
@options["remote"]["host"] if remote?
|
60
|
+
end
|
61
|
+
|
62
|
+
def cache_from
|
63
|
+
if cached?
|
64
|
+
case @options["cache"]["type"]
|
65
|
+
when "gha"
|
66
|
+
cache_from_config_for_gha
|
67
|
+
when "registry"
|
68
|
+
cache_from_config_for_registry
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def cache_to
|
74
|
+
if cached?
|
75
|
+
case @options["cache"]["type"]
|
76
|
+
when "gha"
|
77
|
+
cache_to_config_for_gha
|
78
|
+
when "registry"
|
79
|
+
cache_to_config_for_registry
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
def valid?
|
86
|
+
if @options["cache"] && @options["cache"]["type"]
|
87
|
+
raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless ["gha", "registry"].include?(@options["cache"]["type"])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def cache_image
|
92
|
+
@options["cache"]&.fetch("image", nil) || "#{@image}-build-cache"
|
93
|
+
end
|
94
|
+
|
95
|
+
def cache_image_ref
|
96
|
+
[ @server, cache_image ].compact.join("/")
|
97
|
+
end
|
98
|
+
|
99
|
+
def cache_from_config_for_gha
|
100
|
+
"type=gha"
|
101
|
+
end
|
102
|
+
|
103
|
+
def cache_from_config_for_registry
|
104
|
+
[ "type=registry", "ref=#{cache_image_ref}" ].compact.join(",")
|
105
|
+
end
|
106
|
+
|
107
|
+
def cache_to_config_for_gha
|
108
|
+
[ "type=gha", @options["cache"]&.fetch("options", nil)].compact.join(",")
|
109
|
+
end
|
110
|
+
|
111
|
+
def cache_to_config_for_registry
|
112
|
+
[ "type=registry", @options["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
class Kamal::Configuration::Role
|
2
|
+
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
3
|
+
|
4
|
+
attr_accessor :name
|
5
|
+
|
6
|
+
def initialize(name, config:)
|
7
|
+
@name, @config = name.inquiry, config
|
8
|
+
end
|
9
|
+
|
10
|
+
def primary_host
|
11
|
+
hosts.first
|
12
|
+
end
|
13
|
+
|
14
|
+
def hosts
|
15
|
+
@hosts ||= extract_hosts_from_config
|
16
|
+
end
|
17
|
+
|
18
|
+
def labels
|
19
|
+
default_labels.merge(traefik_labels).merge(custom_labels)
|
20
|
+
end
|
21
|
+
|
22
|
+
def label_args
|
23
|
+
argumentize "--label", labels
|
24
|
+
end
|
25
|
+
|
26
|
+
def env
|
27
|
+
if config.env && config.env["secret"]
|
28
|
+
merged_env_with_secrets
|
29
|
+
else
|
30
|
+
merged_env
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def env_args
|
35
|
+
argumentize_env_with_secrets env
|
36
|
+
end
|
37
|
+
|
38
|
+
def health_check_args
|
39
|
+
if health_check_cmd.present?
|
40
|
+
optionize({ "health-cmd" => health_check_cmd, "health-interval" => health_check_interval })
|
41
|
+
else
|
42
|
+
[]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def health_check_cmd
|
47
|
+
options = specializations["healthcheck"] || {}
|
48
|
+
options = config.healthcheck.merge(options) if running_traefik?
|
49
|
+
|
50
|
+
options["cmd"] || http_health_check(port: options["port"], path: options["path"])
|
51
|
+
end
|
52
|
+
|
53
|
+
def health_check_interval
|
54
|
+
options = specializations["healthcheck"] || {}
|
55
|
+
options = config.healthcheck.merge(options) if running_traefik?
|
56
|
+
|
57
|
+
options["interval"] || "1s"
|
58
|
+
end
|
59
|
+
|
60
|
+
def cmd
|
61
|
+
specializations["cmd"]
|
62
|
+
end
|
63
|
+
|
64
|
+
def option_args
|
65
|
+
if args = specializations["options"]
|
66
|
+
optionize args
|
67
|
+
else
|
68
|
+
[]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def running_traefik?
|
73
|
+
name.web? || specializations["traefik"]
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
attr_accessor :config
|
78
|
+
|
79
|
+
def extract_hosts_from_config
|
80
|
+
if config.servers.is_a?(Array)
|
81
|
+
config.servers
|
82
|
+
else
|
83
|
+
servers = config.servers[name]
|
84
|
+
servers.is_a?(Array) ? servers : Array(servers["hosts"])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def default_labels
|
89
|
+
if config.destination
|
90
|
+
{ "service" => config.service, "role" => name, "destination" => config.destination }
|
91
|
+
else
|
92
|
+
{ "service" => config.service, "role" => name }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def traefik_labels
|
97
|
+
if running_traefik?
|
98
|
+
{
|
99
|
+
# Setting a service property ensures that the generated service name will be consistent between versions
|
100
|
+
"traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
|
101
|
+
|
102
|
+
"traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
|
103
|
+
"traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
|
104
|
+
"traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
|
105
|
+
"traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
|
106
|
+
}
|
107
|
+
else
|
108
|
+
{}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def traefik_service
|
113
|
+
[ config.service, name, config.destination ].compact.join("-")
|
114
|
+
end
|
115
|
+
|
116
|
+
def custom_labels
|
117
|
+
Hash.new.tap do |labels|
|
118
|
+
labels.merge!(config.labels) if config.labels.present?
|
119
|
+
labels.merge!(specializations["labels"]) if specializations["labels"].present?
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def specializations
|
124
|
+
if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
|
125
|
+
{ }
|
126
|
+
else
|
127
|
+
config.servers[name].except("hosts")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def specialized_env
|
132
|
+
specializations["env"] || {}
|
133
|
+
end
|
134
|
+
|
135
|
+
def merged_env
|
136
|
+
config.env&.merge(specialized_env) || {}
|
137
|
+
end
|
138
|
+
|
139
|
+
# Secrets are stored in an array, which won't merge by default, so have to do it by hand.
|
140
|
+
def merged_env_with_secrets
|
141
|
+
merged_env.tap do |new_env|
|
142
|
+
new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"])
|
143
|
+
|
144
|
+
# If there's no secret/clear split, everything is clear
|
145
|
+
clear_app_env = config.env["secret"] ? Array(config.env["clear"]) : Array(config.env["clear"] || config.env)
|
146
|
+
clear_role_env = specialized_env["secret"] ? Array(specialized_env["clear"]) : Array(specialized_env["clear"] || specialized_env)
|
147
|
+
|
148
|
+
new_env["clear"] = (clear_app_env + clear_role_env).uniq
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def http_health_check(port:, path:)
|
153
|
+
"curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present?
|
154
|
+
end
|
155
|
+
end
|