kamal 1.8.2 → 2.0.0.beta1

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/kamal/cli/accessory.rb +44 -20
  4. data/lib/kamal/cli/alias/command.rb +9 -0
  5. data/lib/kamal/cli/app/boot.rb +22 -16
  6. data/lib/kamal/cli/app.rb +40 -5
  7. data/lib/kamal/cli/base.rb +19 -51
  8. data/lib/kamal/cli/build.rb +12 -13
  9. data/lib/kamal/cli/healthcheck/barrier.rb +2 -0
  10. data/lib/kamal/cli/healthcheck/poller.rb +18 -39
  11. data/lib/kamal/cli/lock.rb +2 -3
  12. data/lib/kamal/cli/main.rb +54 -51
  13. data/lib/kamal/cli/proxy.rb +224 -0
  14. data/lib/kamal/cli/prune.rb +0 -1
  15. data/lib/kamal/cli/secrets.rb +36 -0
  16. data/lib/kamal/cli/server.rb +2 -3
  17. data/lib/kamal/cli/templates/deploy.yml +4 -21
  18. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  19. data/lib/kamal/cli/templates/secrets +16 -0
  20. data/lib/kamal/cli.rb +1 -0
  21. data/lib/kamal/commander/specifics.rb +3 -3
  22. data/lib/kamal/commander.rb +24 -8
  23. data/lib/kamal/commands/accessory.rb +7 -7
  24. data/lib/kamal/commands/app/assets.rb +8 -8
  25. data/lib/kamal/commands/app/proxy.rb +16 -0
  26. data/lib/kamal/commands/app.rb +7 -15
  27. data/lib/kamal/commands/auditor.rb +6 -3
  28. data/lib/kamal/commands/base.rb +8 -0
  29. data/lib/kamal/commands/builder/base.rb +26 -13
  30. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  31. data/lib/kamal/commands/builder/local.rb +14 -0
  32. data/lib/kamal/commands/builder/remote.rb +63 -0
  33. data/lib/kamal/commands/builder.rb +13 -29
  34. data/lib/kamal/commands/docker.rb +4 -0
  35. data/lib/kamal/commands/hook.rb +5 -2
  36. data/lib/kamal/commands/lock.rb +2 -6
  37. data/lib/kamal/commands/proxy.rb +77 -0
  38. data/lib/kamal/commands/prune.rb +1 -9
  39. data/lib/kamal/commands/server.rb +11 -1
  40. data/lib/kamal/configuration/accessory.rb +14 -2
  41. data/lib/kamal/configuration/alias.rb +15 -0
  42. data/lib/kamal/configuration/builder.rb +52 -18
  43. data/lib/kamal/configuration/docs/alias.yml +26 -0
  44. data/lib/kamal/configuration/docs/builder.yml +26 -23
  45. data/lib/kamal/configuration/docs/configuration.yml +22 -16
  46. data/lib/kamal/configuration/docs/env.yml +10 -11
  47. data/lib/kamal/configuration/docs/proxy.yml +100 -0
  48. data/lib/kamal/configuration/docs/registry.yml +4 -2
  49. data/lib/kamal/configuration/docs/role.yml +3 -5
  50. data/lib/kamal/configuration/env/tag.rb +4 -3
  51. data/lib/kamal/configuration/env.rb +10 -17
  52. data/lib/kamal/configuration/proxy.rb +66 -0
  53. data/lib/kamal/configuration/registry.rb +3 -2
  54. data/lib/kamal/configuration/role.rb +63 -94
  55. data/lib/kamal/configuration/validator/alias.rb +15 -0
  56. data/lib/kamal/configuration/validator/builder.rb +4 -0
  57. data/lib/kamal/configuration/validator/proxy.rb +11 -0
  58. data/lib/kamal/configuration/validator.rb +42 -24
  59. data/lib/kamal/configuration.rb +91 -33
  60. data/lib/kamal/env_file.rb +4 -0
  61. data/lib/kamal/secrets/adapters/base.rb +18 -0
  62. data/lib/kamal/secrets/adapters/bitwarden.rb +64 -0
  63. data/lib/kamal/secrets/adapters/last_pass.rb +30 -0
  64. data/lib/kamal/secrets/adapters/one_password.rb +61 -0
  65. data/lib/kamal/secrets/adapters/test.rb +10 -0
  66. data/lib/kamal/secrets/adapters.rb +14 -0
  67. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +32 -0
  68. data/lib/kamal/secrets.rb +37 -0
  69. data/lib/kamal/sshkit_with_ext.rb +1 -0
  70. data/lib/kamal/utils.rb +28 -0
  71. data/lib/kamal/version.rb +1 -1
  72. data/lib/kamal.rb +3 -1
  73. metadata +32 -23
  74. data/lib/kamal/cli/env.rb +0 -54
  75. data/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +0 -3
  76. data/lib/kamal/cli/templates/template.env +0 -2
  77. data/lib/kamal/cli/traefik.rb +0 -122
  78. data/lib/kamal/commands/app/cord.rb +0 -22
  79. data/lib/kamal/commands/builder/multiarch/remote.rb +0 -61
  80. data/lib/kamal/commands/builder/multiarch.rb +0 -41
  81. data/lib/kamal/commands/builder/native/cached.rb +0 -25
  82. data/lib/kamal/commands/builder/native/remote.rb +0 -67
  83. data/lib/kamal/commands/builder/native.rb +0 -20
  84. data/lib/kamal/commands/traefik.rb +0 -85
  85. data/lib/kamal/configuration/docs/healthcheck.yml +0 -59
  86. data/lib/kamal/configuration/docs/traefik.yml +0 -62
  87. data/lib/kamal/configuration/healthcheck.rb +0 -63
  88. data/lib/kamal/configuration/traefik.rb +0 -60
  89. /data/lib/kamal/cli/templates/sample_hooks/{pre-traefik-reboot.sample → pre-proxy-reboot.sample} +0 -0
