kamal 1.8.2 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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