dash 2.12.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 +13 -0
- data/bin/dash +18 -0
- data/bin/kamal +18 -0
- data/lib/kamal/cli/accessory.rb +342 -0
- data/lib/kamal/cli/alias/command.rb +10 -0
- data/lib/kamal/cli/app/assets.rb +24 -0
- data/lib/kamal/cli/app/boot.rb +126 -0
- 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 +368 -0
- data/lib/kamal/cli/base.rb +324 -0
- data/lib/kamal/cli/build/clone.rb +59 -0
- data/lib/kamal/cli/build/port_forwarding.rb +66 -0
- data/lib/kamal/cli/build.rb +242 -0
- data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
- data/lib/kamal/cli/healthcheck/error.rb +2 -0
- data/lib/kamal/cli/healthcheck/poller.rb +42 -0
- data/lib/kamal/cli/lock.rb +34 -0
- data/lib/kamal/cli/main.rb +299 -0
- data/lib/kamal/cli/proxy.rb +419 -0
- data/lib/kamal/cli/prune.rb +34 -0
- data/lib/kamal/cli/registry.rb +49 -0
- data/lib/kamal/cli/secrets.rb +50 -0
- data/lib/kamal/cli/server.rb +70 -0
- data/lib/kamal/cli/templates/deploy.yml +102 -0
- data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
- data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -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 +122 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/secrets +22 -0
- data/lib/kamal/cli.rb +9 -0
- data/lib/kamal/commander/specifics.rb +62 -0
- data/lib/kamal/commander.rb +230 -0
- data/lib/kamal/commands/accessory/proxy.rb +16 -0
- data/lib/kamal/commands/accessory.rb +118 -0
- data/lib/kamal/commands/app/assets.rb +51 -0
- data/lib/kamal/commands/app/containers.rb +31 -0
- data/lib/kamal/commands/app/error_pages.rb +9 -0
- data/lib/kamal/commands/app/execution.rb +38 -0
- data/lib/kamal/commands/app/images.rb +13 -0
- data/lib/kamal/commands/app/logging.rb +28 -0
- data/lib/kamal/commands/app/proxy.rb +32 -0
- data/lib/kamal/commands/app.rb +125 -0
- data/lib/kamal/commands/auditor.rb +39 -0
- data/lib/kamal/commands/base.rb +147 -0
- data/lib/kamal/commands/builder/base.rb +143 -0
- data/lib/kamal/commands/builder/clone.rb +32 -0
- data/lib/kamal/commands/builder/cloud.rb +22 -0
- data/lib/kamal/commands/builder/hybrid.rb +21 -0
- data/lib/kamal/commands/builder/local.rb +20 -0
- data/lib/kamal/commands/builder/pack.rb +46 -0
- data/lib/kamal/commands/builder/remote.rb +75 -0
- data/lib/kamal/commands/builder.rb +54 -0
- data/lib/kamal/commands/docker.rb +50 -0
- data/lib/kamal/commands/hook.rb +20 -0
- data/lib/kamal/commands/loadbalancer.rb +130 -0
- data/lib/kamal/commands/lock.rb +70 -0
- data/lib/kamal/commands/proxy.rb +150 -0
- data/lib/kamal/commands/prune.rb +38 -0
- data/lib/kamal/commands/registry.rb +38 -0
- data/lib/kamal/commands/server.rb +15 -0
- data/lib/kamal/commands.rb +2 -0
- data/lib/kamal/configuration/accessory.rb +280 -0
- data/lib/kamal/configuration/alias.rb +15 -0
- data/lib/kamal/configuration/boot.rb +29 -0
- data/lib/kamal/configuration/builder.rb +218 -0
- data/lib/kamal/configuration/docs/accessory.yml +160 -0
- data/lib/kamal/configuration/docs/alias.yml +29 -0
- data/lib/kamal/configuration/docs/boot.yml +21 -0
- data/lib/kamal/configuration/docs/builder.yml +132 -0
- data/lib/kamal/configuration/docs/configuration.yml +228 -0
- data/lib/kamal/configuration/docs/env.yml +118 -0
- data/lib/kamal/configuration/docs/logging.yml +21 -0
- data/lib/kamal/configuration/docs/output.yml +25 -0
- data/lib/kamal/configuration/docs/proxy.yml +207 -0
- data/lib/kamal/configuration/docs/registry.yml +64 -0
- data/lib/kamal/configuration/docs/role.yml +54 -0
- data/lib/kamal/configuration/docs/servers.yml +27 -0
- data/lib/kamal/configuration/docs/ssh.yml +81 -0
- data/lib/kamal/configuration/docs/sshkit.yml +31 -0
- data/lib/kamal/configuration/env/tag.rb +13 -0
- data/lib/kamal/configuration/env.rb +42 -0
- data/lib/kamal/configuration/loadbalancer.rb +34 -0
- data/lib/kamal/configuration/logging.rb +33 -0
- data/lib/kamal/configuration/output.rb +34 -0
- data/lib/kamal/configuration/proxy/boot.rb +124 -0
- data/lib/kamal/configuration/proxy/run.rb +152 -0
- data/lib/kamal/configuration/proxy.rb +156 -0
- data/lib/kamal/configuration/registry.rb +40 -0
- data/lib/kamal/configuration/role.rb +247 -0
- data/lib/kamal/configuration/servers.rb +25 -0
- data/lib/kamal/configuration/ssh.rb +76 -0
- data/lib/kamal/configuration/sshkit.rb +26 -0
- data/lib/kamal/configuration/validation.rb +27 -0
- data/lib/kamal/configuration/validator/accessory.rb +13 -0
- data/lib/kamal/configuration/validator/alias.rb +15 -0
- data/lib/kamal/configuration/validator/builder.rb +15 -0
- data/lib/kamal/configuration/validator/configuration.rb +6 -0
- data/lib/kamal/configuration/validator/env.rb +54 -0
- data/lib/kamal/configuration/validator/proxy.rb +47 -0
- data/lib/kamal/configuration/validator/registry.rb +27 -0
- data/lib/kamal/configuration/validator/role.rb +13 -0
- data/lib/kamal/configuration/validator/servers.rb +7 -0
- data/lib/kamal/configuration/validator.rb +251 -0
- data/lib/kamal/configuration/volume.rb +29 -0
- data/lib/kamal/configuration.rb +465 -0
- data/lib/kamal/docker.rb +30 -0
- data/lib/kamal/env_file.rb +44 -0
- data/lib/kamal/git.rb +37 -0
- data/lib/kamal/otel_shipper.rb +176 -0
- data/lib/kamal/output/base_logger.rb +29 -0
- data/lib/kamal/output/file_logger.rb +51 -0
- data/lib/kamal/output/formatter.rb +36 -0
- data/lib/kamal/output/otel_logger.rb +70 -0
- data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +59 -0
- data/lib/kamal/secrets/adapters/base.rb +33 -0
- data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
- 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 +40 -0
- data/lib/kamal/secrets/adapters/one_password.rb +104 -0
- data/lib/kamal/secrets/adapters/passbolt.rb +129 -0
- data/lib/kamal/secrets/adapters/test.rb +16 -0
- data/lib/kamal/secrets/adapters.rb +16 -0
- data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +47 -0
- data/lib/kamal/secrets.rb +53 -0
- data/lib/kamal/sshkit_with_ext.rb +273 -0
- data/lib/kamal/tags.rb +40 -0
- data/lib/kamal/utils/sensitive.rb +20 -0
- data/lib/kamal/utils.rb +110 -0
- data/lib/kamal/version.rb +3 -0
- data/lib/kamal.rb +15 -0
- metadata +388 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
class Kamal::Configuration::Proxy
|
|
2
|
+
include Kamal::Configuration::Validation
|
|
3
|
+
|
|
4
|
+
DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified", "User-Agent" ]
|
|
5
|
+
CONTAINER_NAME = "kamal-proxy"
|
|
6
|
+
LOADBALANCER_CONTAINER_NAME = "kamal-loadbalancer"
|
|
7
|
+
|
|
8
|
+
delegate :argumentize, :optionize, to: Kamal::Utils
|
|
9
|
+
|
|
10
|
+
attr_reader :config, :proxy_config, :role_name, :run, :secrets
|
|
11
|
+
def initialize(config:, proxy_config:, role_name: nil, secrets:, context: "proxy")
|
|
12
|
+
@config = config
|
|
13
|
+
@proxy_config = proxy_config
|
|
14
|
+
@proxy_config = {} if @proxy_config.nil?
|
|
15
|
+
@role_name = role_name
|
|
16
|
+
@secrets = secrets
|
|
17
|
+
validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
|
|
18
|
+
@run = Kamal::Configuration::Proxy::Run.new(config, run_config: @proxy_config["run"], context: "#{context}/run") if @proxy_config && @proxy_config["run"].present?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def app_port
|
|
22
|
+
proxy_config.fetch("app_port", 80)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ssl?
|
|
26
|
+
proxy_config.fetch("ssl", false)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def hosts
|
|
30
|
+
proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def loadbalancer
|
|
34
|
+
proxy_config["loadbalancer"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_balancing?
|
|
38
|
+
effective_loadbalancer.present?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def effective_loadbalancer
|
|
42
|
+
return false if loadbalancer == false
|
|
43
|
+
return loadbalancer if loadbalancer.present?
|
|
44
|
+
return config.primary_role.hosts.first if config.primary_role && Array(config.primary_role.hosts).size > 1
|
|
45
|
+
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def loadbalancer_on_proxy_host?
|
|
50
|
+
load_balancing? && config.proxy_hosts.include?(effective_loadbalancer)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def custom_ssl_certificate?
|
|
54
|
+
ssl = proxy_config["ssl"]
|
|
55
|
+
return false unless ssl.is_a?(Hash)
|
|
56
|
+
ssl["certificate_pem"].present? && ssl["private_key_pem"].present?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def certificate_pem_content
|
|
60
|
+
ssl = proxy_config["ssl"]
|
|
61
|
+
return nil unless ssl.is_a?(Hash)
|
|
62
|
+
secrets[ssl["certificate_pem"]]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def private_key_pem_content
|
|
66
|
+
ssl = proxy_config["ssl"]
|
|
67
|
+
return nil unless ssl.is_a?(Hash)
|
|
68
|
+
secrets[ssl["private_key_pem"]]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def host_tls_cert
|
|
72
|
+
tls_path(config.proxy_boot.tls_directory, "cert.pem")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def host_tls_key
|
|
76
|
+
tls_path(config.proxy_boot.tls_directory, "key.pem")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def container_tls_cert
|
|
80
|
+
tls_path(config.proxy_boot.tls_container_directory, "cert.pem")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def container_tls_key
|
|
84
|
+
tls_path(config.proxy_boot.tls_container_directory, "key.pem") if custom_ssl_certificate?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def path_prefixes
|
|
88
|
+
proxy_config["path_prefixes"] || proxy_config["path_prefix"]&.split(",") || []
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def deploy_options
|
|
92
|
+
opts = {
|
|
93
|
+
host: hosts,
|
|
94
|
+
tls: ssl? ? true : nil,
|
|
95
|
+
"tls-certificate-path": container_tls_cert,
|
|
96
|
+
"tls-private-key-path": container_tls_key,
|
|
97
|
+
"deploy-timeout": seconds_duration(config.deploy_timeout),
|
|
98
|
+
"drain-timeout": seconds_duration(config.drain_timeout),
|
|
99
|
+
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
|
|
100
|
+
"health-check-timeout": seconds_duration(proxy_config.dig("healthcheck", "timeout")),
|
|
101
|
+
"health-check-path": proxy_config.dig("healthcheck", "path"),
|
|
102
|
+
"target-timeout": seconds_duration(proxy_config["response_timeout"]),
|
|
103
|
+
"buffer-requests": proxy_config.fetch("buffering", { "requests": true }).fetch("requests", true),
|
|
104
|
+
"buffer-responses": proxy_config.fetch("buffering", { "responses": true }).fetch("responses", true),
|
|
105
|
+
"buffer-memory": proxy_config.dig("buffering", "memory"),
|
|
106
|
+
"max-request-body": proxy_config.dig("buffering", "max_request_body"),
|
|
107
|
+
"max-response-body": proxy_config.dig("buffering", "max_response_body"),
|
|
108
|
+
"path-prefix": path_prefixes,
|
|
109
|
+
"strip-path-prefix": proxy_config.dig("strip_path_prefix"),
|
|
110
|
+
"forward-headers": proxy_config.dig("forward_headers"),
|
|
111
|
+
"tls-redirect": proxy_config.dig("ssl_redirect"),
|
|
112
|
+
"log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
|
|
113
|
+
"log-response-header": proxy_config.dig("logging", "response_headers"),
|
|
114
|
+
"error-pages": error_pages
|
|
115
|
+
}.compact
|
|
116
|
+
|
|
117
|
+
if load_balancing?
|
|
118
|
+
opts.delete(:host)
|
|
119
|
+
opts.delete(:tls)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
opts
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def deploy_command_args(target:)
|
|
126
|
+
optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "="
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def stop_options(drain_timeout: nil, message: nil)
|
|
130
|
+
{
|
|
131
|
+
"drain-timeout": seconds_duration(drain_timeout),
|
|
132
|
+
message: message
|
|
133
|
+
}.compact
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def stop_command_args(**options)
|
|
137
|
+
optionize stop_options(**options), with: "="
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def merge(other)
|
|
141
|
+
self.class.new config: config, proxy_config: other.proxy_config.deep_merge(proxy_config), role_name: role_name, secrets: secrets
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
def tls_path(directory, filename)
|
|
146
|
+
File.join([ directory, role_name, filename ].compact) if custom_ssl_certificate?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def seconds_duration(value)
|
|
150
|
+
value ? "#{value}s" : nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def error_pages
|
|
154
|
+
File.join config.proxy_boot.error_pages_container_directory, config.version if config.error_pages_path
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class Kamal::Configuration::Registry
|
|
2
|
+
include Kamal::Configuration::Validation
|
|
3
|
+
|
|
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
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def server
|
|
11
|
+
registry_config["server"]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def username
|
|
15
|
+
lookup("username")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def password
|
|
19
|
+
lookup("password")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def local?
|
|
23
|
+
server.to_s.match?("^localhost[:$]")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def local_port
|
|
27
|
+
local? ? (server.split(":").last.to_i || 80) : nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
attr_reader :registry_config, :secrets
|
|
32
|
+
|
|
33
|
+
def lookup(key)
|
|
34
|
+
if registry_config[key].is_a?(Array)
|
|
35
|
+
secrets[registry_config[key].first]
|
|
36
|
+
else
|
|
37
|
+
registry_config[key]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
class Kamal::Configuration::Role
|
|
2
|
+
include Kamal::Configuration::Validation
|
|
3
|
+
|
|
4
|
+
delegate :argumentize, :optionize, to: Kamal::Utils
|
|
5
|
+
|
|
6
|
+
attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_proxy
|
|
7
|
+
|
|
8
|
+
alias to_s name
|
|
9
|
+
|
|
10
|
+
def initialize(name, config:)
|
|
11
|
+
@name, @config = name.inquiry, config
|
|
12
|
+
validate! \
|
|
13
|
+
role_config,
|
|
14
|
+
example: validation_yml["servers"]["workers"],
|
|
15
|
+
context: "servers/#{name}",
|
|
16
|
+
with: Kamal::Configuration::Validator::Role
|
|
17
|
+
|
|
18
|
+
@specialized_env = Kamal::Configuration::Env.new \
|
|
19
|
+
config: specializations.fetch("env", {}),
|
|
20
|
+
secrets: config.secrets,
|
|
21
|
+
context: "servers/#{name}/env"
|
|
22
|
+
|
|
23
|
+
@specialized_logging = Kamal::Configuration::Logging.new \
|
|
24
|
+
logging_config: specializations.fetch("logging", {}),
|
|
25
|
+
context: "servers/#{name}/logging"
|
|
26
|
+
|
|
27
|
+
initialize_specialized_proxy
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def primary_host
|
|
31
|
+
hosts.first
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def hosts
|
|
35
|
+
tagged_hosts.keys
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def env_tags(host)
|
|
39
|
+
tagged_hosts.fetch(host).collect { |tag| config.env_tag(tag) }.compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cmd
|
|
43
|
+
specializations["cmd"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def option_args
|
|
47
|
+
optionize docker_options.reject { |key, _| key.to_s == "restart" }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def restart_policy
|
|
51
|
+
restart_policy_option || "unless-stopped"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def labels
|
|
55
|
+
default_labels.merge(custom_labels)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def label_args
|
|
59
|
+
argumentize "--label", labels
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def logging_args
|
|
63
|
+
logging.args
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def logging
|
|
67
|
+
@logging ||= config.logging.merge(specialized_logging)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def proxy
|
|
71
|
+
@proxy ||= specialized_proxy.merge(config.proxy) if running_proxy?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def running_proxy?
|
|
75
|
+
@running_proxy
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ssl?
|
|
79
|
+
running_proxy? && proxy.ssl?
|
|
80
|
+
end
|
|
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 = stop_timeout || (running_proxy? ? nil : config.drain_timeout)
|
|
85
|
+
|
|
86
|
+
[ *argumentize("-t", timeout) ]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def stop_timeout
|
|
90
|
+
specializations["stop_timeout"] || config.stop_timeout
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def env(host)
|
|
94
|
+
@envs ||= {}
|
|
95
|
+
@envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def env_args(host)
|
|
99
|
+
[ *env(host).clear_args, *argumentize("--env-file", secrets_path) ]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def env_directory
|
|
103
|
+
File.join(config.env_directory, "roles")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def secrets_io(host)
|
|
107
|
+
env(host).secrets_io
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def secrets_path
|
|
111
|
+
File.join(config.env_directory, "roles", "#{name}.env")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def asset_volume_args
|
|
115
|
+
asset_volume&.docker_args
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def primary?
|
|
120
|
+
name == @config.primary_role_name
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def container_name(version = nil)
|
|
125
|
+
[ container_prefix, version || config.version ].compact.join("-")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def container_prefix
|
|
129
|
+
[ config.service, name, config.destination ].compact.join("-")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def asset_path
|
|
134
|
+
asset_path_config&.dig(0)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def assets?
|
|
138
|
+
asset_path.present? && running_proxy?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def asset_volume(version = config.version)
|
|
142
|
+
if assets?
|
|
143
|
+
Kamal::Configuration::Volume.new \
|
|
144
|
+
host_path: asset_volume_directory(version), container_path: asset_path, options: asset_path_options
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def asset_path_options
|
|
149
|
+
asset_path_config&.dig(1)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def asset_extracted_directory(version = config.version)
|
|
153
|
+
File.join config.assets_directory, "extracted", [ name, version ].join("-")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def asset_volume_directory(version = config.version)
|
|
157
|
+
File.join config.assets_directory, "volumes", [ name, version ].join("-")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def ensure_one_host_for_ssl
|
|
161
|
+
# Skip SSL validation when a loadbalancer is present or custom certificates are provided
|
|
162
|
+
if running_proxy? && proxy.ssl? && hosts.size > 1 && !proxy.loadbalancer.present? && !proxy.custom_ssl_certificate?
|
|
163
|
+
raise Kamal::ConfigurationError, "SSL is only supported on a single server unless you provide custom certificates or configure a loadbalancer, found #{hosts.size} servers for role #{name}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
def initialize_specialized_proxy
|
|
169
|
+
proxy_specializations = specializations["proxy"]
|
|
170
|
+
|
|
171
|
+
if primary?
|
|
172
|
+
# only false means no proxy for non-primary roles
|
|
173
|
+
@running_proxy = proxy_specializations != false
|
|
174
|
+
else
|
|
175
|
+
# false and nil both mean no proxy for non-primary roles
|
|
176
|
+
@running_proxy = !!proxy_specializations
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
if running_proxy?
|
|
180
|
+
proxy_config = proxy_specializations == true || proxy_specializations.nil? ? {} : proxy_specializations
|
|
181
|
+
|
|
182
|
+
@specialized_proxy = Kamal::Configuration::Proxy.new \
|
|
183
|
+
config: config,
|
|
184
|
+
proxy_config: proxy_config,
|
|
185
|
+
secrets: config.secrets,
|
|
186
|
+
role_name: name,
|
|
187
|
+
context: "servers/#{name}/proxy"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def tagged_hosts
|
|
192
|
+
{}.tap do |tagged_hosts|
|
|
193
|
+
extract_hosts_from_config.map do |host_config|
|
|
194
|
+
if host_config.is_a?(Hash)
|
|
195
|
+
host, tags = host_config.first
|
|
196
|
+
tagged_hosts[host] = Array(tags)
|
|
197
|
+
elsif host_config.is_a?(String)
|
|
198
|
+
tagged_hosts[host_config] = []
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def extract_hosts_from_config
|
|
205
|
+
if config.raw_config.servers.is_a?(Array)
|
|
206
|
+
config.raw_config.servers
|
|
207
|
+
else
|
|
208
|
+
servers = config.raw_config.servers[name]
|
|
209
|
+
servers.is_a?(Array) ? servers : Array(servers["hosts"])
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def default_labels
|
|
214
|
+
{ "service" => config.service, "role" => name, "destination" => config.destination }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def specializations
|
|
218
|
+
@specializations ||= role_config.is_a?(Array) ? {} : role_config
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def role_config
|
|
222
|
+
@role_config ||= config.raw_config.servers.is_a?(Array) ? {} : config.raw_config.servers[name]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def docker_options
|
|
226
|
+
specializations["options"] || {}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def restart_policy_option
|
|
230
|
+
docker_options.find { |key, _| key.to_s == "restart" }&.last
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def custom_labels
|
|
234
|
+
Hash.new.tap do |labels|
|
|
235
|
+
labels.merge!(config.labels) if config.labels.present?
|
|
236
|
+
labels.merge!(specializations["labels"]) if specializations["labels"].present?
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def asset_path_config
|
|
241
|
+
raw_path = specializations["asset_path"] || config.asset_path
|
|
242
|
+
return nil unless raw_path.present?
|
|
243
|
+
|
|
244
|
+
parts = raw_path.split(":", 2)
|
|
245
|
+
[ parts[0], parts[1] ]
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class Kamal::Configuration::Servers
|
|
2
|
+
include Kamal::Configuration::Validation
|
|
3
|
+
|
|
4
|
+
attr_reader :config, :servers_config, :roles
|
|
5
|
+
|
|
6
|
+
def initialize(config:)
|
|
7
|
+
@config = config
|
|
8
|
+
@servers_config = config.raw_config.servers
|
|
9
|
+
validate! servers_config, with: Kamal::Configuration::Validator::Servers
|
|
10
|
+
|
|
11
|
+
@roles = role_names.map { |role_name| Kamal::Configuration::Role.new role_name, config: config }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
def role_names
|
|
16
|
+
case servers_config
|
|
17
|
+
when Array
|
|
18
|
+
[ "web" ]
|
|
19
|
+
when NilClass
|
|
20
|
+
[]
|
|
21
|
+
else
|
|
22
|
+
servers_config.keys.sort
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
class Kamal::Configuration::Ssh
|
|
2
|
+
LOGGER = ::Logger.new(STDERR)
|
|
3
|
+
|
|
4
|
+
include Kamal::Configuration::Validation
|
|
5
|
+
|
|
6
|
+
attr_reader :ssh_config, :secrets
|
|
7
|
+
|
|
8
|
+
def initialize(config:)
|
|
9
|
+
@ssh_config = config.raw_config.ssh || {}
|
|
10
|
+
@secrets = config.secrets
|
|
11
|
+
validate! ssh_config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def user
|
|
15
|
+
ssh_config.fetch("user", "root")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def port
|
|
19
|
+
ssh_config.fetch("port", 22)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def proxy
|
|
23
|
+
if (proxy = ssh_config["proxy"])
|
|
24
|
+
Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
|
|
25
|
+
elsif (proxy_command = ssh_config["proxy_command"])
|
|
26
|
+
Net::SSH::Proxy::Command.new(proxy_command)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def keys_only
|
|
31
|
+
ssh_config["keys_only"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def keys
|
|
35
|
+
ssh_config["keys"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def key_data
|
|
39
|
+
key_data = ssh_config["key_data"]
|
|
40
|
+
return unless key_data
|
|
41
|
+
|
|
42
|
+
key_data.map do |k|
|
|
43
|
+
if secrets.key?(k)
|
|
44
|
+
secrets[k]
|
|
45
|
+
else
|
|
46
|
+
warn "Inline key_data usage is deprecated and will be removed in Kamal 3. Please store your key_data in a secret."
|
|
47
|
+
k
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def config
|
|
53
|
+
ssh_config["config"]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def forward_agent
|
|
57
|
+
ssh_config["forward_agent"]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def options
|
|
61
|
+
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data, config: config, forward_agent: forward_agent }.compact
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_h
|
|
65
|
+
options.except(:logger).merge(log_level: log_level)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
def logger
|
|
70
|
+
LOGGER.tap { |logger| logger.level = log_level }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def log_level
|
|
74
|
+
ssh_config.fetch("log_level", :fatal)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class Kamal::Configuration::Sshkit
|
|
2
|
+
include Kamal::Configuration::Validation
|
|
3
|
+
|
|
4
|
+
attr_reader :sshkit_config
|
|
5
|
+
|
|
6
|
+
def initialize(config:)
|
|
7
|
+
@sshkit_config = config.raw_config.sshkit || {}
|
|
8
|
+
validate! sshkit_config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def max_concurrent_starts
|
|
12
|
+
sshkit_config.fetch("max_concurrent_starts", 30)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def pool_idle_timeout
|
|
16
|
+
sshkit_config.fetch("pool_idle_timeout", 900)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def dns_retries
|
|
20
|
+
Integer(sshkit_config.fetch("dns_retries", 3))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
sshkit_config
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "active_support/inflector"
|
|
3
|
+
|
|
4
|
+
module Kamal::Configuration::Validation
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
def validation_doc
|
|
9
|
+
@validation_doc ||= File.read(File.join(File.dirname(__FILE__), "docs", "#{validation_config_key}.yml"))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def validation_config_key
|
|
13
|
+
@validation_config_key ||= name.demodulize.underscore
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate!(config, example: nil, context: nil, with: Kamal::Configuration::Validator)
|
|
18
|
+
context ||= self.class.validation_config_key
|
|
19
|
+
example ||= validation_yml[self.class.validation_config_key]
|
|
20
|
+
|
|
21
|
+
with.new(config, example: example, context: context).validate!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validation_yml
|
|
25
|
+
@validation_yml ||= YAML.load(self.class.validation_doc)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validator
|
|
2
|
+
def validate!
|
|
3
|
+
super
|
|
4
|
+
|
|
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
|
+
end
|
|
8
|
+
|
|
9
|
+
validate_labels!(config["labels"])
|
|
10
|
+
|
|
11
|
+
validate_docker_options!(config["options"])
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator
|
|
2
|
+
def validate!
|
|
3
|
+
super
|
|
4
|
+
|
|
5
|
+
if config["cache"] && config["cache"]["type"]
|
|
6
|
+
error "Invalid cache type: #{config["cache"]["type"]}" unless [ "gha", "registry" ].include?(config["cache"]["type"])
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
error "Builder arch not set" unless config["arch"].present?
|
|
10
|
+
|
|
11
|
+
error "buildpacks only support building for one arch" if config["pack"] && config["arch"].is_a?(Array) && config["arch"].size > 1
|
|
12
|
+
|
|
13
|
+
error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank?
|
|
14
|
+
end
|
|
15
|
+
end
|