@@ -1,10 +1,9 @@
1
1
  class Kamal::Configuration::Role
2
2
  include Kamal::Configuration::Validation
3
3
 
4
- CORD_FILE = "cord"
5
4
  delegate :argumentize, :optionize, to: Kamal::Utils
6
5
 
7
- attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_healthcheck
6
+ attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_proxy
8
7
 
9
8
  alias to_s name
10
9
 
@@ -18,16 +17,14 @@ class Kamal::Configuration::Role
18
17
 
19
18
  @specialized_env = Kamal::Configuration::Env.new \
20
19
  config: specializations.fetch("env", {}),
21
- secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env"),
20
+ secrets: config.secrets,
22
21
  context: "servers/#{name}/env"
23
22
 
24
23
  @specialized_logging = Kamal::Configuration::Logging.new \
25
24
  logging_config: specializations.fetch("logging", {}),
26
25
  context: "servers/#{name}/logging"
27
26
 
28
- @specialized_healthcheck = Kamal::Configuration::Healthcheck.new \
29
- healthcheck_config: specializations.fetch("healthcheck", {}),
30
- context: "servers/#{name}/healthcheck"
27
+ initialize_specialized_proxy
31
28
  end
32
29
 
33
30
  def primary_host
@@ -55,7 +52,7 @@ class Kamal::Configuration::Role
55
52
  end
56
53
 
57
54
  def labels
58
- default_labels.merge(traefik_labels).merge(custom_labels)
55
+ default_labels.merge(custom_labels)
59
56
  end
60
57
 
61
58
  def label_args
@@ -70,87 +67,53 @@ class Kamal::Configuration::Role
70
67
  @logging ||= config.logging.merge(specialized_logging)
71
68
  end
72
69
 
73
-
74
- def env(host)
75
- @envs ||= {}
76
- @envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
70
+ def proxy
71
+ @proxy ||= config.proxy.merge(specialized_proxy) if running_proxy?
77
72
  end
78
73
 
79
- def env_args(host)
80
- env(host).args
74
+ def running_proxy?
75
+ @running_proxy
81
76
  end
82
77
 
83
- def asset_volume_args
84
- asset_volume&.docker_args
78
+ def ssl?
79
+ running_proxy? && proxy.ssl?
85
80
  end
86
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
87
85
 
88
- def health_check_args(cord: true)
89
- if running_traefik? || healthcheck.set_port_or_path?
90
- if cord && uses_cord?
91
- optionize({ "health-cmd" => health_check_cmd_with_cord, "health-interval" => healthcheck.interval })
92
- .concat(cord_volume.docker_args)
93
- else
94
- optionize({ "health-cmd" => healthcheck.cmd, "health-interval" => healthcheck.interval })
95
- end
96
- else
97
- []
98
- end
86
+ [ *argumentize("-t", timeout) ]
99
87
  end
100
88
 
