kamal-insecure 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.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +13 -0
  4. data/bin/kamal +18 -0
  5. data/lib/kamal/cli/accessory.rb +313 -0
  6. data/lib/kamal/cli/alias/command.rb +10 -0
  7. data/lib/kamal/cli/app/assets.rb +24 -0
  8. data/lib/kamal/cli/app/boot.rb +126 -0
  9. data/lib/kamal/cli/app/error_pages.rb +33 -0
  10. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  11. data/lib/kamal/cli/app.rb +400 -0
  12. data/lib/kamal/cli/base.rb +223 -0
  13. data/lib/kamal/cli/build/clone.rb +61 -0
  14. data/lib/kamal/cli/build.rb +204 -0
  15. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  16. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  17. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  18. data/lib/kamal/cli/lock.rb +45 -0
  19. data/lib/kamal/cli/main.rb +277 -0
  20. data/lib/kamal/cli/proxy.rb +290 -0
  21. data/lib/kamal/cli/prune.rb +34 -0
  22. data/lib/kamal/cli/registry.rb +19 -0
  23. data/lib/kamal/cli/secrets.rb +49 -0
  24. data/lib/kamal/cli/server.rb +50 -0
  25. data/lib/kamal/cli/templates/deploy.yml +101 -0
  26. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  28. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  29. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  30. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  31. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  32. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  33. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
  34. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  35. data/lib/kamal/cli/templates/secrets +17 -0
  36. data/lib/kamal/cli.rb +9 -0
  37. data/lib/kamal/commander/specifics.rb +62 -0
  38. data/lib/kamal/commander.rb +167 -0
  39. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  40. data/lib/kamal/commands/accessory.rb +113 -0
  41. data/lib/kamal/commands/app/assets.rb +51 -0
  42. data/lib/kamal/commands/app/containers.rb +31 -0
  43. data/lib/kamal/commands/app/error_pages.rb +9 -0
  44. data/lib/kamal/commands/app/execution.rb +32 -0
  45. data/lib/kamal/commands/app/images.rb +13 -0
  46. data/lib/kamal/commands/app/logging.rb +28 -0
  47. data/lib/kamal/commands/app/proxy.rb +32 -0
  48. data/lib/kamal/commands/app.rb +124 -0
  49. data/lib/kamal/commands/auditor.rb +39 -0
  50. data/lib/kamal/commands/base.rb +134 -0
  51. data/lib/kamal/commands/builder/base.rb +124 -0
  52. data/lib/kamal/commands/builder/clone.rb +31 -0
  53. data/lib/kamal/commands/builder/cloud.rb +22 -0
  54. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  55. data/lib/kamal/commands/builder/local.rb +14 -0
  56. data/lib/kamal/commands/builder/pack.rb +46 -0
  57. data/lib/kamal/commands/builder/remote.rb +63 -0
  58. data/lib/kamal/commands/builder.rb +48 -0
  59. data/lib/kamal/commands/docker.rb +34 -0
  60. data/lib/kamal/commands/hook.rb +20 -0
  61. data/lib/kamal/commands/lock.rb +70 -0
  62. data/lib/kamal/commands/proxy.rb +127 -0
  63. data/lib/kamal/commands/prune.rb +38 -0
  64. data/lib/kamal/commands/registry.rb +16 -0
  65. data/lib/kamal/commands/server.rb +15 -0
  66. data/lib/kamal/commands.rb +2 -0
  67. data/lib/kamal/configuration/accessory.rb +241 -0
  68. data/lib/kamal/configuration/alias.rb +15 -0
  69. data/lib/kamal/configuration/boot.rb +25 -0
  70. data/lib/kamal/configuration/builder.rb +211 -0
  71. data/lib/kamal/configuration/docs/accessory.yml +128 -0
  72. data/lib/kamal/configuration/docs/alias.yml +26 -0
  73. data/lib/kamal/configuration/docs/boot.yml +19 -0
  74. data/lib/kamal/configuration/docs/builder.yml +132 -0
  75. data/lib/kamal/configuration/docs/configuration.yml +184 -0
  76. data/lib/kamal/configuration/docs/env.yml +116 -0
  77. data/lib/kamal/configuration/docs/logging.yml +21 -0
  78. data/lib/kamal/configuration/docs/proxy.yml +164 -0
  79. data/lib/kamal/configuration/docs/registry.yml +56 -0
  80. data/lib/kamal/configuration/docs/role.yml +53 -0
  81. data/lib/kamal/configuration/docs/servers.yml +27 -0
  82. data/lib/kamal/configuration/docs/ssh.yml +70 -0
  83. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  84. data/lib/kamal/configuration/env/tag.rb +13 -0
  85. data/lib/kamal/configuration/env.rb +38 -0
  86. data/lib/kamal/configuration/logging.rb +33 -0
  87. data/lib/kamal/configuration/proxy/boot.rb +129 -0
  88. data/lib/kamal/configuration/proxy.rb +124 -0
  89. data/lib/kamal/configuration/registry.rb +32 -0
  90. data/lib/kamal/configuration/role.rb +222 -0
  91. data/lib/kamal/configuration/servers.rb +25 -0
  92. data/lib/kamal/configuration/ssh.rb +57 -0
  93. data/lib/kamal/configuration/sshkit.rb +22 -0
  94. data/lib/kamal/configuration/validation.rb +27 -0
  95. data/lib/kamal/configuration/validator/accessory.rb +13 -0
  96. data/lib/kamal/configuration/validator/alias.rb +15 -0
  97. data/lib/kamal/configuration/validator/builder.rb +15 -0
  98. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  99. data/lib/kamal/configuration/validator/env.rb +54 -0
  100. data/lib/kamal/configuration/validator/proxy.rb +25 -0
  101. data/lib/kamal/configuration/validator/registry.rb +25 -0
  102. data/lib/kamal/configuration/validator/role.rb +13 -0
  103. data/lib/kamal/configuration/validator/servers.rb +7 -0
  104. data/lib/kamal/configuration/validator.rb +191 -0
  105. data/lib/kamal/configuration/volume.rb +22 -0
  106. data/lib/kamal/configuration.rb +372 -0
  107. data/lib/kamal/docker.rb +30 -0
  108. data/lib/kamal/env_file.rb +44 -0
  109. data/lib/kamal/git.rb +37 -0
  110. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
  111. data/lib/kamal/secrets/adapters/base.rb +33 -0
  112. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  113. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  114. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  115. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  116. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  117. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  118. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  119. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  120. data/lib/kamal/secrets/adapters/test.rb +14 -0
  121. data/lib/kamal/secrets/adapters.rb +16 -0
  122. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +33 -0
  123. data/lib/kamal/secrets.rb +42 -0
  124. data/lib/kamal/sshkit_with_ext.rb +142 -0
  125. data/lib/kamal/tags.rb +40 -0
  126. data/lib/kamal/utils/sensitive.rb +20 -0
  127. data/lib/kamal/utils.rb +110 -0
  128. data/lib/kamal/version.rb +3 -0
  129. data/lib/kamal.rb +14 -0
  130. metadata +365 -0
