kamal 1.8.2 → 2.0.0.beta1
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 +4 -4
- data/README.md +1 -1
- data/lib/kamal/cli/accessory.rb +44 -20
- data/lib/kamal/cli/alias/command.rb +9 -0
- data/lib/kamal/cli/app/boot.rb +22 -16
- data/lib/kamal/cli/app.rb +40 -5
- data/lib/kamal/cli/base.rb +19 -51
- data/lib/kamal/cli/build.rb +12 -13
- data/lib/kamal/cli/healthcheck/barrier.rb +2 -0
- data/lib/kamal/cli/healthcheck/poller.rb +18 -39
- data/lib/kamal/cli/lock.rb +2 -3
- data/lib/kamal/cli/main.rb +54 -51
- data/lib/kamal/cli/proxy.rb +224 -0
- data/lib/kamal/cli/prune.rb +0 -1
- data/lib/kamal/cli/secrets.rb +36 -0
- data/lib/kamal/cli/server.rb +2 -3
- data/lib/kamal/cli/templates/deploy.yml +4 -21
- data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/secrets +16 -0
- data/lib/kamal/cli.rb +1 -0
- data/lib/kamal/commander/specifics.rb +3 -3
- data/lib/kamal/commander.rb +24 -8
- data/lib/kamal/commands/accessory.rb +7 -7
- data/lib/kamal/commands/app/assets.rb +8 -8
- data/lib/kamal/commands/app/proxy.rb +16 -0
- data/lib/kamal/commands/app.rb +7 -15
- data/lib/kamal/commands/auditor.rb +6 -3
- data/lib/kamal/commands/base.rb +8 -0
- data/lib/kamal/commands/builder/base.rb +26 -13
- data/lib/kamal/commands/builder/hybrid.rb +21 -0
- data/lib/kamal/commands/builder/local.rb +14 -0
- data/lib/kamal/commands/builder/remote.rb +63 -0
- data/lib/kamal/commands/builder.rb +13 -29
- data/lib/kamal/commands/docker.rb +4 -0
- data/lib/kamal/commands/hook.rb +5 -2
- data/lib/kamal/commands/lock.rb +2 -6
- data/lib/kamal/commands/proxy.rb +77 -0
- data/lib/kamal/commands/prune.rb +1 -9
- data/lib/kamal/commands/server.rb +11 -1
- data/lib/kamal/configuration/accessory.rb +14 -2
- data/lib/kamal/configuration/alias.rb +15 -0
- data/lib/kamal/configuration/builder.rb +52 -18
- data/lib/kamal/configuration/docs/alias.yml +26 -0
- data/lib/kamal/configuration/docs/builder.yml +26 -23
- data/lib/kamal/configuration/docs/configuration.yml +22 -16
- data/lib/kamal/configuration/docs/env.yml +10 -11
- data/lib/kamal/configuration/docs/proxy.yml +100 -0
- data/lib/kamal/configuration/docs/registry.yml +4 -2
- data/lib/kamal/configuration/docs/role.yml +3 -5
- data/lib/kamal/configuration/env/tag.rb +4 -3
- data/lib/kamal/configuration/env.rb +10 -17
- data/lib/kamal/configuration/proxy.rb +66 -0
- data/lib/kamal/configuration/registry.rb +3 -2
- data/lib/kamal/configuration/role.rb +63 -94
- data/lib/kamal/configuration/validator/alias.rb +15 -0
- data/lib/kamal/configuration/validator/builder.rb +4 -0
- data/lib/kamal/configuration/validator/proxy.rb +11 -0
- data/lib/kamal/configuration/validator.rb +42 -24
- data/lib/kamal/configuration.rb +91 -33
- data/lib/kamal/env_file.rb +4 -0
- data/lib/kamal/secrets/adapters/base.rb +18 -0
- data/lib/kamal/secrets/adapters/bitwarden.rb +64 -0
- data/lib/kamal/secrets/adapters/last_pass.rb +30 -0
- data/lib/kamal/secrets/adapters/one_password.rb +61 -0
- data/lib/kamal/secrets/adapters/test.rb +10 -0
- data/lib/kamal/secrets/adapters.rb +14 -0
- data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +32 -0
- data/lib/kamal/secrets.rb +37 -0
- data/lib/kamal/sshkit_with_ext.rb +1 -0
- data/lib/kamal/utils.rb +28 -0
- data/lib/kamal/version.rb +1 -1
- data/lib/kamal.rb +3 -1
- metadata +32 -23
- data/lib/kamal/cli/env.rb +0 -54
- data/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +0 -3
- data/lib/kamal/cli/templates/template.env +0 -2
- data/lib/kamal/cli/traefik.rb +0 -122
- data/lib/kamal/commands/app/cord.rb +0 -22
- data/lib/kamal/commands/builder/multiarch/remote.rb +0 -61
- data/lib/kamal/commands/builder/multiarch.rb +0 -41
- data/lib/kamal/commands/builder/native/cached.rb +0 -25
- data/lib/kamal/commands/builder/native/remote.rb +0 -67
- data/lib/kamal/commands/builder/native.rb +0 -20
- data/lib/kamal/commands/traefik.rb +0 -85
- data/lib/kamal/configuration/docs/healthcheck.yml +0 -59
- data/lib/kamal/configuration/docs/traefik.yml +0 -62
- data/lib/kamal/configuration/healthcheck.rb +0 -63
- data/lib/kamal/configuration/traefik.rb +0 -60
- /data/lib/kamal/cli/templates/sample_hooks/{pre-traefik-reboot.sample → pre-proxy-reboot.sample} +0 -0
@@ -1,10 +1,9 @@
|
|
1
1
|
class Kamal::Configuration::Role
|
2
2
|
include Kamal::Configuration::Validation
|
3
3
|
|
4
|
-
CORD_FILE = "cord"
|
5
4
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
6
5
|
|
7
|
-
attr_reader :name, :config, :specialized_env, :specialized_logging, :
|
6
|
+
attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_proxy
|
8
7
|
|
9
8
|
alias to_s name
|
10
9
|
|
@@ -18,16 +17,14 @@ class Kamal::Configuration::Role
|
|
18
17
|
|
19
18
|
@specialized_env = Kamal::Configuration::Env.new \
|
20
19
|
config: specializations.fetch("env", {}),
|
21
|
-
|
20
|
+
secrets: config.secrets,
|
22
21
|
context: "servers/#{name}/env"
|
23
22
|
|
24
23
|
@specialized_logging = Kamal::Configuration::Logging.new \
|
25
24
|
logging_config: specializations.fetch("logging", {}),
|
26
25
|
context: "servers/#{name}/logging"
|
27
26
|
|
28
|
-
|
29
|
-
healthcheck_config: specializations.fetch("healthcheck", {}),
|
30
|
-
context: "servers/#{name}/healthcheck"
|
27
|
+
initialize_specialized_proxy
|
31
28
|
end
|
32
29
|
|
33
30
|
def primary_host
|
@@ -55,7 +52,7 @@ class Kamal::Configuration::Role
|
|
55
52
|
end
|
56
53
|
|
57
54
|
def labels
|
58
|
-
default_labels.merge(
|
55
|
+
default_labels.merge(custom_labels)
|
59
56
|
end
|
60
57
|
|
61
58
|
def label_args
|
@@ -70,87 +67,53 @@ class Kamal::Configuration::Role
|
|
70
67
|
@logging ||= config.logging.merge(specialized_logging)
|
71
68
|
end
|
72
69
|
|
73
|
-
|
74
|
-
|
75
|
-
@envs ||= {}
|
76
|
-
@envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
|
70
|
+
def proxy
|
71
|
+
@proxy ||= config.proxy.merge(specialized_proxy) if running_proxy?
|
77
72
|
end
|
78
73
|
|
79
|
-
def
|
80
|
-
|
74
|
+
def running_proxy?
|
75
|
+
@running_proxy
|
81
76
|
end
|
82
77
|
|
83
|
-
def
|
84
|
-
|
78
|
+
def ssl?
|
79
|
+
running_proxy? && proxy.ssl?
|
85
80
|
end
|
86
81
|
|
82
|
+
def stop_args
|
83
|
+
# When deploying with the proxy, kamal-proxy will drain request before returning so we don't need to wait.
|
84
|
+
timeout = running_proxy? ? nil : config.drain_timeout
|
87
85
|
|
88
|
-
|
89
|
-
if running_traefik? || healthcheck.set_port_or_path?
|
90
|
-
if cord && uses_cord?
|
91
|
-
optionize({ "health-cmd" => health_check_cmd_with_cord, "health-interval" => healthcheck.interval })
|
92
|
-
.concat(cord_volume.docker_args)
|
93
|
-
else
|
94
|
-
optionize({ "health-cmd" => healthcheck.cmd, "health-interval" => healthcheck.interval })
|
95
|
-
end
|
96
|
-
else
|
97
|
-
[]
|
98
|
-
end
|
86
|
+
[ *argumentize("-t", timeout) ]
|
99
87
|
end
|
100
88
|
|
101
|
-
def
|
102
|
-
@
|
103
|
-
|
104
|
-
config.healthcheck.merge(specialized_healthcheck)
|
105
|
-
else
|
106
|
-
specialized_healthcheck
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def health_check_cmd_with_cord
|
111
|
-
"(#{healthcheck.cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)"
|
112
|
-
end
|
113
|
-
|
114
|
-
|
115
|
-
def running_traefik?
|
116
|
-
if specializations["traefik"].nil?
|
117
|
-
primary?
|
118
|
-
else
|
119
|
-
specializations["traefik"]
|
120
|
-
end
|
89
|
+
def env(host)
|
90
|
+
@envs ||= {}
|
91
|
+
@envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
|
121
92
|
end
|
122
93
|
|
123
|
-
def
|
124
|
-
|
94
|
+
def env_args(host)
|
95
|
+
[ *env(host).clear_args, *argumentize("--env-file", secrets_path) ]
|
125
96
|
end
|
126
97
|
|
127
|
-
|
128
|
-
|
129
|
-
running_traefik? && cord_volume && healthcheck.cmd.present?
|
98
|
+
def env_directory
|
99
|
+
File.join(config.env_directory, "roles")
|
130
100
|
end
|
131
101
|
|
132
|
-
def
|
133
|
-
|
102
|
+
def secrets_io(host)
|
103
|
+
env(host).secrets_io
|
134
104
|
end
|
135
105
|
|
136
|
-
def
|
137
|
-
|
138
|
-
@cord_volume ||= Kamal::Configuration::Volume.new \
|
139
|
-
host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")),
|
140
|
-
container_path: cord
|
141
|
-
end
|
106
|
+
def secrets_path
|
107
|
+
File.join(config.env_directory, "roles", "#{name}.env")
|
142
108
|
end
|
143
109
|
|
144
|
-
def
|
145
|
-
|
110
|
+
def asset_volume_args
|
111
|
+
asset_volume&.docker_args
|
146
112
|
end
|
147
113
|
|
148
|
-
def cord_container_directory
|
149
|
-
health_check_options.fetch("cord", nil)
|
150
|
-
end
|
151
114
|
|
152
|
-
def
|
153
|
-
|
115
|
+
def primary?
|
116
|
+
name == @config.primary_role_name
|
154
117
|
end
|
155
118
|
|
156
119
|
|
@@ -168,25 +131,52 @@ class Kamal::Configuration::Role
|
|
168
131
|
end
|
169
132
|
|
170
133
|
def assets?
|
171
|
-
asset_path.present? &&
|
134
|
+
asset_path.present? && running_proxy?
|
172
135
|
end
|
173
136
|
|
174
|
-
def asset_volume(version =
|
137
|
+
def asset_volume(version = config.version)
|
175
138
|
if assets?
|
176
139
|
Kamal::Configuration::Volume.new \
|
177
|
-
host_path:
|
140
|
+
host_path: asset_volume_directory(version), container_path: asset_path
|
178
141
|
end
|
179
142
|
end
|
180
143
|
|
181
|
-
def
|
182
|
-
File.join config.
|
144
|
+
def asset_extracted_directory(version = config.version)
|
145
|
+
File.join config.assets_directory, "extracted", [ name, version ].join("-")
|
183
146
|
end
|
184
147
|
|
185
|
-
def
|
186
|
-
File.join config.
|
148
|
+
def asset_volume_directory(version = config.version)
|
149
|
+
File.join config.assets_directory, "volumes", [ name, version ].join("-")
|
150
|
+
end
|
151
|
+
|
152
|
+
def ensure_one_host_for_ssl
|
153
|
+
if running_proxy? && proxy.ssl? && hosts.size > 1
|
154
|
+
raise Kamal::ConfigurationError, "SSL is only supported on a single server, found #{hosts.size} servers for role #{name}"
|
155
|
+
end
|
187
156
|
end
|
188
157
|
|
189
158
|
private
|
159
|
+
def initialize_specialized_proxy
|
160
|
+
proxy_specializations = specializations["proxy"]
|
161
|
+
|
162
|
+
if primary?
|
163
|
+
# only false means no proxy for non-primary roles
|
164
|
+
@running_proxy = proxy_specializations != false
|
165
|
+
else
|
166
|
+
# false and nil both mean no proxy for non-primary roles
|
167
|
+
@running_proxy = !!proxy_specializations
|
168
|
+
end
|
169
|
+
|
170
|
+
if running_proxy?
|
171
|
+
proxy_config = proxy_specializations == true || proxy_specializations.nil? ? {} : proxy_specializations
|
172
|
+
|
173
|
+
@specialized_proxy = Kamal::Configuration::Proxy.new \
|
174
|
+
config: config,
|
175
|
+
proxy_config: proxy_config,
|
176
|
+
context: "servers/#{name}/proxy"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
190
180
|
def tagged_hosts
|
191
181
|
{}.tap do |tagged_hosts|
|
192
182
|
extract_hosts_from_config.map do |host_config|
|
@@ -221,27 +211,6 @@ class Kamal::Configuration::Role
|
|
221
211
|
end
|
222
212
|
end
|
223
213
|
|
224
|
-
def traefik_labels
|
225
|
-
if running_traefik?
|
226
|
-
{
|
227
|
-
# Setting a service property ensures that the generated service name will be consistent between versions
|
228
|
-
"traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
|
229
|
-
|
230
|
-
"traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
|
231
|
-
"traefik.http.routers.#{traefik_service}.priority" => "2",
|
232
|
-
"traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
|
233
|
-
"traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
|
234
|
-
"traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
|
235
|
-
}
|
236
|
-
else
|
237
|
-
{}
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
def traefik_service
|
242
|
-
container_prefix
|
243
|
-
end
|
244
|
-
|
245
214
|
def custom_labels
|
246
215
|
Hash.new.tap do |labels|
|
247
216
|
labels.merge!(config.labels) if config.labels.present?
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Kamal::Configuration::Validator::Alias < Kamal::Configuration::Validator
|
2
|
+
def validate!
|
3
|
+
super
|
4
|
+
|
5
|
+
name = context.delete_prefix("aliases/")
|
6
|
+
|
7
|
+
if name !~ /\A[a-z0-9_-]+\z/
|
8
|
+
error "Invalid alias name: '#{name}'. Must only contain lowercase letters, alphanumeric, hyphens and underscores."
|
9
|
+
end
|
10
|
+
|
11
|
+
if Kamal::Cli::Main.commands.include?(name)
|
12
|
+
error "Alias '#{name}' conflicts with a built-in command."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -5,5 +5,9 @@ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator
|
|
5
5
|
if config["cache"] && config["cache"]["type"]
|
6
6
|
error "Invalid cache type: #{config["cache"]["type"]}" unless [ "gha", "registry" ].include?(config["cache"]["type"])
|
7
7
|
end
|
8
|
+
|
9
|
+
error "Builder arch not set" unless config["arch"].present?
|
10
|
+
|
11
|
+
error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank?
|
8
12
|
end
|
9
13
|
end
|
@@ -13,32 +13,40 @@ class Kamal::Configuration::Validator
|
|
13
13
|
|
14
14
|
private
|
15
15
|
def validate_against_example!(validation_config, example)
|
16
|
-
validate_type! validation_config,
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
16
|
+
validate_type! validation_config, example.class
|
17
|
+
|
18
|
+
if example.class == Hash
|
19
|
+
check_unknown_keys! validation_config, example
|
20
|
+
|
21
|
+
validation_config.each do |key, value|
|
22
|
+
next if extension?(key)
|
23
|
+
with_context(key) do
|
24
|
+
example_value = example[key]
|
25
|
+
|
26
|
+
if example_value == "..."
|
27
|
+
unless key.to_s == "proxy" && boolean?(value.class)
|
28
|
+
validate_type! value, *(Array if key == :servers), Hash
|
29
|
+
end
|
30
|
+
elsif key == "hosts"
|
31
|
+
validate_servers! value
|
32
|
+
elsif example_value.is_a?(Array)
|
33
|
+
if key == "arch"
|
34
|
+
validate_array_of_or_type! value, example_value.first.class
|
35
|
+
else
|
36
|
+
validate_array_of! value, example_value.first.class
|
37
|
+
end
|
38
|
+
elsif example_value.is_a?(Hash)
|
39
|
+
case key.to_s
|
40
|
+
when "options", "args"
|
41
|
+
validate_type! value, Hash
|
42
|
+
when "labels"
|
43
|
+
validate_hash_of! value, example_value.first[1].class
|
44
|
+
else
|
45
|
+
validate_against_example! value, example_value
|
46
|
+
end
|
37
47
|
else
|
38
|
-
|
48
|
+
validate_type! value, example_value.class
|
39
49
|
end
|
40
|
-
else
|
41
|
-
validate_type! value, example_value.class
|
42
50
|
end
|
43
51
|
end
|
44
52
|
end
|
@@ -69,6 +77,16 @@ class Kamal::Configuration::Validator
|
|
69
77
|
value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
70
78
|
end
|
71
79
|
|
80
|
+
def validate_array_of_or_type!(value, type)
|
81
|
+
if value.is_a?(Array)
|
82
|
+
validate_array_of! value, type
|
83
|
+
else
|
84
|
+
validate_type! value, type
|
85
|
+
end
|
86
|
+
rescue Kamal::ConfigurationError
|
87
|
+
type_error(Array, type)
|
88
|
+
end
|
89
|
+
|
72
90
|
def validate_array_of!(array, type)
|
73
91
|
validate_type! array, Array
|
74
92
|
|
data/lib/kamal/configuration.rb
CHANGED
@@ -2,19 +2,22 @@ require "active_support/ordered_options"
|
|
2
2
|
require "active_support/core_ext/string/inquiry"
|
3
3
|
require "active_support/core_ext/module/delegation"
|
4
4
|
require "active_support/core_ext/hash/keys"
|
5
|
-
require "pathname"
|
6
5
|
require "erb"
|
7
6
|
require "net/ssh/proxy/jump"
|
8
7
|
|
9
8
|
class Kamal::Configuration
|
10
|
-
delegate :service, :image, :labels, :
|
9
|
+
delegate :service, :image, :labels, :hooks_path, to: :raw_config, allow_nil: true
|
11
10
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
12
11
|
|
13
|
-
attr_reader :destination, :raw_config
|
14
|
-
attr_reader :accessories, :boot, :builder, :env, :
|
12
|
+
attr_reader :destination, :raw_config, :secrets
|
13
|
+
attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :servers, :ssh, :sshkit, :registry
|
15
14
|
|
16
15
|
include Validation
|
17
16
|
|
17
|
+
PROXY_MINIMUM_VERSION = "v0.3.0"
|
18
|
+
PROXY_HTTP_PORT = 80
|
19
|
+
PROXY_HTTPS_PORT = 443
|
20
|
+
|
18
21
|
class << self
|
19
22
|
def create_from(config_file:, destination: nil, version: nil)
|
20
23
|
raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
|
@@ -49,18 +52,20 @@ class Kamal::Configuration
|
|
49
52
|
|
50
53
|
validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
|
51
54
|
|
55
|
+
@secrets = Kamal::Secrets.new(destination: destination)
|
56
|
+
|
52
57
|
# Eager load config to validate it, these are first as they have dependencies later on
|
53
58
|
@servers = Servers.new(config: self)
|
54
59
|
@registry = Registry.new(config: self)
|
55
60
|
|
56
61
|
@accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
|
62
|
+
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
|
57
63
|
@boot = Boot.new(config: self)
|
58
64
|
@builder = Builder.new(config: self)
|
59
|
-
@env = Env.new(config: @raw_config.env || {})
|
65
|
+
@env = Env.new(config: @raw_config.env || {}, secrets: secrets)
|
60
66
|
|
61
|
-
@healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
|
62
67
|
@logging = Logging.new(logging_config: @raw_config.logging)
|
63
|
-
@
|
68
|
+
@proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy || {})
|
64
69
|
@ssh = Ssh.new(config: self)
|
65
70
|
@sshkit = Sshkit.new(config: self)
|
66
71
|
|
@@ -69,6 +74,9 @@ class Kamal::Configuration
|
|
69
74
|
ensure_valid_kamal_version
|
70
75
|
ensure_retain_containers_valid
|
71
76
|
ensure_valid_service_name
|
77
|
+
ensure_no_traefik_reboot_hooks
|
78
|
+
ensure_one_host_for_ssl_roles
|
79
|
+
ensure_unique_hosts_for_ssl_roles
|
72
80
|
end
|
73
81
|
|
74
82
|
|
@@ -129,16 +137,16 @@ class Kamal::Configuration
|
|
129
137
|
raw_config.allow_empty_roles
|
130
138
|
end
|
131
139
|
|
132
|
-
def
|
133
|
-
roles.select(&:
|
140
|
+
def proxy_roles
|
141
|
+
roles.select(&:running_proxy?)
|
134
142
|
end
|
135
143
|
|
136
|
-
def
|
137
|
-
|
144
|
+
def proxy_role_names
|
145
|
+
proxy_roles.flat_map(&:name)
|
138
146
|
end
|
139
147
|
|
140
|
-
def
|
141
|
-
|
148
|
+
def proxy_hosts
|
149
|
+
proxy_roles.flat_map(&:hosts).uniq
|
142
150
|
end
|
143
151
|
|
144
152
|
def repository
|
@@ -183,31 +191,44 @@ class Kamal::Configuration
|
|
183
191
|
end
|
184
192
|
|
185
193
|
|
186
|
-
def healthcheck_service
|
187
|
-
[ "healthcheck", service, destination ].compact.join("-")
|
188
|
-
end
|
189
|
-
|
190
194
|
def readiness_delay
|
191
195
|
raw_config.readiness_delay || 7
|
192
196
|
end
|
193
197
|
|
194
|
-
def
|
195
|
-
|
198
|
+
def deploy_timeout
|
199
|
+
raw_config.deploy_timeout || 30
|
200
|
+
end
|
201
|
+
|
202
|
+
def drain_timeout
|
203
|
+
raw_config.drain_timeout || 30
|
196
204
|
end
|
197
205
|
|
198
206
|
|
199
207
|
def run_directory
|
200
|
-
|
208
|
+
".kamal"
|
201
209
|
end
|
202
210
|
|
203
|
-
def
|
204
|
-
|
205
|
-
run_directory
|
206
|
-
else
|
207
|
-
File.join "$(pwd)", run_directory
|
208
|
-
end
|
211
|
+
def apps_directory
|
212
|
+
File.join run_directory, "apps"
|
209
213
|
end
|
210
214
|
|
215
|
+
def app_directory
|
216
|
+
File.join apps_directory, [ service, destination ].compact.join("-")
|
217
|
+
end
|
218
|
+
|
219
|
+
def proxy_directory
|
220
|
+
File.join run_directory, "proxy"
|
221
|
+
end
|
222
|
+
|
223
|
+
def env_directory
|
224
|
+
File.join app_directory, "env"
|
225
|
+
end
|
226
|
+
|
227
|
+
def assets_directory
|
228
|
+
File.join app_directory, "assets"
|
229
|
+
end
|
230
|
+
|
231
|
+
|
211
232
|
def hooks_path
|
212
233
|
raw_config.hooks_path || ".kamal/hooks"
|
213
234
|
end
|
@@ -217,13 +238,9 @@ class Kamal::Configuration
|
|
217
238
|
end
|
218
239
|
|
219
240
|
|
220
|
-
def host_env_directory
|
221
|
-
File.join(run_directory, "env")
|
222
|
-
end
|
223
|
-
|
224
241
|
def env_tags
|
225
242
|
@env_tags ||= if (tags = raw_config.env["tags"])
|
226
|
-
tags.collect { |name, config| Env::Tag.new(name, config: config) }
|
243
|
+
tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
|
227
244
|
else
|
228
245
|
[]
|
229
246
|
end
|
@@ -233,6 +250,24 @@ class Kamal::Configuration
|
|
233
250
|
env_tags.detect { |t| t.name == name.to_s }
|
234
251
|
end
|
235
252
|
|
253
|
+
def proxy_publish_args
|
254
|
+
argumentize "--publish", [ "#{PROXY_HTTP_PORT}:#{PROXY_HTTP_PORT}", "#{PROXY_HTTPS_PORT}:#{PROXY_HTTPS_PORT}" ]
|
255
|
+
end
|
256
|
+
|
257
|
+
def proxy_image
|
258
|
+
"basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}"
|
259
|
+
end
|
260
|
+
|
261
|
+
def proxy_container_name
|
262
|
+
"kamal-proxy"
|
263
|
+
end
|
264
|
+
|
265
|
+
def proxy_config_volume
|
266
|
+
Kamal::Configuration::Volume.new \
|
267
|
+
host_path: File.join(proxy_directory, "config"),
|
268
|
+
container_path: "/home/kamal-proxy/.config/kamal-proxy"
|
269
|
+
end
|
270
|
+
|
236
271
|
|
237
272
|
def to_h
|
238
273
|
{
|
@@ -248,8 +283,7 @@ class Kamal::Configuration
|
|
248
283
|
sshkit: sshkit.to_h,
|
249
284
|
builder: builder.to_h,
|
250
285
|
accessories: raw_config.accessories,
|
251
|
-
logging: logging_args
|
252
|
-
healthcheck: healthcheck.to_h
|
286
|
+
logging: logging_args
|
253
287
|
}.compact
|
254
288
|
end
|
255
289
|
|
@@ -307,6 +341,30 @@ class Kamal::Configuration
|
|
307
341
|
true
|
308
342
|
end
|
309
343
|
|
344
|
+
def ensure_no_traefik_reboot_hooks
|
345
|
+
hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) }
|
346
|
+
|
347
|
+
if hooks.any?
|
348
|
+
raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot"
|
349
|
+
end
|
350
|
+
|
351
|
+
true
|
352
|
+
end
|
353
|
+
|
354
|
+
def ensure_one_host_for_ssl_roles
|
355
|
+
roles.each(&:ensure_one_host_for_ssl)
|
356
|
+
|
357
|
+
true
|
358
|
+
end
|
359
|
+
|
360
|
+
def ensure_unique_hosts_for_ssl_roles
|
361
|
+
hosts = roles.select(&:ssl?).map { |role| role.proxy.host }
|
362
|
+
duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
|
363
|
+
|
364
|
+
raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
|
365
|
+
|
366
|
+
true
|
367
|
+
end
|
310
368
|
|
311
369
|
def role_names
|
312
370
|
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
data/lib/kamal/env_file.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::Base
|
2
|
+
delegate :optionize, to: Kamal::Utils
|
3
|
+
|
4
|
+
def fetch(secrets, account:, from: nil)
|
5
|
+
session = login(account)
|
6
|
+
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
7
|
+
fetch_secrets(full_secrets, account: account, session: session)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def login(...)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch_secrets(...)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
2
|
+
private
|
3
|
+
def login(account)
|
4
|
+
status = run_command("status")
|
5
|
+
|
6
|
+
if status["status"] == "unauthenticated"
|
7
|
+
run_command("login #{account.shellescape}", raw: true)
|
8
|
+
status = run_command("status")
|
9
|
+
end
|
10
|
+
|
11
|
+
if status["status"] == "locked"
|
12
|
+
session = run_command("unlock --raw", raw: true).presence
|
13
|
+
status = run_command("status", session: session)
|
14
|
+
end
|
15
|
+
|
16
|
+
raise RuntimeError, "Failed to login to and unlock Bitwarden" unless status["status"] == "unlocked"
|
17
|
+
|
18
|
+
run_command("sync", session: session, raw: true)
|
19
|
+
raise RuntimeError, "Failed to sync Bitwarden" unless $?.success?
|
20
|
+
|
21
|
+
session
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch_secrets(secrets, account:, session:)
|
25
|
+
{}.tap do |results|
|
26
|
+
items_fields(secrets).each do |item, fields|
|
27
|
+
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
28
|
+
raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success?
|
29
|
+
item_json = JSON.parse(item_json)
|
30
|
+
|
31
|
+
if fields.any?
|
32
|
+
fields.each do |field|
|
33
|
+
item_field = item_json["fields"].find { |f| f["name"] == field }
|
34
|
+
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
|
35
|
+
value = item_field["value"]
|
36
|
+
results["#{item}/#{field}"] = value
|
37
|
+
end
|
38
|
+
else
|
39
|
+
results[item] = item_json["login"]["password"]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def items_fields(secrets)
|
46
|
+
{}.tap do |items|
|
47
|
+
secrets.each do |secret|
|
48
|
+
item, field = secret.split("/")
|
49
|
+
items[item] ||= []
|
50
|
+
items[item] << field
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def signedin?(account)
|
56
|
+
run_command("status")["status"] != "unauthenticated"
|
57
|
+
end
|
58
|
+
|
59
|
+
def run_command(command, session: nil, raw: false)
|
60
|
+
full_command = [ *("BW_SESSION=#{session.shellescape}" if session), "bw", command ].join(" ")
|
61
|
+
result = `#{full_command}`.strip
|
62
|
+
raw ? result : JSON.parse(result)
|
63
|
+
end
|
64
|
+
end
|