101
- def healthcheck
102
- @healthcheck ||=
103
- if running_traefik?
104
- config.healthcheck.merge(specialized_healthcheck)
105
- else
106
- specialized_healthcheck
107
- end
108
- end
109
-
110
- def health_check_cmd_with_cord
111
- "(#{healthcheck.cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)"
112
- end
113
-
114
-
115
- def running_traefik?
116
- if specializations["traefik"].nil?
117
- primary?
118
- else
119
- specializations["traefik"]
120
- end
89
+ def env(host)
90
+ @envs ||= {}
91
+ @envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
121
92
  end
122
93
 
123
- def primary?
124
- self == @config.primary_role
94
+ def env_args(host)
95
+ [ *env(host).clear_args, *argumentize("--env-file", secrets_path) ]
125
96
  end
126
97
 
127
-
128
- def uses_cord?
129
- running_traefik? && cord_volume && healthcheck.cmd.present?
98
+ def env_directory
99
+ File.join(config.env_directory, "roles")
130
100
  end
131
101
 
132
- def cord_host_directory
133
- File.join config.run_directory_as_docker_volume, "cords", [ container_prefix, config.run_id ].join("-")
102
+ def secrets_io(host)
103
+ env(host).secrets_io
134
104
  end
135
105
 
136
- def cord_volume
137
- if (cord = healthcheck.cord)
138
- @cord_volume ||= Kamal::Configuration::Volume.new \
139
- host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")),
140
- container_path: cord
141
- end
106
+ def secrets_path
107
+ File.join(config.env_directory, "roles", "#{name}.env")
142
108
  end
143
109
 
144
- def cord_host_file
145
- File.join cord_volume.host_path, CORD_FILE
110
+ def asset_volume_args
111
+ asset_volume&.docker_args
146
112
  end
147
113
 
148
- def cord_container_directory
149
- health_check_options.fetch("cord", nil)
150
- end
151
114
 
152
- def cord_container_file
153
- File.join cord_volume.container_path, CORD_FILE
115
+ def primary?
116
+ name == @config.primary_role_name
154
117
  end
155
118
 
156
119
 
@@ -168,25 +131,52 @@ class Kamal::Configuration::Role
168
131
  end
169
132
 
170
133
  def assets?
171
- asset_path.present? && running_traefik?
134
+ asset_path.present? && running_proxy?
172
135
  end
173
136
 
174
- def asset_volume(version = nil)
137
+ def asset_volume(version = config.version)
175
138
  if assets?
176
139
  Kamal::Configuration::Volume.new \
177
- host_path: asset_volume_path(version), container_path: asset_path
140
+ host_path: asset_volume_directory(version), container_path: asset_path
178
141
  end
179
142
  end
180
143
 
181
- def asset_extracted_path(version = nil)
182
- File.join config.run_directory, "assets", "extracted", container_name(version)
144
+ def asset_extracted_directory(version = config.version)
145
+ File.join config.assets_directory, "extracted", [ name, version ].join("-")
183
146
  end
184
147
 
185
- def asset_volume_path(version = nil)
186
- File.join config.run_directory, "assets", "volumes", container_name(version)
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
154
+ raise Kamal::ConfigurationError, "SSL is only supported on a single server, found #{hosts.size} servers for role #{name}"
155
+ end
187
156
  end
188
157
 
189
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
+ context: "servers/#{name}/proxy"
177
+ end
178
+ end
179
+
190
180
  def tagged_hosts
191
181
  {}.tap do |tagged_hosts|
192
182
  extract_hosts_from_config.map do |host_config|
@@ -221,27 +211,6 @@ class Kamal::Configuration::Role
221
211
  end
222
212
  end
223
213
 
224
- def traefik_labels
225
- if running_traefik?
226
- {
227
- # Setting a service property ensures that the generated service name will be consistent between versions
228
- "traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
229
-
230
- "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
231
- "traefik.http.routers.#{traefik_service}.priority" => "2",
232
- "traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
233
- "traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
234
- "traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
235
- }
236
- else
237
- {}
238
- end
239
- end
240
-
241
- def traefik_service
242
- container_prefix
243
- end
244
-
245
214
  def custom_labels
246
215
  Hash.new.tap do |labels|
247
216
  labels.merge!(config.labels) if config.labels.present?
@@ -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
@@ -5,5 +5,9 @@ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator
5
5
  if config["cache"] && config["cache"]["type"]
6
6
  error "Invalid cache type: #{config["cache"]["type"]}" unless [ "gha", "registry" ].include?(config["cache"]["type"])
7
7
  end
8
+
9
+ error "Builder arch not set" unless config["arch"].present?
10
+
11
+ error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank?
8
12
  end