@@ -0,0 +1,129 @@
1
+ class Kamal::Configuration::Proxy::Boot
2
+ MINIMUM_VERSION = "v0.9.0"
3
+ DEFAULT_HTTP_PORT = 80
4
+ DEFAULT_HTTPS_PORT = 443
5
+ DEFAULT_LOG_MAX_SIZE = "10m"
6
+
7
+ attr_reader :config
8
+ delegate :argumentize, :optionize, to: Kamal::Utils
9
+
10
+ def initialize(config:)
11
+ @config = config
12
+ end
13
+
14
+ def publish_args(http_port, https_port, bind_ips = nil)
15
+ ensure_valid_bind_ips(bind_ips)
16
+
17
+ (bind_ips || [ nil ]).map do |bind_ip|
18
+ bind_ip = format_bind_ip(bind_ip)
19
+ publish_http = [ bind_ip, http_port, DEFAULT_HTTP_PORT ].compact.join(":")
20
+ publish_https = [ bind_ip, https_port, DEFAULT_HTTPS_PORT ].compact.join(":")
21
+
22
+ argumentize "--publish", [ publish_http, publish_https ]
23
+ end.join(" ")
24
+ end
25
+
26
+ def logging_args(max_size)
27
+ argumentize "--log-opt", "max-size=#{max_size}" if max_size.present?
28
+ end
29
+
30
+ def default_boot_options
31
+ [
32
+ *(publish_args(DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, nil)),
33
+ *(logging_args(DEFAULT_LOG_MAX_SIZE))
34
+ ]
35
+ end
36
+
37
+ def repository_name
38
+ "basecamp"
39
+ end
40
+
41
+ def image_name
42
+ "kamal-proxy"
43
+ end
44
+
45
+ def image_default
46
+ "#{repository_name}/#{image_name}"
47
+ end
48
+
49
+ def container_name
50
+ "kamal-proxy"
51
+ end
52
+
53
+ def host_directory
54
+ File.join config.run_directory, "proxy"
55
+ end
56
+
57
+ def options_file
58
+ File.join host_directory, "options"
59
+ end
60
+
61
+ def image_file
62
+ File.join host_directory, "image"
63
+ end
64
+
65
+ def image_version_file
66
+ File.join host_directory, "image_version"
67
+ end
68
+
69
+ def run_command_file
70
+ File.join host_directory, "run_command"
71
+ end
72
+
73
+ def apps_directory
74
+ File.join host_directory, "apps-config"
75
+ end
76
+
77
+ def apps_container_directory
78
+ "/home/kamal-proxy/.apps-config"
79
+ end
80
+
81
+ def apps_volume
82
+ Kamal::Configuration::Volume.new \
83
+ host_path: apps_directory,
84
+ container_path: apps_container_directory
85
+ end
86
+
87
+ def app_directory
88
+ File.join apps_directory, config.service_and_destination
89
+ end
90
+
91
+ def app_container_directory
92
+ File.join apps_container_directory, config.service_and_destination
93
+ end
94
+
95
+ def error_pages_directory
96
+ File.join app_directory, "error_pages"
97
+ end
98
+
99
+ def error_pages_container_directory
100
+ File.join app_container_directory, "error_pages"
101
+ end
102
+
103
+ def tls_directory
104
+ File.join app_directory, "tls"
105
+ end
106
+
107
+ def tls_container_directory
108
+ File.join app_container_directory, "tls"
109
+ end
110
+
111
+ private
112
+ def ensure_valid_bind_ips(bind_ips)
113
+ bind_ips.present? && bind_ips.each do |ip|
114
+ next if ip =~ Resolv::IPv4::Regex || ip =~ Resolv::IPv6::Regex
115
+ raise ArgumentError, "Invalid publish IP address: #{ip}"
116
+ end
117
+
118
+ true
119
+ end
120
+
121
+ def format_bind_ip(ip)
122
+ # Ensure IPv6 address inside square brackets - e.g. [::1]
123
+ if ip =~ Resolv::IPv6::Regex && ip !~ /\A\[.*\]\z/
124
+ "[#{ip}]"
125
+ else
126
+ ip
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,124 @@
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
+
7
+ delegate :argumentize, :optionize, to: Kamal::Utils
8
+
9
+ attr_reader :config, :proxy_config, :role_name, :secrets
10
+
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
+ end
19
+
20
+ def app_port
21
+ proxy_config.fetch("app_port", 80)
22
+ end
23
+
24
+ def ssl?
25
+ proxy_config.fetch("ssl", false)
26
+ end
27
+
28
+ def hosts
29
+ proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
30
+ end
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
+
66
+ def deploy_options
67
+ {
68
+ host: hosts,
69
+ tls: ssl? ? true : nil,
70
+ "tls-certificate-path": container_tls_cert,
71
+ "tls-private-key-path": container_tls_key,
72
+ "deploy-timeout": seconds_duration(config.deploy_timeout),
73
+ "drain-timeout": seconds_duration(config.drain_timeout),
74
+ "health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
75
+ "health-check-timeout": seconds_duration(proxy_config.dig("healthcheck", "timeout")),
76
+ "health-check-path": proxy_config.dig("healthcheck", "path"),
77
+ "target-timeout": seconds_duration(proxy_config["response_timeout"]),
78
+ "buffer-requests": proxy_config.fetch("buffering", { "requests": true }).fetch("requests", true),
79
+ "buffer-responses": proxy_config.fetch("buffering", { "responses": true }).fetch("responses", true),
80
+ "buffer-memory": proxy_config.dig("buffering", "memory"),
81
+ "max-request-body": proxy_config.dig("buffering", "max_request_body"),
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"),
85
+ "forward-headers": proxy_config.dig("forward_headers"),
86
+ "tls-redirect": proxy_config.dig("ssl_redirect"),
87
+ "log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
88
+ "log-response-header": proxy_config.dig("logging", "response_headers"),
89
+ "error-pages": error_pages
90
+ }.compact
91
+ end
92
+
93
+ def deploy_command_args(target:)
94
+ optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "="
95
+ end
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
+
108
+ def merge(other)
109
+ self.class.new config: config, proxy_config: other.proxy_config.deep_merge(proxy_config), role_name: role_name, secrets: secrets
110
+ end
111
+
112
+ private
113
+ def tls_path(directory, filename)
114
+ File.join([ directory, role_name, filename ].compact) if custom_ssl_certificate?
115
+ end
116
+
117
+ def seconds_duration(value)
118
+ value ? "#{value}s" : nil
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
124
+ end
@@ -0,0 +1,32 @@
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
+ private
23
+ attr_reader :registry_config, :secrets
24
+
25
+ def lookup(key)
26
+ if registry_config[key].is_a?(Array)
27
+ secrets[registry_config[key].first]
28
+ else
29
+ registry_config[key]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,222 @@
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) }
40
+ end
41
+
42
+ def cmd
43
+ specializations["cmd"]
44
+ end
45
+
46
+ def option_args
47
+ if args = specializations["options"]
48
+ optionize args
49
+ else
50
+ []
51
+ end
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 = running_proxy? ? nil : config.drain_timeout
85
+
86
+ [ *argumentize("-t", timeout) ]
87
+ end
88
+
89
+ def env(host)
90
+ @envs ||= {}
91
+ @envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
92
+ end
93
+
94
+ def env_args(host)
95
+ [ *env(host).clear_args, *argumentize("--env-file", secrets_path) ]
96
+ end
97
+
98
+ def env_directory
99
+ File.join(config.env_directory, "roles")
100
+ end
101
+
102
+ def secrets_io(host)
103
+ env(host).secrets_io
104
+ end
105
+
106
+ def secrets_path
107
+ File.join(config.env_directory, "roles", "#{name}.env")
108
+ end
109
+
110
+ def asset_volume_args
111
+ asset_volume&.docker_args
112
+ end
113
+
114
+
115
+ def primary?
116
+ name == @config.primary_role_name
117
+ end
118
+
119
+
120
+ def container_name(version = nil)
121
+ [ container_prefix, version || config.version ].compact.join("-")
122
+ end
123
+
124
+ def container_prefix
125
+ [ config.service, name, config.destination ].compact.join("-")
126
+ end
127
+
128
+
129
+ def asset_path
130
+ specializations["asset_path"] || config.asset_path
131
+ end
132
+
133
+ def assets?
134
+ asset_path.present? && running_proxy?
135
+ end
136
+
137
+ def asset_volume(version = config.version)
138
+ if assets?
139
+ Kamal::Configuration::Volume.new \
140
+ host_path: asset_volume_directory(version), container_path: asset_path
141
+ end
142
+ end
143
+
144
+ def asset_extracted_directory(version = config.version)
145
+ File.join config.assets_directory, "extracted", [ name, version ].join("-")
146
+ end
147
+
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 && !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
+ end
156
+ end
157
+
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
+ secrets: config.secrets,
177
+ role_name: name,
178
+ context: "servers/#{name}/proxy"
179
+ end
180
+ end
181
+
182
+ def tagged_hosts
183
+ {}.tap do |tagged_hosts|
184
+ extract_hosts_from_config.map do |host_config|
185
+ if host_config.is_a?(Hash)
186
+ host, tags = host_config.first
187
+ tagged_hosts[host] = Array(tags)
188
+ elsif host_config.is_a?(String)
189
+ tagged_hosts[host_config] = []
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ def extract_hosts_from_config
196
+ if config.raw_config.servers.is_a?(Array)
197
+ config.raw_config.servers
198
+ else
199
+ servers = config.raw_config.servers[name]
200
+ servers.is_a?(Array) ? servers : Array(servers["hosts"])
201
+ end
202
+ end
203
+
204
+ def default_labels
205
+ { "service" => config.service, "role" => name, "destination" => config.destination }
206
+ end
207
+
208
+ def specializations
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]
214
+ end
215
+
216
+ def custom_labels
217
+ Hash.new.tap do |labels|
218
+ labels.merge!(config.labels) if config.labels.present?
219
+ labels.merge!(specializations["labels"]) if specializations["labels"].present?
220
+ end
221
+ end
222
+ 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,57 @@
1
+ class Kamal::Configuration::Ssh
2
+ LOGGER = ::Logger.new(STDERR)
3
+
4
+ include Kamal::Configuration::Validation
5
+
6
+ attr_reader :ssh_config
7
+
8
+ def initialize(config:)
9
+ @ssh_config = config.raw_config.ssh || {}
10
+ validate! ssh_config
11
+ end
12
+
13
+ def user
14
+ ssh_config.fetch("user", "root")
15
+ end
16
+
17
+ def port
18
+ ssh_config.fetch("port", 22)
19
+ end
20
+
21
+ def proxy
22
+ if (proxy = ssh_config["proxy"])
23
+ Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
24
+ elsif (proxy_command = ssh_config["proxy_command"])
25
+ Net::SSH::Proxy::Command.new(proxy_command)
26
+ end
27
+ end
28
+
29
+ def keys_only
30
+ ssh_config["keys_only"]
31
+ end
32
+
33
+ def keys
34
+ ssh_config["keys"]
35
+ end
36
+
37
+ def key_data
38
+ ssh_config["key_data"]
39
+ end
40
+
41
+ def options
42
+ { user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data }.compact
43
+ end
44
+
45
+ def to_h
46
+ options.except(:logger).merge(log_level: log_level)
47
+ end
48
+
49
+ private
50
+ def logger
51
+ LOGGER.tap { |logger| logger.level = log_level }
52
+ end
53
+
54
+ def log_level
55
+ ssh_config.fetch("log_level", :fatal)
56
+ end
57
+ end
@@ -0,0 +1,22 @@
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 to_h
20
+ sshkit_config
21
+ end
22
+ 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