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.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +13 -0
  4. data/bin/dash +18 -0
  5. data/bin/kamal +18 -0
  6. data/lib/kamal/cli/accessory.rb +342 -0
  7. data/lib/kamal/cli/alias/command.rb +10 -0
  8. data/lib/kamal/cli/app/assets.rb +24 -0
  9. data/lib/kamal/cli/app/boot.rb +126 -0
  10. data/lib/kamal/cli/app/error_pages.rb +33 -0
  11. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  12. data/lib/kamal/cli/app.rb +368 -0
  13. data/lib/kamal/cli/base.rb +324 -0
  14. data/lib/kamal/cli/build/clone.rb +59 -0
  15. data/lib/kamal/cli/build/port_forwarding.rb +66 -0
  16. data/lib/kamal/cli/build.rb +242 -0
  17. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  18. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  19. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  20. data/lib/kamal/cli/lock.rb +34 -0
  21. data/lib/kamal/cli/main.rb +299 -0
  22. data/lib/kamal/cli/proxy.rb +419 -0
  23. data/lib/kamal/cli/prune.rb +34 -0
  24. data/lib/kamal/cli/registry.rb +49 -0
  25. data/lib/kamal/cli/secrets.rb +50 -0
  26. data/lib/kamal/cli/server.rb +70 -0
  27. data/lib/kamal/cli/templates/deploy.yml +102 -0
  28. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  29. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  30. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  31. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  32. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  33. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  34. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  35. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
  36. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  37. data/lib/kamal/cli/templates/secrets +22 -0
  38. data/lib/kamal/cli.rb +9 -0
  39. data/lib/kamal/commander/specifics.rb +62 -0
  40. data/lib/kamal/commander.rb +230 -0
  41. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  42. data/lib/kamal/commands/accessory.rb +118 -0
  43. data/lib/kamal/commands/app/assets.rb +51 -0
  44. data/lib/kamal/commands/app/containers.rb +31 -0
  45. data/lib/kamal/commands/app/error_pages.rb +9 -0
  46. data/lib/kamal/commands/app/execution.rb +38 -0
  47. data/lib/kamal/commands/app/images.rb +13 -0
  48. data/lib/kamal/commands/app/logging.rb +28 -0
  49. data/lib/kamal/commands/app/proxy.rb +32 -0
  50. data/lib/kamal/commands/app.rb +125 -0
  51. data/lib/kamal/commands/auditor.rb +39 -0
  52. data/lib/kamal/commands/base.rb +147 -0
  53. data/lib/kamal/commands/builder/base.rb +143 -0
  54. data/lib/kamal/commands/builder/clone.rb +32 -0
  55. data/lib/kamal/commands/builder/cloud.rb +22 -0
  56. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  57. data/lib/kamal/commands/builder/local.rb +20 -0
  58. data/lib/kamal/commands/builder/pack.rb +46 -0
  59. data/lib/kamal/commands/builder/remote.rb +75 -0
  60. data/lib/kamal/commands/builder.rb +54 -0
  61. data/lib/kamal/commands/docker.rb +50 -0
  62. data/lib/kamal/commands/hook.rb +20 -0
  63. data/lib/kamal/commands/loadbalancer.rb +130 -0
  64. data/lib/kamal/commands/lock.rb +70 -0
  65. data/lib/kamal/commands/proxy.rb +150 -0
  66. data/lib/kamal/commands/prune.rb +38 -0
  67. data/lib/kamal/commands/registry.rb +38 -0
  68. data/lib/kamal/commands/server.rb +15 -0
  69. data/lib/kamal/commands.rb +2 -0
  70. data/lib/kamal/configuration/accessory.rb +280 -0
  71. data/lib/kamal/configuration/alias.rb +15 -0
  72. data/lib/kamal/configuration/boot.rb +29 -0
  73. data/lib/kamal/configuration/builder.rb +218 -0
  74. data/lib/kamal/configuration/docs/accessory.yml +160 -0
  75. data/lib/kamal/configuration/docs/alias.yml +29 -0
  76. data/lib/kamal/configuration/docs/boot.yml +21 -0
  77. data/lib/kamal/configuration/docs/builder.yml +132 -0
  78. data/lib/kamal/configuration/docs/configuration.yml +228 -0
  79. data/lib/kamal/configuration/docs/env.yml +118 -0
  80. data/lib/kamal/configuration/docs/logging.yml +21 -0
  81. data/lib/kamal/configuration/docs/output.yml +25 -0
  82. data/lib/kamal/configuration/docs/proxy.yml +207 -0
  83. data/lib/kamal/configuration/docs/registry.yml +64 -0
  84. data/lib/kamal/configuration/docs/role.yml +54 -0
  85. data/lib/kamal/configuration/docs/servers.yml +27 -0
  86. data/lib/kamal/configuration/docs/ssh.yml +81 -0
  87. data/lib/kamal/configuration/docs/sshkit.yml +31 -0
  88. data/lib/kamal/configuration/env/tag.rb +13 -0
  89. data/lib/kamal/configuration/env.rb +42 -0
  90. data/lib/kamal/configuration/loadbalancer.rb +34 -0
  91. data/lib/kamal/configuration/logging.rb +33 -0
  92. data/lib/kamal/configuration/output.rb +34 -0
  93. data/lib/kamal/configuration/proxy/boot.rb +124 -0
  94. data/lib/kamal/configuration/proxy/run.rb +152 -0
  95. data/lib/kamal/configuration/proxy.rb +156 -0
  96. data/lib/kamal/configuration/registry.rb +40 -0
  97. data/lib/kamal/configuration/role.rb +247 -0
  98. data/lib/kamal/configuration/servers.rb +25 -0
  99. data/lib/kamal/configuration/ssh.rb +76 -0
  100. data/lib/kamal/configuration/sshkit.rb +26 -0
  101. data/lib/kamal/configuration/validation.rb +27 -0
  102. data/lib/kamal/configuration/validator/accessory.rb +13 -0
  103. data/lib/kamal/configuration/validator/alias.rb +15 -0
  104. data/lib/kamal/configuration/validator/builder.rb +15 -0
  105. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  106. data/lib/kamal/configuration/validator/env.rb +54 -0
  107. data/lib/kamal/configuration/validator/proxy.rb +47 -0
  108. data/lib/kamal/configuration/validator/registry.rb +27 -0
  109. data/lib/kamal/configuration/validator/role.rb +13 -0
  110. data/lib/kamal/configuration/validator/servers.rb +7 -0
  111. data/lib/kamal/configuration/validator.rb +251 -0
  112. data/lib/kamal/configuration/volume.rb +29 -0
  113. data/lib/kamal/configuration.rb +465 -0
  114. data/lib/kamal/docker.rb +30 -0
  115. data/lib/kamal/env_file.rb +44 -0
  116. data/lib/kamal/git.rb +37 -0
  117. data/lib/kamal/otel_shipper.rb +176 -0
  118. data/lib/kamal/output/base_logger.rb +29 -0
  119. data/lib/kamal/output/file_logger.rb +51 -0
  120. data/lib/kamal/output/formatter.rb +36 -0
  121. data/lib/kamal/output/otel_logger.rb +70 -0
  122. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +59 -0
  123. data/lib/kamal/secrets/adapters/base.rb +33 -0
  124. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  125. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  126. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  127. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  128. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  129. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  130. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  131. data/lib/kamal/secrets/adapters/passbolt.rb +129 -0
  132. data/lib/kamal/secrets/adapters/test.rb +16 -0
  133. data/lib/kamal/secrets/adapters.rb +16 -0
  134. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +47 -0
  135. data/lib/kamal/secrets.rb +53 -0
  136. data/lib/kamal/sshkit_with_ext.rb +273 -0
  137. data/lib/kamal/tags.rb +40 -0
  138. data/lib/kamal/utils/sensitive.rb +20 -0
  139. data/lib/kamal/utils.rb +110 -0
  140. data/lib/kamal/version.rb +3 -0
  141. data/lib/kamal.rb +15 -0
  142. 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
@@ -0,0 +1,6 @@
1
+ class Kamal::Configuration::Validator::Configuration < Kamal::Configuration::Validator
2
+ private
3
+ def allow_extensions?
4
+ true
5
+ end
6
+ end