9
13
  end
@@ -0,0 +1,11 @@
1
+ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
2
+ def validate!
3
+ unless config.nil?
4
+ super
5
+
6
+ if config["host"].blank? && config["ssl"]
7
+ error "Must set a host to enable automatic SSL"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -13,32 +13,40 @@ class Kamal::Configuration::Validator
13
13
 
14
14
  private
15
15
  def validate_against_example!(validation_config, example)
16
- validate_type! validation_config, Hash
17
-
18
- check_unknown_keys! validation_config, example
19
-
20
- validation_config.each do |key, value|
21
- next if extension?(key)
22
- with_context(key) do
23
- example_value = example[key]
24
-
25
- if example_value == "..."
26
- validate_type! value, *(Array if key == :servers), Hash
27
- elsif key == "hosts"
28
- validate_servers! value
29
- elsif example_value.is_a?(Array)
30
- validate_array_of! value, example_value.first.class
31
- elsif example_value.is_a?(Hash)
32
- case key.to_s
33
- when "options", "args"
34
- validate_type! value, Hash
35
- when "labels"
36
- validate_hash_of! value, example_value.first[1].class
16
+ validate_type! validation_config, example.class
17
+
18
+ if example.class == Hash
19
+ check_unknown_keys! validation_config, example
20
+
21
+ validation_config.each do |key, value|
22
+ next if extension?(key)
23
+ with_context(key) do
24
+ example_value = example[key]
25
+
26
+ if example_value == "..."
27
+ unless key.to_s == "proxy" && boolean?(value.class)
28
+ validate_type! value, *(Array if key == :servers), Hash
29
+ end
30
+ elsif key == "hosts"
31
+ validate_servers! value
32
+ elsif example_value.is_a?(Array)
33
+ if key == "arch"
34
+ validate_array_of_or_type! value, example_value.first.class
35
+ else
36
+ validate_array_of! value, example_value.first.class
37
+ end
38
+ elsif example_value.is_a?(Hash)
39
+ case key.to_s
40
+ when "options", "args"
41
+ validate_type! value, Hash
42
+ when "labels"
43
+ validate_hash_of! value, example_value.first[1].class
44
+ else
45
+ validate_against_example! value, example_value
46
+ end
37
47
  else
38
- validate_against_example! value, example_value
48
+ validate_type! value, example_value.class
39
49
  end
40
- else
41
- validate_type! value, example_value.class
42
50
  end
43
51
  end
44
52
  end
@@ -69,6 +77,16 @@ class Kamal::Configuration::Validator
69
77
  value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
70
78
  end
71
79
 
80
+ def validate_array_of_or_type!(value, type)
81
+ if value.is_a?(Array)
82
+ validate_array_of! value, type
83
+ else
84
+ validate_type! value, type
85
+ end
86
+ rescue Kamal::ConfigurationError
87
+ type_error(Array, type)
88
+ end
89
+
72
90
  def validate_array_of!(array, type)
73
91
  validate_type! array, Array
74
92
 
@@ -2,19 +2,22 @@ require "active_support/ordered_options"
2
2
  require "active_support/core_ext/string/inquiry"
3
3
  require "active_support/core_ext/module/delegation"
4
4
  require "active_support/core_ext/hash/keys"
5
- require "pathname"
6
5
  require "erb"
7
6
  require "net/ssh/proxy/jump"
8
7
 
9
8
  class Kamal::Configuration
10
- delegate :service, :image, :labels, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
9
+ delegate :service, :image, :labels, :hooks_path, to: :raw_config, allow_nil: true
11
10
  delegate :argumentize, :optionize, to: Kamal::Utils
12
11
 
13
- attr_reader :destination, :raw_config
14
- attr_reader :accessories, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry
12
+ attr_reader :destination, :raw_config, :secrets
13
+ attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :servers, :ssh, :sshkit, :registry
15
14
 
16
15
  include Validation
17
16
 
17
+ PROXY_MINIMUM_VERSION = "v0.3.0"
18
+ PROXY_HTTP_PORT = 80
19
+ PROXY_HTTPS_PORT = 443
20
+
18
21
  class << self
19
22
  def create_from(config_file:, destination: nil, version: nil)
20
23
  raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
@@ -49,18 +52,20 @@ class Kamal::Configuration
49
52
 
50
53
  validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
51
54
 
55
+ @secrets = Kamal::Secrets.new(destination: destination)
56
+
52
57
  # Eager load config to validate it, these are first as they have dependencies later on
