kamal 2.3.0 → 2.7.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 +4 -4
- data/lib/kamal/cli/accessory.rb +42 -16
- data/lib/kamal/cli/alias/command.rb +1 -0
- data/lib/kamal/cli/app/{prepare_assets.rb → assets.rb} +1 -1
- data/lib/kamal/cli/app/boot.rb +3 -2
- data/lib/kamal/cli/app/error_pages.rb +33 -0
- data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
- data/lib/kamal/cli/app.rb +94 -29
- data/lib/kamal/cli/base.rb +29 -4
- data/lib/kamal/cli/build.rb +60 -18
- data/lib/kamal/cli/main.rb +8 -10
- data/lib/kamal/cli/proxy.rb +58 -25
- data/lib/kamal/cli/registry.rb +2 -0
- data/lib/kamal/cli/secrets.rb +9 -3
- data/lib/kamal/cli/server.rb +4 -2
- data/lib/kamal/cli/templates/deploy.yml +6 -3
- data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
- data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
- data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +1 -1
- data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +19 -6
- data/lib/kamal/cli.rb +1 -0
- data/lib/kamal/commander/specifics.rb +9 -1
- data/lib/kamal/commander.rb +18 -27
- data/lib/kamal/commands/accessory/proxy.rb +16 -0
- data/lib/kamal/commands/accessory.rb +9 -9
- data/lib/kamal/commands/app/assets.rb +4 -4
- data/lib/kamal/commands/app/containers.rb +2 -2
- data/lib/kamal/commands/app/error_pages.rb +9 -0
- data/lib/kamal/commands/app/execution.rb +6 -4
- data/lib/kamal/commands/app/images.rb +1 -1
- data/lib/kamal/commands/app/logging.rb +14 -4
- data/lib/kamal/commands/app/proxy.rb +17 -1
- data/lib/kamal/commands/app.rb +19 -10
- data/lib/kamal/commands/auditor.rb +11 -5
- data/lib/kamal/commands/base.rb +37 -1
- data/lib/kamal/commands/builder/base.rb +20 -7
- data/lib/kamal/commands/builder/cloud.rb +22 -0
- data/lib/kamal/commands/builder/pack.rb +46 -0
- data/lib/kamal/commands/builder.rb +11 -19
- data/lib/kamal/commands/proxy.rb +55 -15
- data/lib/kamal/commands/registry.rb +9 -7
- data/lib/kamal/configuration/accessory.rb +66 -11
- data/lib/kamal/configuration/builder.rb +20 -0
- data/lib/kamal/configuration/docs/accessory.yml +32 -4
- data/lib/kamal/configuration/docs/alias.yml +2 -2
- data/lib/kamal/configuration/docs/builder.yml +22 -0
- data/lib/kamal/configuration/docs/configuration.yml +6 -0
- data/lib/kamal/configuration/docs/env.yml +31 -0
- data/lib/kamal/configuration/docs/proxy.yml +78 -15
- data/lib/kamal/configuration/docs/registry.yml +4 -0
- data/lib/kamal/configuration/env.rb +13 -4
- data/lib/kamal/configuration/proxy/boot.rb +129 -0
- data/lib/kamal/configuration/proxy.rb +67 -5
- data/lib/kamal/configuration/registry.rb +6 -6
- data/lib/kamal/configuration/role.rb +11 -9
- data/lib/kamal/configuration/servers.rb +8 -1
- data/lib/kamal/configuration/validator/accessory.rb +6 -2
- data/lib/kamal/configuration/validator/builder.rb +2 -0
- data/lib/kamal/configuration/validator/proxy.rb +10 -0
- data/lib/kamal/configuration/validator/role.rb +3 -1
- data/lib/kamal/configuration/validator/servers.rb +1 -1
- data/lib/kamal/configuration/validator.rb +21 -1
- data/lib/kamal/configuration.rb +36 -57
- data/lib/kamal/docker.rb +30 -0
- data/lib/kamal/git.rb +10 -0
- data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
- data/lib/kamal/secrets/adapters/base.rb +13 -3
- data/lib/kamal/secrets/adapters/bitwarden.rb +2 -2
- data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
- data/lib/kamal/secrets/adapters/doppler.rb +57 -0
- data/lib/kamal/secrets/adapters/enpass.rb +71 -0
- data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
- data/lib/kamal/secrets/adapters/last_pass.rb +3 -2
- data/lib/kamal/secrets/adapters/one_password.rb +47 -13
- data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
- data/lib/kamal/secrets/adapters/test.rb +2 -2
- data/lib/kamal/secrets/adapters.rb +2 -0
- data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +2 -1
- data/lib/kamal/secrets.rb +1 -1
- data/lib/kamal/version.rb +1 -1
- metadata +22 -10
@@ -6,11 +6,14 @@ class Kamal::Configuration::Proxy
|
|
6
6
|
|
7
7
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
8
8
|
|
9
|
-
attr_reader :config, :proxy_config
|
9
|
+
attr_reader :config, :proxy_config, :role_name, :secrets
|
10
10
|
|
11
|
-
def initialize(config:, proxy_config:, context: "proxy")
|
11
|
+
def initialize(config:, proxy_config:, role_name: nil, secrets:, context: "proxy")
|
12
12
|
@config = config
|
13
13
|
@proxy_config = proxy_config
|
14
|
+
@proxy_config = {} if @proxy_config.nil?
|
15
|
+
@role_name = role_name
|
16
|
+
@secrets = secrets
|
14
17
|
validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
|
15
18
|
end
|
16
19
|
|
@@ -26,10 +29,46 @@ class Kamal::Configuration::Proxy
|
|
26
29
|
proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
|
27
30
|
end
|
28
31
|
|
32
|
+
def custom_ssl_certificate?
|
33
|
+
ssl = proxy_config["ssl"]
|
34
|
+
return false unless ssl.is_a?(Hash)
|
35
|
+
ssl["certificate_pem"].present? && ssl["private_key_pem"].present?
|
36
|
+
end
|
37
|
+
|
38
|
+
def certificate_pem_content
|
39
|
+
ssl = proxy_config["ssl"]
|
40
|
+
return nil unless ssl.is_a?(Hash)
|
41
|
+
secrets[ssl["certificate_pem"]]
|
42
|
+
end
|
43
|
+
|
44
|
+
def private_key_pem_content
|
45
|
+
ssl = proxy_config["ssl"]
|
46
|
+
return nil unless ssl.is_a?(Hash)
|
47
|
+
secrets[ssl["private_key_pem"]]
|
48
|
+
end
|
49
|
+
|
50
|
+
def host_tls_cert
|
51
|
+
tls_path(config.proxy_boot.tls_directory, "cert.pem")
|
52
|
+
end
|
53
|
+
|
54
|
+
def host_tls_key
|
55
|
+
tls_path(config.proxy_boot.tls_directory, "key.pem")
|
56
|
+
end
|
57
|
+
|
58
|
+
def container_tls_cert
|
59
|
+
tls_path(config.proxy_boot.tls_container_directory, "cert.pem")
|
60
|
+
end
|
61
|
+
|
62
|
+
def container_tls_key
|
63
|
+
tls_path(config.proxy_boot.tls_container_directory, "key.pem") if custom_ssl_certificate?
|
64
|
+
end
|
65
|
+
|
29
66
|
def deploy_options
|
30
67
|
{
|
31
68
|
host: hosts,
|
32
|
-
tls:
|
69
|
+
tls: ssl? ? true : nil,
|
70
|
+
"tls-certificate-path": container_tls_cert,
|
71
|
+
"tls-private-key-path": container_tls_key,
|
33
72
|
"deploy-timeout": seconds_duration(config.deploy_timeout),
|
34
73
|
"drain-timeout": seconds_duration(config.drain_timeout),
|
35
74
|
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
|
@@ -41,9 +80,13 @@ class Kamal::Configuration::Proxy
|
|
41
80
|
"buffer-memory": proxy_config.dig("buffering", "memory"),
|
42
81
|
"max-request-body": proxy_config.dig("buffering", "max_request_body"),
|
43
82
|
"max-response-body": proxy_config.dig("buffering", "max_response_body"),
|
83
|
+
"path-prefix": proxy_config.dig("path_prefix"),
|
84
|
+
"strip-path-prefix": proxy_config.dig("strip_path_prefix"),
|
44
85
|
"forward-headers": proxy_config.dig("forward_headers"),
|
86
|
+
"tls-redirect": proxy_config.dig("ssl_redirect"),
|
45
87
|
"log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
|
46
|
-
"log-response-header": proxy_config.dig("logging", "response_headers")
|
88
|
+
"log-response-header": proxy_config.dig("logging", "response_headers"),
|
89
|
+
"error-pages": error_pages
|
47
90
|
}.compact
|
48
91
|
end
|
49
92
|
|
@@ -51,12 +94,31 @@ class Kamal::Configuration::Proxy
|
|
51
94
|
optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "="
|
52
95
|
end
|
53
96
|
|
97
|
+
def stop_options(drain_timeout: nil, message: nil)
|
98
|
+
{
|
99
|
+
"drain-timeout": seconds_duration(drain_timeout),
|
100
|
+
message: message
|
101
|
+
}.compact
|
102
|
+
end
|
103
|
+
|
104
|
+
def stop_command_args(**options)
|
105
|
+
optionize stop_options(**options), with: "="
|
106
|
+
end
|
107
|
+
|
54
108
|
def merge(other)
|
55
|
-
self.class.new config: config, proxy_config: proxy_config.deep_merge(
|
109
|
+
self.class.new config: config, proxy_config: other.proxy_config.deep_merge(proxy_config), role_name: role_name, secrets: secrets
|
56
110
|
end
|
57
111
|
|
58
112
|
private
|
113
|
+
def tls_path(directory, filename)
|
114
|
+
File.join([ directory, role_name, filename ].compact) if custom_ssl_certificate?
|
115
|
+
end
|
116
|
+
|
59
117
|
def seconds_duration(value)
|
60
118
|
value ? "#{value}s" : nil
|
61
119
|
end
|
120
|
+
|
121
|
+
def error_pages
|
122
|
+
File.join config.proxy_boot.error_pages_container_directory, config.version if config.error_pages_path
|
123
|
+
end
|
62
124
|
end
|
@@ -1,12 +1,10 @@
|
|
1
1
|
class Kamal::Configuration::Registry
|
2
2
|
include Kamal::Configuration::Validation
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@secrets = config.secrets
|
9
|
-
validate! registry_config, with: Kamal::Configuration::Validator::Registry
|
4
|
+
def initialize(config:, secrets:, context: "registry")
|
5
|
+
@registry_config = config["registry"] || {}
|
6
|
+
@secrets = secrets
|
7
|
+
validate! registry_config, context: context, with: Kamal::Configuration::Validator::Registry
|
10
8
|
end
|
11
9
|
|
12
10
|
def server
|
@@ -22,6 +20,8 @@ class Kamal::Configuration::Registry
|
|
22
20
|
end
|
23
21
|
|
24
22
|
private
|
23
|
+
attr_reader :registry_config, :secrets
|
24
|
+
|
25
25
|
def lookup(key)
|
26
26
|
if registry_config[key].is_a?(Array)
|
27
27
|
secrets[registry_config[key].first]
|
@@ -10,7 +10,7 @@ class Kamal::Configuration::Role
|
|
10
10
|
def initialize(name, config:)
|
11
11
|
@name, @config = name.inquiry, config
|
12
12
|
validate! \
|
13
|
-
|
13
|
+
role_config,
|
14
14
|
example: validation_yml["servers"]["workers"],
|
15
15
|
context: "servers/#{name}",
|
16
16
|
with: Kamal::Configuration::Validator::Role
|
@@ -68,7 +68,7 @@ class Kamal::Configuration::Role
|
|
68
68
|
end
|
69
69
|
|
70
70
|
def proxy
|
71
|
-
@proxy ||= config.proxy
|
71
|
+
@proxy ||= specialized_proxy.merge(config.proxy) if running_proxy?
|
72
72
|
end
|
73
73
|
|
74
74
|
def running_proxy?
|
@@ -150,8 +150,8 @@ class Kamal::Configuration::Role
|
|
150
150
|
end
|
151
151
|
|
152
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}"
|
153
|
+
if running_proxy? && proxy.ssl? && hosts.size > 1 && !proxy.custom_ssl_certificate?
|
154
|
+
raise Kamal::ConfigurationError, "SSL is only supported on a single server unless you provide custom certificates, found #{hosts.size} servers for role #{name}"
|
155
155
|
end
|
156
156
|
end
|
157
157
|
|
@@ -173,6 +173,8 @@ class Kamal::Configuration::Role
|
|
173
173
|
@specialized_proxy = Kamal::Configuration::Proxy.new \
|
174
174
|
config: config,
|
175
175
|
proxy_config: proxy_config,
|
176
|
+
secrets: config.secrets,
|
177
|
+
role_name: name,
|
176
178
|
context: "servers/#{name}/proxy"
|
177
179
|
end
|
178
180
|
end
|
@@ -204,11 +206,11 @@ class Kamal::Configuration::Role
|
|
204
206
|
end
|
205
207
|
|
206
208
|
def specializations
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
209
|
+
@specializations ||= role_config.is_a?(Array) ? {} : role_config
|
210
|
+
end
|
211
|
+
|
212
|
+
def role_config
|
213
|
+
@role_config ||= config.raw_config.servers.is_a?(Array) ? {} : config.raw_config.servers[name]
|
212
214
|
end
|
213
215
|
|
214
216
|
def custom_labels
|
@@ -13,6 +13,13 @@ class Kamal::Configuration::Servers
|
|
13
13
|
|
14
14
|
private
|
15
15
|
def role_names
|
16
|
-
|
16
|
+
case servers_config
|
17
|
+
when Array
|
18
|
+
[ "web" ]
|
19
|
+
when NilClass
|
20
|
+
[]
|
21
|
+
else
|
22
|
+
servers_config.keys.sort
|
23
|
+
end
|
17
24
|
end
|
18
25
|
end
|
@@ -2,8 +2,12 @@ class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validat
|
|
2
2
|
def validate!
|
3
3
|
super
|
4
4
|
|
5
|
-
if (config.keys & [ "host", "hosts", "roles" ]).size != 1
|
6
|
-
error "specify one of `host`, `hosts` or `
|
5
|
+
if (config.keys & [ "host", "hosts", "role", "roles", "tag", "tags" ]).size != 1
|
6
|
+
error "specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`"
|
7
7
|
end
|
8
|
+
|
9
|
+
validate_labels!(config["labels"])
|
10
|
+
|
11
|
+
validate_docker_options!(config["options"])
|
8
12
|
end
|
9
13
|
end
|
@@ -8,6 +8,8 @@ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator
|
|
8
8
|
|
9
9
|
error "Builder arch not set" unless config["arch"].present?
|
10
10
|
|
11
|
+
error "buildpacks only support building for one arch" if config["pack"] && config["arch"].is_a?(Array) && config["arch"].size > 1
|
12
|
+
|
11
13
|
error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank?
|
12
14
|
end
|
13
15
|
end
|
@@ -10,6 +10,16 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
|
|
10
10
|
if (config.keys & [ "host", "hosts" ]).size > 1
|
11
11
|
error "Specify one of 'host' or 'hosts', not both"
|
12
12
|
end
|
13
|
+
|
14
|
+
if config["ssl"].is_a?(Hash)
|
15
|
+
if config["ssl"]["certificate_pem"].present? && config["ssl"]["private_key_pem"].blank?
|
16
|
+
error "Missing private_key_pem setting (required when certificate_pem is present)"
|
17
|
+
end
|
18
|
+
|
19
|
+
if config["ssl"]["private_key_pem"].present? && config["ssl"]["certificate_pem"].blank?
|
20
|
+
error "Missing certificate_pem setting (required when private_key_pem is present)"
|
21
|
+
end
|
22
|
+
end
|
13
23
|
end
|
14
24
|
end
|
15
25
|
end
|
@@ -3,9 +3,11 @@ class Kamal::Configuration::Validator::Role < Kamal::Configuration::Validator
|
|
3
3
|
validate_type! config, Array, Hash
|
4
4
|
|
5
5
|
if config.is_a?(Array)
|
6
|
-
validate_servers!
|
6
|
+
validate_servers!(config)
|
7
7
|
else
|
8
8
|
super
|
9
|
+
validate_labels!(config["labels"])
|
10
|
+
validate_docker_options!(config["options"])
|
9
11
|
end
|
10
12
|
end
|
11
13
|
end
|
@@ -24,7 +24,9 @@ class Kamal::Configuration::Validator
|
|
24
24
|
example_value = example[key]
|
25
25
|
|
26
26
|
if example_value == "..."
|
27
|
-
|
27
|
+
if key.to_s == "ssl"
|
28
|
+
validate_type! value, TrueClass, FalseClass, Hash
|
29
|
+
elsif key.to_s != "proxy" || !boolean?(value.class)
|
28
30
|
validate_type! value, *(Array if key == :servers), Hash
|
29
31
|
end
|
30
32
|
elsif key == "hosts"
|
@@ -168,4 +170,22 @@ class Kamal::Configuration::Validator
|
|
168
170
|
unknown_keys.reject! { |key| extension?(key) } if allow_extensions?
|
169
171
|
unknown_keys_error unknown_keys if unknown_keys.present?
|
170
172
|
end
|
173
|
+
|
174
|
+
def validate_labels!(labels)
|
175
|
+
return true if labels.blank?
|
176
|
+
|
177
|
+
with_context("labels") do
|
178
|
+
labels.each do |key, _|
|
179
|
+
with_context(key) do
|
180
|
+
error "invalid label. destination, role, and service are reserved labels" if %w[destination role service].include?(key)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def validate_docker_options!(options)
|
187
|
+
if options
|
188
|
+
error "Cannot set restart policy in docker options, unless-stopped is required" if options["restart"]
|
189
|
+
end
|
190
|
+
end
|
171
191
|
end
|
data/lib/kamal/configuration.rb
CHANGED
@@ -10,15 +10,10 @@ class Kamal::Configuration
|
|
10
10
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
11
11
|
|
12
12
|
attr_reader :destination, :raw_config, :secrets
|
13
|
-
attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :servers, :ssh, :sshkit, :registry
|
13
|
+
attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :proxy_boot, :servers, :ssh, :sshkit, :registry
|
14
14
|
|
15
15
|
include Validation
|
16
16
|
|
17
|
-
PROXY_MINIMUM_VERSION = "v0.8.2"
|
18
|
-
PROXY_HTTP_PORT = 80
|
19
|
-
PROXY_HTTPS_PORT = 443
|
20
|
-
PROXY_LOG_MAX_SIZE = "10m"
|
21
|
-
|
22
17
|
class << self
|
23
18
|
def create_from(config_file:, destination: nil, version: nil)
|
24
19
|
ENV["KAMAL_DESTINATION"] = destination
|
@@ -37,7 +32,7 @@ class Kamal::Configuration
|
|
37
32
|
if file.exist?
|
38
33
|
# Newer Psych doesn't load aliases by default
|
39
34
|
load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
|
40
|
-
YAML.send(load_method, ERB.new(
|
35
|
+
YAML.send(load_method, ERB.new(File.read(file)).result).symbolize_keys
|
41
36
|
else
|
42
37
|
raise "Configuration file not found in #{file}"
|
43
38
|
end
|
@@ -59,7 +54,7 @@ class Kamal::Configuration
|
|
59
54
|
|
60
55
|
# Eager load config to validate it, these are first as they have dependencies later on
|
61
56
|
@servers = Servers.new(config: self)
|
62
|
-
@registry = Registry.new(config:
|
57
|
+
@registry = Registry.new(config: @raw_config, secrets: secrets)
|
63
58
|
|
64
59
|
@accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
|
65
60
|
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
|
@@ -68,7 +63,8 @@ class Kamal::Configuration
|
|
68
63
|
@env = Env.new(config: @raw_config.env || {}, secrets: secrets)
|
69
64
|
|
70
65
|
@logging = Logging.new(logging_config: @raw_config.logging)
|
71
|
-
@proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy
|
66
|
+
@proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy, secrets: secrets)
|
67
|
+
@proxy_boot = Proxy::Boot.new(config: self)
|
72
68
|
@ssh = Ssh.new(config: self)
|
73
69
|
@sshkit = Sshkit.new(config: self)
|
74
70
|
|
@@ -82,7 +78,6 @@ class Kamal::Configuration
|
|
82
78
|
ensure_unique_hosts_for_ssl_roles
|
83
79
|
end
|
84
80
|
|
85
|
-
|
86
81
|
def version=(version)
|
87
82
|
@declared_version = version
|
88
83
|
end
|
@@ -106,6 +101,9 @@ class Kamal::Configuration
|
|
106
101
|
raw_config.minimum_version
|
107
102
|
end
|
108
103
|
|
104
|
+
def service_and_destination
|
105
|
+
[ service, destination ].compact.join("-")
|
106
|
+
end
|
109
107
|
|
110
108
|
def roles
|
111
109
|
servers.roles
|
@@ -119,11 +117,14 @@ class Kamal::Configuration
|
|
119
117
|
accessories.detect { |a| a.name == name.to_s }
|
120
118
|
end
|
121
119
|
|
122
|
-
|
123
120
|
def all_hosts
|
124
121
|
(roles + accessories).flat_map(&:hosts).uniq
|
125
122
|
end
|
126
123
|
|
124
|
+
def app_hosts
|
125
|
+
roles.flat_map(&:hosts).uniq
|
126
|
+
end
|
127
|
+
|
127
128
|
def primary_host
|
128
129
|
primary_role&.primary_host
|
129
130
|
end
|
@@ -148,8 +149,12 @@ class Kamal::Configuration
|
|
148
149
|
proxy_roles.flat_map(&:name)
|
149
150
|
end
|
150
151
|
|
152
|
+
def proxy_accessories
|
153
|
+
accessories.select(&:running_proxy?)
|
154
|
+
end
|
155
|
+
|
151
156
|
def proxy_hosts
|
152
|
-
proxy_roles.flat_map(&:hosts).uniq
|
157
|
+
(proxy_roles.flat_map(&:hosts) + proxy_accessories.flat_map(&:hosts)).uniq
|
153
158
|
end
|
154
159
|
|
155
160
|
def repository
|
@@ -180,7 +185,6 @@ class Kamal::Configuration
|
|
180
185
|
raw_config.retain_containers || 5
|
181
186
|
end
|
182
187
|
|
183
|
-
|
184
188
|
def volume_args
|
185
189
|
if raw_config.volumes.present?
|
186
190
|
argumentize "--volume", raw_config.volumes
|
@@ -193,7 +197,6 @@ class Kamal::Configuration
|
|
193
197
|
logging.args
|
194
198
|
end
|
195
199
|
|
196
|
-
|
197
200
|
def readiness_delay
|
198
201
|
raw_config.readiness_delay || 7
|
199
202
|
end
|
@@ -206,7 +209,6 @@ class Kamal::Configuration
|
|
206
209
|
raw_config.drain_timeout || 30
|
207
210
|
end
|
208
211
|
|
209
|
-
|
210
212
|
def run_directory
|
211
213
|
".kamal"
|
212
214
|
end
|
@@ -216,7 +218,7 @@ class Kamal::Configuration
|
|
216
218
|
end
|
217
219
|
|
218
220
|
def app_directory
|
219
|
-
File.join apps_directory,
|
221
|
+
File.join apps_directory, service_and_destination
|
220
222
|
end
|
221
223
|
|
222
224
|
def env_directory
|
@@ -227,7 +229,6 @@ class Kamal::Configuration
|
|
227
229
|
File.join app_directory, "assets"
|
228
230
|
end
|
229
231
|
|
230
|
-
|
231
232
|
def hooks_path
|
232
233
|
raw_config.hooks_path || ".kamal/hooks"
|
233
234
|
end
|
@@ -236,6 +237,9 @@ class Kamal::Configuration
|
|
236
237
|
raw_config.asset_path
|
237
238
|
end
|
238
239
|
|
240
|
+
def error_pages_path
|
241
|
+
raw_config.error_pages_path
|
242
|
+
end
|
239
243
|
|
240
244
|
def env_tags
|
241
245
|
@env_tags ||= if (tags = raw_config.env["tags"])
|
@@ -249,35 +253,6 @@ class Kamal::Configuration
|
|
249
253
|
env_tags.detect { |t| t.name == name.to_s }
|
250
254
|
end
|
251
255
|
|
252
|
-
def proxy_publish_args(http_port, https_port)
|
253
|
-
argumentize "--publish", [ "#{http_port}:#{PROXY_HTTP_PORT}", "#{https_port}:#{PROXY_HTTPS_PORT}" ]
|
254
|
-
end
|
255
|
-
|
256
|
-
def proxy_logging_args(max_size)
|
257
|
-
argumentize "--log-opt", "max-size=#{max_size}" if max_size.present?
|
258
|
-
end
|
259
|
-
|
260
|
-
def proxy_options_default
|
261
|
-
[ *proxy_publish_args(PROXY_HTTP_PORT, PROXY_HTTPS_PORT), *proxy_logging_args(PROXY_LOG_MAX_SIZE) ]
|
262
|
-
end
|
263
|
-
|
264
|
-
def proxy_image
|
265
|
-
"basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}"
|
266
|
-
end
|
267
|
-
|
268
|
-
def proxy_container_name
|
269
|
-
"kamal-proxy"
|
270
|
-
end
|
271
|
-
|
272
|
-
def proxy_directory
|
273
|
-
File.join run_directory, "proxy"
|
274
|
-
end
|
275
|
-
|
276
|
-
def proxy_options_file
|
277
|
-
File.join proxy_directory, "options"
|
278
|
-
end
|
279
|
-
|
280
|
-
|
281
256
|
def to_h
|
282
257
|
{
|
283
258
|
roles: role_names,
|
@@ -307,22 +282,26 @@ class Kamal::Configuration
|
|
307
282
|
end
|
308
283
|
|
309
284
|
def ensure_required_keys_present
|
310
|
-
%i[ service image registry
|
285
|
+
%i[ service image registry ].each do |key|
|
311
286
|
raise Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present?
|
312
287
|
end
|
313
288
|
|
314
|
-
|
315
|
-
raise Kamal::ConfigurationError, "
|
316
|
-
|
289
|
+
if raw_config.servers.nil?
|
290
|
+
raise Kamal::ConfigurationError, "No servers or accessories specified" unless raw_config.accessories.present?
|
291
|
+
else
|
292
|
+
unless role(primary_role_name).present?
|
293
|
+
raise Kamal::ConfigurationError, "The primary_role #{primary_role_name} isn't defined"
|
294
|
+
end
|
317
295
|
|
318
|
-
|
319
|
-
|
320
|
-
|
296
|
+
if primary_role.hosts.empty?
|
297
|
+
raise Kamal::ConfigurationError, "No servers specified for the #{primary_role.name} primary_role"
|
298
|
+
end
|
321
299
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
300
|
+
unless allow_empty_roles?
|
301
|
+
roles.each do |role|
|
302
|
+
if role.hosts.empty?
|
303
|
+
raise Kamal::ConfigurationError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
|
304
|
+
end
|
326
305
|
end
|
327
306
|
end
|
328
307
|
end
|
data/lib/kamal/docker.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
require "open3"
|
3
|
+
|
4
|
+
module Kamal::Docker
|
5
|
+
extend self
|
6
|
+
BUILD_CHECK_TAG = "kamal-local-build-check"
|
7
|
+
|
8
|
+
def included_files
|
9
|
+
Tempfile.create do |dockerfile|
|
10
|
+
dockerfile.write(<<~DOCKERFILE)
|
11
|
+
FROM busybox
|
12
|
+
COPY . app
|
13
|
+
WORKDIR app
|
14
|
+
CMD find . -type f | sed "s|^\./||"
|
15
|
+
DOCKERFILE
|
16
|
+
dockerfile.close
|
17
|
+
|
18
|
+
cmd = "docker buildx build -t=#{BUILD_CHECK_TAG} -f=#{dockerfile.path} ."
|
19
|
+
system(cmd) || raise("failed to build check image")
|
20
|
+
end
|
21
|
+
|
22
|
+
cmd = "docker run --rm #{BUILD_CHECK_TAG}"
|
23
|
+
out, err, status = Open3.capture3(cmd)
|
24
|
+
unless status
|
25
|
+
raise "failed to run check image:\n#{err}"
|
26
|
+
end
|
27
|
+
|
28
|
+
out.lines.map(&:strip)
|
29
|
+
end
|
30
|
+
end
|
data/lib/kamal/git.rb
CHANGED
@@ -24,4 +24,14 @@ module Kamal::Git
|
|
24
24
|
def root
|
25
25
|
`git rev-parse --show-toplevel`.strip
|
26
26
|
end
|
27
|
+
|
28
|
+
# returns an array of relative path names of files with uncommitted changes
|
29
|
+
def uncommitted_files
|
30
|
+
`git ls-files --modified`.lines.map(&:strip)
|
31
|
+
end
|
32
|
+
|
33
|
+
# returns an array of relative path names of untracked files, including gitignored files
|
34
|
+
def untracked_files
|
35
|
+
`git ls-files --others`.lines.map(&:strip)
|
36
|
+
end
|
27
37
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Base
|
2
|
+
def requires_account?
|
3
|
+
false
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
def login(_account)
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def fetch_secrets(secrets, from:, account: nil, session:)
|
12
|
+
{}.tap do |results|
|
13
|
+
get_from_secrets_manager(prefixed_secrets(secrets, from: from), account: account).each do |secret|
|
14
|
+
secret_name = secret["Name"]
|
15
|
+
secret_string = JSON.parse(secret["SecretString"])
|
16
|
+
|
17
|
+
secret_string.each do |key, value|
|
18
|
+
results["#{secret_name}/#{key}"] = value
|
19
|
+
end
|
20
|
+
rescue JSON::ParserError
|
21
|
+
results["#{secret_name}"] = secret["SecretString"]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_from_secrets_manager(secrets, account: nil)
|
27
|
+
args = [ "aws", "secretsmanager", "batch-get-secret-value", "--secret-id-list" ] + secrets.map(&:shellescape)
|
28
|
+
args += [ "--profile", account.shellescape ] if account
|
29
|
+
args += [ "--output", "json" ]
|
30
|
+
cmd = args.join(" ")
|
31
|
+
|
32
|
+
`#{cmd}`.tap do |secrets|
|
33
|
+
raise RuntimeError, "Could not read #{secrets} from AWS Secrets Manager" unless $?.success?
|
34
|
+
|
35
|
+
secrets = JSON.parse(secrets)
|
36
|
+
|
37
|
+
return secrets["SecretValues"] unless secrets["Errors"].present?
|
38
|
+
|
39
|
+
raise RuntimeError, secrets["Errors"].map { |error| "#{error['SecretId']}: #{error['Message']}" }.join(" ")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def check_dependencies!
|
44
|
+
raise RuntimeError, "AWS CLI is not installed" unless cli_installed?
|
45
|
+
end
|
46
|
+
|
47
|
+
def cli_installed?
|
48
|
+
`aws --version 2> /dev/null`
|
49
|
+
$?.success?
|
50
|
+
end
|
51
|
+
end
|
@@ -1,11 +1,17 @@
|
|
1
1
|
class Kamal::Secrets::Adapters::Base
|
2
2
|
delegate :optionize, to: Kamal::Utils
|
3
3
|
|
4
|
-
def fetch(secrets, account
|
4
|
+
def fetch(secrets, account: nil, from: nil)
|
5
|
+
raise RuntimeError, "Missing required option '--account'" if requires_account? && account.blank?
|
6
|
+
|
5
7
|
check_dependencies!
|
8
|
+
|
6
9
|
session = login(account)
|
7
|
-
|
8
|
-
|
10
|
+
fetch_secrets(secrets, from: from, account: account, session: session)
|
11
|
+
end
|
12
|
+
|
13
|
+
def requires_account?
|
14
|
+
true
|
9
15
|
end
|
10
16
|
|
11
17
|
private
|
@@ -20,4 +26,8 @@ class Kamal::Secrets::Adapters::Base
|
|
20
26
|
def check_dependencies!
|
21
27
|
raise NotImplementedError
|
22
28
|
end
|
29
|
+
|
30
|
+
def prefixed_secrets(secrets, from:)
|
31
|
+
secrets.map { |secret| [ from, secret ].compact.join("/") }
|
32
|
+
end
|
23
33
|
end
|
@@ -21,9 +21,9 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
|
21
21
|
session
|
22
22
|
end
|
23
23
|
|
24
|
-
def fetch_secrets(secrets, account:, session:)
|
24
|
+
def fetch_secrets(secrets, from:, account:, session:)
|
25
25
|
{}.tap do |results|
|
26
|
-
items_fields(secrets).each do |item, fields|
|
26
|
+
items_fields(prefixed_secrets(secrets, from: from)).each do |item, fields|
|
27
27
|
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
28
28
|
raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
|
29
29
|
item_json = JSON.parse(item_json)
|