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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +42 -16
  3. data/lib/kamal/cli/alias/command.rb +1 -0
  4. data/lib/kamal/cli/app/{prepare_assets.rb → assets.rb} +1 -1
  5. data/lib/kamal/cli/app/boot.rb +3 -2
  6. data/lib/kamal/cli/app/error_pages.rb +33 -0
  7. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  8. data/lib/kamal/cli/app.rb +94 -29
  9. data/lib/kamal/cli/base.rb +29 -4
  10. data/lib/kamal/cli/build.rb +60 -18
  11. data/lib/kamal/cli/main.rb +8 -10
  12. data/lib/kamal/cli/proxy.rb +58 -25
  13. data/lib/kamal/cli/registry.rb +2 -0
  14. data/lib/kamal/cli/secrets.rb +9 -3
  15. data/lib/kamal/cli/server.rb +4 -2
  16. data/lib/kamal/cli/templates/deploy.yml +6 -3
  17. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  18. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
  19. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  20. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
  21. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +1 -1
  22. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +19 -6
  23. data/lib/kamal/cli.rb +1 -0
  24. data/lib/kamal/commander/specifics.rb +9 -1
  25. data/lib/kamal/commander.rb +18 -27
  26. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  27. data/lib/kamal/commands/accessory.rb +9 -9
  28. data/lib/kamal/commands/app/assets.rb +4 -4
  29. data/lib/kamal/commands/app/containers.rb +2 -2
  30. data/lib/kamal/commands/app/error_pages.rb +9 -0
  31. data/lib/kamal/commands/app/execution.rb +6 -4
  32. data/lib/kamal/commands/app/images.rb +1 -1
  33. data/lib/kamal/commands/app/logging.rb +14 -4
  34. data/lib/kamal/commands/app/proxy.rb +17 -1
  35. data/lib/kamal/commands/app.rb +19 -10
  36. data/lib/kamal/commands/auditor.rb +11 -5
  37. data/lib/kamal/commands/base.rb +37 -1
  38. data/lib/kamal/commands/builder/base.rb +20 -7
  39. data/lib/kamal/commands/builder/cloud.rb +22 -0
  40. data/lib/kamal/commands/builder/pack.rb +46 -0
  41. data/lib/kamal/commands/builder.rb +11 -19
  42. data/lib/kamal/commands/proxy.rb +55 -15
  43. data/lib/kamal/commands/registry.rb +9 -7
  44. data/lib/kamal/configuration/accessory.rb +66 -11
  45. data/lib/kamal/configuration/builder.rb +20 -0
  46. data/lib/kamal/configuration/docs/accessory.yml +32 -4
  47. data/lib/kamal/configuration/docs/alias.yml +2 -2
  48. data/lib/kamal/configuration/docs/builder.yml +22 -0
  49. data/lib/kamal/configuration/docs/configuration.yml +6 -0
  50. data/lib/kamal/configuration/docs/env.yml +31 -0
  51. data/lib/kamal/configuration/docs/proxy.yml +78 -15
  52. data/lib/kamal/configuration/docs/registry.yml +4 -0
  53. data/lib/kamal/configuration/env.rb +13 -4
  54. data/lib/kamal/configuration/proxy/boot.rb +129 -0
  55. data/lib/kamal/configuration/proxy.rb +67 -5
  56. data/lib/kamal/configuration/registry.rb +6 -6
  57. data/lib/kamal/configuration/role.rb +11 -9
  58. data/lib/kamal/configuration/servers.rb +8 -1
  59. data/lib/kamal/configuration/validator/accessory.rb +6 -2
  60. data/lib/kamal/configuration/validator/builder.rb +2 -0
  61. data/lib/kamal/configuration/validator/proxy.rb +10 -0
  62. data/lib/kamal/configuration/validator/role.rb +3 -1
  63. data/lib/kamal/configuration/validator/servers.rb +1 -1
  64. data/lib/kamal/configuration/validator.rb +21 -1
  65. data/lib/kamal/configuration.rb +36 -57
  66. data/lib/kamal/docker.rb +30 -0
  67. data/lib/kamal/git.rb +10 -0
  68. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
  69. data/lib/kamal/secrets/adapters/base.rb +13 -3
  70. data/lib/kamal/secrets/adapters/bitwarden.rb +2 -2
  71. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  72. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  73. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  74. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  75. data/lib/kamal/secrets/adapters/last_pass.rb +3 -2
  76. data/lib/kamal/secrets/adapters/one_password.rb +47 -13
  77. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  78. data/lib/kamal/secrets/adapters/test.rb +2 -2
  79. data/lib/kamal/secrets/adapters.rb +2 -0
  80. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +2 -1
  81. data/lib/kamal/secrets.rb +1 -1
  82. data/lib/kamal/version.rb +1 -1
  83. 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: proxy_config["ssl"].presence,
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(other.proxy_config)
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
- attr_reader :registry_config, :secrets
5
-
6
- def initialize(config:)
7
- @registry_config = config.raw_config.registry || {}
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
- specializations,
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.merge(specialized_proxy) if running_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
- if config.raw_config.servers.is_a?(Array) || config.raw_config.servers[name].is_a?(Array)
208
- {}
209
- else
210
- config.raw_config.servers[name]
211
- end
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
- servers_config.is_a?(Array) ? [ "web" ] : servers_config.keys.sort
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 `roles`"
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! "servers", config
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
@@ -1,6 +1,6 @@
1
1
  class Kamal::Configuration::Validator::Servers < Kamal::Configuration::Validator
2
2
  def validate!
3
- validate_type! config, Array, Hash
3
+ validate_type! config, Array, Hash, NilClass
4
4
 
5
5
  validate_servers! config if config.is_a?(Array)
6
6
  end
@@ -24,7 +24,9 @@ class Kamal::Configuration::Validator
24
24
  example_value = example[key]
25
25
 
26
26
  if example_value == "..."
27
- unless key.to_s == "proxy" && boolean?(value.class)
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
@@ -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(IO.read(file)).result).symbolize_keys
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: self)
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, [ service, destination ].compact.join("-")
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 servers ].each do |key|
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
- unless role(primary_role_name).present?
315
- raise Kamal::ConfigurationError, "The primary_role #{primary_role_name} isn't defined"
316
- end
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
- if primary_role.hosts.empty?
319
- raise Kamal::ConfigurationError, "No servers specified for the #{primary_role.name} primary_role"
320
- end
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
- unless allow_empty_roles?
323
- roles.each do |role|
324
- if role.hosts.empty?
325
- raise Kamal::ConfigurationError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
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
@@ -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:, from: nil)
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
- full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
8
- fetch_secrets(full_secrets, account: account, session: session)
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)