53
58
  @servers = Servers.new(config: self)
54
59
  @registry = Registry.new(config: self)
55
60
 
56
61
  @accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
62
+ @aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
57
63
  @boot = Boot.new(config: self)
58
64
  @builder = Builder.new(config: self)
59
- @env = Env.new(config: @raw_config.env || {})
65
+ @env = Env.new(config: @raw_config.env || {}, secrets: secrets)
60
66
 
61
- @healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
62
67
  @logging = Logging.new(logging_config: @raw_config.logging)
63
- @traefik = Traefik.new(config: self)
68
+ @proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy || {})
64
69
  @ssh = Ssh.new(config: self)
65
70
  @sshkit = Sshkit.new(config: self)
66
71
 
@@ -69,6 +74,9 @@ class Kamal::Configuration
69
74
  ensure_valid_kamal_version
70
75
  ensure_retain_containers_valid
71
76
  ensure_valid_service_name
77
+ ensure_no_traefik_reboot_hooks
78
+ ensure_one_host_for_ssl_roles
79
+ ensure_unique_hosts_for_ssl_roles
72
80
  end
73
81
 
74
82
 
@@ -129,16 +137,16 @@ class Kamal::Configuration
129
137
  raw_config.allow_empty_roles
130
138
  end
131
139
 
132
- def traefik_roles
133
- roles.select(&:running_traefik?)
140
+ def proxy_roles
141
+ roles.select(&:running_proxy?)
134
142
  end
135
143
 
136
- def traefik_role_names
137
- traefik_roles.flat_map(&:name)
144
+ def proxy_role_names
145
+ proxy_roles.flat_map(&:name)
138
146
  end
139
147
 
140
- def traefik_hosts
141
- traefik_roles.flat_map(&:hosts).uniq
148
+ def proxy_hosts
149
+ proxy_roles.flat_map(&:hosts).uniq
142
150
  end
143
151
 
144
152
  def repository
@@ -183,31 +191,44 @@ class Kamal::Configuration
183
191
  end
184
192
 
185
193
 
186
- def healthcheck_service
187
- [ "healthcheck", service, destination ].compact.join("-")
188
- end
189
-
190
194
  def readiness_delay
191
195
  raw_config.readiness_delay || 7
192
196
  end
193
197
 
194
- def run_id
195
- @run_id ||= SecureRandom.hex(16)
198
+ def deploy_timeout
199
+ raw_config.deploy_timeout || 30
200
+ end
201
+
202
+ def drain_timeout
203
+ raw_config.drain_timeout || 30
196
204
  end
197
205
 
198
206
 
199
207
  def run_directory
200
- raw_config.run_directory || ".kamal"
208
+ ".kamal"
201
209
  end
202
210
 
203
- def run_directory_as_docker_volume
204
- if Pathname.new(run_directory).absolute?
205
- run_directory
206
- else
207
- File.join "$(pwd)", run_directory
208
- end
211
+ def apps_directory
212
+ File.join run_directory, "apps"
209
213
  end
210
214
 
215
+ def app_directory
216
+ File.join apps_directory, [ service, destination ].compact.join("-")
217
+ end
218
+
219
+ def proxy_directory
220
+ File.join run_directory, "proxy"
221
+ end
222
+
223
+ def env_directory
224
+ File.join app_directory, "env"
225
+ end
226
+
227
+ def assets_directory
228
+ File.join app_directory, "assets"
229
+ end
230
+
231
+
211
232
  def hooks_path
212
233
  raw_config.hooks_path || ".kamal/hooks"
213
234
  end
@@ -217,13 +238,9 @@ class Kamal::Configuration
217
238
  end
218
239
 
219
240
 
220
- def host_env_directory
221
- File.join(run_directory, "env")
222
- end
223
-
224
241
  def env_tags
225
242
  @env_tags ||= if (tags = raw_config.env["tags"])
226
- tags.collect { |name, config| Env::Tag.new(name, config: config) }
243
+ tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
227
244
  else
228
245
  []
229
246
  end
@@ -233,6 +250,24 @@ class Kamal::Configuration
233
250
  env_tags.detect { |t| t.name == name.to_s }
234
251
  end
235
252
 
253
+ def proxy_publish_args
254
+ argumentize "--publish", [ "#{PROXY_HTTP_PORT}:#{PROXY_HTTP_PORT}", "#{PROXY_HTTPS_PORT}:#{PROXY_HTTPS_PORT}" ]
255
+ end
256
+
257
+ def proxy_image
258
+ "basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}"
259
+ end
260
+
261
+ def proxy_container_name
262
+ "kamal-proxy"
263
+ end
264
+
265
+ def proxy_config_volume
266
+ Kamal::Configuration::Volume.new \
267
+ host_path: File.join(proxy_directory, "config"),
268
+ container_path: "/home/kamal-proxy/.config/kamal-proxy"
269
+ end
270
+
236
271
 
237
272
  def to_h
238
273
  {
@@ -248,8 +283,7 @@ class Kamal::Configuration
248
283
  sshkit: sshkit.to_h,
249
284
  builder: builder.to_h,
250
285
  accessories: raw_config.accessories,
251
- logging: logging_args,
252
- healthcheck: healthcheck.to_h
286
+ logging: logging_args
253
287
  }.compact
254
288
  end
255
289
 
@@ -307,6 +341,30 @@ class Kamal::Configuration
307
341
  true
308
342
  end
309
343
 
344
+ def ensure_no_traefik_reboot_hooks
345
+ hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) }
346
+
347
+ if hooks.any?
348
+ raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot"
349
+ end
350
+
351
+ true
352
+ end
353
+
354
+ def ensure_one_host_for_ssl_roles
355
+ roles.each(&:ensure_one_host_for_ssl)
356
+
357
+ true
358
+ end
359
+
360
+ def ensure_unique_hosts_for_ssl_roles
361
+ hosts = roles.select(&:ssl?).map { |role| role.proxy.host }
362
+ duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
363
+
364
+ raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
365
+
366
+ true
367
+ end
310
368
 
311
369
  def role_names
312
370
  raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
@@ -15,6 +15,10 @@ class Kamal::EnvFile
15
15
  env_file.presence || "\n"
16
16
  end
17
17
 
18
+ def to_io
19
+ StringIO.new(to_s)
20
+ end
21
+
18
22
  alias to_str to_s
19
23
 
20
24
  private
@@ -0,0 +1,18 @@
1
+ class Kamal::Secrets::Adapters::Base
2
+ delegate :optionize, to: Kamal::Utils
3
+
4
+ def fetch(secrets, account:, from: nil)
5
+ session = login(account)
6
+ full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
7
+ fetch_secrets(full_secrets, account: account, session: session)
8
+ end
9
+
10
+ private
11
+ def login(...)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def fetch_secrets(...)
16
+ raise NotImplementedError
17
+ end
18
+ end
@@ -0,0 +1,64 @@
1
+ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
2
+ private
3
+ def login(account)
4
+ status = run_command("status")
5
+
6
+ if status["status"] == "unauthenticated"
7
+ run_command("login #{account.shellescape}", raw: true)
8
+ status = run_command("status")
9
+ end
10
+
11
+ if status["status"] == "locked"
12
+ session = run_command("unlock --raw", raw: true).presence
13
+ status = run_command("status", session: session)
14
+ end
15
+
16
+ raise RuntimeError, "Failed to login to and unlock Bitwarden" unless status["status"] == "unlocked"
17
+
18
+ run_command("sync", session: session, raw: true)
19
+ raise RuntimeError, "Failed to sync Bitwarden" unless $?.success?
20
+
21
+ session
22
+ end
23
+
24
+ def fetch_secrets(secrets, account:, session:)
25
+ {}.tap do |results|
26
+ items_fields(secrets).each do |item, fields|
27
+ item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
28
+ raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success?
29
+ item_json = JSON.parse(item_json)
30
+
31
+ if fields.any?
32
+ fields.each do |field|
33
+ item_field = item_json["fields"].find { |f| f["name"] == field }
34
+ raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
35
+ value = item_field["value"]
36
+ results["#{item}/#{field}"] = value
37
+ end
38
+ else
39
+ results[item] = item_json["login"]["password"]
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def items_fields(secrets)
46
+ {}.tap do |items|
47
+ secrets.each do |secret|
48
+ item, field = secret.split("/")
49
+ items[item] ||= []
50
+ items[item] << field
51
+ end
52
+ end
53
+ end
54
+
55
+ def signedin?(account)
56
+ run_command("status")["status"] != "unauthenticated"
57
+ end
58
+
59
+ def run_command(command, session: nil, raw: false)
60
+ full_command = [ *("BW_SESSION=#{session.shellescape}" if session), "bw", command ].join(" ")
61
+ result = `#{full_command}`.strip
62
+ raw ? result : JSON.parse(result)
63
+ end
64
+ end