kamal 2.9.0 → 2.10.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.
@@ -74,25 +74,31 @@ class Kamal::Configuration::Accessory
74
74
  end
75
75
 
76
76
  def files
77
- accessory_config["files"]&.to_h do |local_to_remote_mapping|
78
- local_file, remote_file = local_to_remote_mapping.split(":")
79
- [ expand_local_file(local_file), expand_remote_file(remote_file) ]
77
+ accessory_config["files"]&.to_h do |config|
78
+ parse_path_config(config, default_mode: "755") do |local, remote|
79
+ {
80
+ key: expand_local_file(local),
81
+ host_path: expand_remote_file(remote),
82
+ container_path: remote
83
+ }
84
+ end
80
85
  end || {}
81
86
  end
82
87
 
83
88
  def directories
84
- accessory_config["directories"]&.to_h do |host_to_container_mapping|
85
- host_path, container_path = host_to_container_mapping.split(":")
86
- [ expand_host_path(host_path), container_path ]
89
+ accessory_config["directories"]&.to_h do |config|
90
+ parse_path_config(config, default_mode: nil) do |local, remote|
91
+ {
92
+ key: expand_host_path(local),
93
+ host_path: expand_host_path_for_volume(local),
94
+ container_path: remote
95
+ }
96
+ end
87
97
  end || {}
88
98
  end
89
99
 
90
- def volumes
91
- specific_volumes + remote_files_as_volumes + remote_directories_as_volumes
92
- end
93
-
94
100
  def volume_args
95
- argumentize "--volume", volumes
101
+ (specific_volumes + path_volumes(files) + path_volumes(directories)).flat_map(&:docker_args)
96
102
  end
97
103
 
98
104
  def option_args
@@ -142,17 +148,17 @@ class Kamal::Configuration::Accessory
142
148
 
143
149
  def expand_local_file(local_file)
144
150
  if local_file.end_with?("erb")
145
- with_clear_env_loaded { read_dynamic_file(local_file) }
151
+ with_env_loaded { read_dynamic_file(local_file) }
146
152
  else
147
153
  Pathname.new(File.expand_path(local_file)).to_s
148
154
  end
149
155
  end
150
156
 
151
- def with_clear_env_loaded
152
- env.clear.each { |k, v| ENV[k] = v }
157
+ def with_env_loaded
158
+ env.to_h.each { |k, v| ENV[k] = v }
153
159
  yield
154
160
  ensure
155
- env.clear.each { |k, v| ENV.delete(k) }
161
+ env.to_h.each { |k, v| ENV.delete(k) }
156
162
  end
157
163
 
158
164
  def read_dynamic_file(local_file)
@@ -164,27 +170,58 @@ class Kamal::Configuration::Accessory
164
170
  end
165
171
 
166
172
  def specific_volumes
167
- accessory_config["volumes"] || []
173
+ (accessory_config["volumes"] || []).collect do |volume_string|
174
+ host_path, container_path, options = volume_string.split(":", 3)
175
+ Kamal::Configuration::Volume.new \
176
+ host_path: host_path,
177
+ container_path: container_path,
178
+ options: options
179
+ end
168
180
  end
169
181
 
170
- def remote_files_as_volumes
171
- accessory_config["files"]&.collect do |local_to_remote_mapping|
172
- _, remote_file = local_to_remote_mapping.split(":")
173
- "#{service_data_directory + remote_file}:#{remote_file}"
174
- end || []
182
+ def path_volumes(paths)
183
+ paths.map do |local, config|
184
+ Kamal::Configuration::Volume.new \
185
+ host_path: config[:host_path],
186
+ container_path: config[:container_path],
187
+ options: config[:options]
188
+ end
175
189
  end
176
190
 
177
- def remote_directories_as_volumes
178
- accessory_config["directories"]&.collect do |host_to_container_mapping|
179
- host_path, container_path = host_to_container_mapping.split(":")
180
- [ expand_host_path(host_path), container_path ].join(":")
181
- end || []
191
+ def parse_path_config(config, default_mode:)
192
+ if config.is_a?(Hash)
193
+ local, remote = config["local"], config["remote"]
194
+ expanded = yield(local, remote)
195
+ [
196
+ expanded[:key],
197
+ expanded.except(:key).merge(
198
+ options: config["options"],
199
+ mode: config["mode"] || default_mode,
200
+ owner: config["owner"]
201
+ )
202
+ ]
203
+ else
204
+ local, remote, options = config.split(":", 3)
205
+ expanded = yield(local, remote)
206
+ [
207
+ expanded[:key],
208
+ expanded.except(:key).merge(
209
+ options: options,
210
+ mode: default_mode,
211
+ owner: nil
212
+ )
213
+ ]
214
+ end
182
215
  end
183
216
 
184
217
  def expand_host_path(host_path)
185
218
  absolute_path?(host_path) ? host_path : File.join(service_data_directory, host_path)
186
219
  end
187
220
 
221
+ def expand_host_path_for_volume(host_path)
222
+ absolute_path?(host_path) ? host_path : File.join(service_name, host_path)
223
+ end
224
+
188
225
  def absolute_path?(path)
189
226
  Pathname.new(path).absolute?
190
227
  end
@@ -22,4 +22,8 @@ class Kamal::Configuration::Boot
22
22
  def wait
23
23
  boot_config["wait"]
24
24
  end
25
+
26
+ def parallel_roles
27
+ boot_config["parallel_roles"]
28
+ end
25
29
  end
@@ -90,22 +90,54 @@ accessories:
90
90
  # Copying files
91
91
  #
92
92
  # You can specify files to mount into the container.
93
- # The format is `local:remote`, where `local` is the path to the file on the local machine
94
- # and `remote` is the path to the file in the container.
95
93
  #
96
94
  # They will be uploaded from the local repo to the host and then mounted.
97
- #
98
95
  # ERB files will be evaluated before being copied.
96
+ #
97
+ # You can use the string format: `local:remote` or `local:remote:options`
98
+ # where the options can be `ro` for read-only or `z`/`Z` for SELinux labels
99
99
  files:
100
100
  - config/my.cnf.erb:/etc/mysql/my.cnf
101
- - config/myoptions.cnf:/etc/mysql/myoptions.cnf
101
+ - config/myoptions.cnf:/etc/mysql/myoptions.cnf:ro
102
+ - config/certs:/etc/mysql/certs:ro,Z
103
+ #
104
+ # Or you can use the hash format for custom mode and ownership.
105
+ #
106
+ # Note: Setting `owner` requires root access:
107
+ files:
108
+ - local: config/secret.key
109
+ remote: /etc/mysql/secret.key
110
+ mode: "0600"
111
+ owner: "mysql:mysql"
112
+ - local: config/ca-cert.pem
113
+ remote: /etc/mysql/certs/ca-cert.pem
114
+ mode: "0644"
115
+ owner: "1000:1000"
116
+ options: "Z"
102
117
 
103
118
  # Directories
104
119
  #
105
120
  # You can specify directories to mount into the container. They will be created on the host
106
- # before being mounted:
121
+ # before being mounted.
122
+ #
123
+ # You can use the string format: `local:remote` or `local:remote:options`
124
+ # where the options can be `ro` for read-only or `z`/`Z` for SELinux labels
107
125
  directories:
108
126
  - mysql-logs:/var/log/mysql
127
+ - mysql-data:/var/lib/mysql:z
128
+ #
129
+ # Or you can use the hash format for custom mode and ownership.
130
+ #
131
+ # Note: Setting `owner` requires root access:
132
+ directories:
133
+ - local: mysql-data
134
+ remote: /var/lib/mysql
135
+ mode: "0750"
136
+ owner: "mysql:mysql"
137
+ - local: mysql-logs
138
+ remote: /var/log/mysql
139
+ mode: "0755"
140
+ options: "z"
109
141
 
110
142
  # Volumes
111
143
  #
@@ -4,16 +4,18 @@
4
4
  #
5
5
  # Kamal’s default is to boot new containers on all hosts in parallel. However, you can control this with the boot configuration.
6
6
 
7
- # Fixed group sizes
8
- #
9
- # Here, we boot 2 hosts at a time with a 10-second gap between each group:
10
7
  boot:
11
- limit: 2
12
- wait: 10
13
8
 
14
- # Percentage of hosts
15
- #
16
- # Here, we boot 25% of the hosts at a time with a 2-second gap between each group:
17
- boot:
9
+ # The number or percentage of hosts to boot at a time.
10
+ # This can be an integer (e.g., 3) or a percentage string (e.g., 25%).
18
11
  limit: 25%
19
- wait: 2
12
+
13
+ # The number of seconds to wait between booting each group of hosts.
14
+ wait: 10
15
+
16
+ # Whether to boot roles in parallel on a host.
17
+ #
18
+ # If a host has multiple roles, control whether they are booted in parallel or sequentially on that host.
19
+ #
20
+ # Defaults to false.
21
+ parallel_roles: true
@@ -73,7 +73,10 @@ env:
73
73
  # This requires that file names change when the contents change
74
74
  # (e.g., by including a hash of the contents in the name).
75
75
  #
76
- # To configure this, set the path to the assets:
76
+ # To configure this, set the path to the assets.
77
+ #
78
+ # You can also specify mount options after a colon, such as `ro` for read-only
79
+ # or `z`/`Z` for SELinux labels
77
80
  asset_path: /path/to/assets
78
81
 
79
82
  # Hooks path
@@ -148,6 +148,30 @@ proxy:
148
148
  - X-Request-ID
149
149
  - X-Request-Start
150
150
 
151
+ # Run configuration
152
+ #
153
+ # These options are used when booting the proxy container.
154
+ #
155
+ run:
156
+ http_port: 8080 # HTTP port to use (default 80)
157
+ https_port: 8443 # HTTPS port to use (default 443)
158
+ metrics_port: 9090 # Port for Prometheus metrics
159
+ debug: true # Debug logging (default: false)
160
+ log_max_size: "30m" # Maximum log file size (default: "10m")
161
+ publish: false # Publish ports to the host (default: true)
162
+ bind_ips: # List of IPs to bind to when publishing ports
163
+ - 0.0.0.0
164
+ registry: registry:4443 # Container registry for the kamal-proxy image
165
+ # (defaults to Docker Hub)
166
+ repository: myrepo/kamal-proxy # Container repository for the kamal-proxy image
167
+ # (defaults to `basecamp/kamal-proxy`)
168
+ version: v0.8.0 # Version tag of the kamal-proxy image to use
169
+ options: # Additional options to pass to `docker run`
170
+ label:
171
+ - custom.label=kamal-proxy
172
+ memory: 512m
173
+ cpus: 0.5
174
+
151
175
  # Enabling/disabling the proxy on roles
152
176
  #
153
177
  # The proxy is enabled by default on the primary role but can be disabled by
@@ -58,9 +58,12 @@ ssh:
58
58
 
59
59
  # Key data
60
60
  #
61
- # An array of strings, with each element of the array being
62
- # a raw private key in PEM format.
63
- key_data: [ "-----BEGIN OPENSSH PRIVATE KEY-----" ]
61
+ # An array of strings, with each element of the array being a secret name.
62
+ key_data:
63
+ - SSH_PRIVATE_KEY
64
+ # You can also provide raw private key in PEM format, but this is deprecated.
65
+ key_data:
66
+ - "-----BEGIN OPENSSH PRIVATE KEY----- ..."
64
67
 
65
68
  # Config
66
69
  #
@@ -1,7 +1,7 @@
1
1
  class Kamal::Configuration::Env
2
2
  include Kamal::Configuration::Validation
3
3
 
4
- attr_reader :context, :clear, :secret_keys
4
+ attr_reader :context, :clear, :secrets, :secret_keys
5
5
  delegate :argumentize, to: Kamal::Utils
6
6
 
7
7
  def initialize(config:, secrets:, context: "env")
@@ -23,12 +23,16 @@ class Kamal::Configuration::Env
23
23
  def merge(other)
24
24
  self.class.new \
25
25
  config: { "clear" => clear.merge(other.clear), "secret" => secret_keys | other.secret_keys },
26
- secrets: @secrets
26
+ secrets: secrets
27
+ end
28
+
29
+ def to_h
30
+ clear.merge(aliased_secrets)
27
31
  end
28
32
 
29
33
  private
30
34
  def aliased_secrets
31
- secret_keys.to_h { |key| extract_alias(key) }.transform_values { |secret_key| @secrets[secret_key] }
35
+ secret_keys.to_h { |key| extract_alias(key) }.transform_values { |secret_key| secrets[secret_key] }
32
36
  end
33
37
 
34
38
  def extract_alias(key)
@@ -1,9 +1,4 @@
1
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
2
  attr_reader :config
8
3
  delegate :argumentize, :optionize, to: Kamal::Utils
9
4
 
@@ -16,8 +11,8 @@ class Kamal::Configuration::Proxy::Boot
16
11
 
17
12
  (bind_ips || [ nil ]).map do |bind_ip|
18
13
  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(":")
14
+ publish_http = [ bind_ip, http_port, Kamal::Configuration::Proxy::Run::DEFAULT_HTTP_PORT ].compact.join(":")
15
+ publish_https = [ bind_ip, https_port, Kamal::Configuration::Proxy::Run::DEFAULT_HTTPS_PORT ].compact.join(":")
21
16
 
22
17
  argumentize "--publish", [ publish_http, publish_https ]
23
18
  end.join(" ")
@@ -29,8 +24,8 @@ class Kamal::Configuration::Proxy::Boot
29
24
 
30
25
  def default_boot_options
31
26
  [
32
- *(publish_args(DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, nil)),
33
- *(logging_args(DEFAULT_LOG_MAX_SIZE))
27
+ *(publish_args(Kamal::Configuration::Proxy::Run::DEFAULT_HTTP_PORT, Kamal::Configuration::Proxy::Run::DEFAULT_HTTPS_PORT, nil)),
28
+ *(logging_args(Kamal::Configuration::Proxy::Run::DEFAULT_LOG_MAX_SIZE))
34
29
  ]
35
30
  end
36
31
 
@@ -0,0 +1,143 @@
1
+ class Kamal::Configuration::Proxy::Run
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, :run_config
8
+ delegate :argumentize, :optionize, to: Kamal::Utils
9
+
10
+ def initialize(config, run_config:, context: "proxy/run")
11
+ @config = config
12
+ @run_config = run_config
13
+ @context = context
14
+ end
15
+
16
+ def debug?
17
+ run_config.fetch("debug", nil)
18
+ end
19
+
20
+ def publish?
21
+ run_config.fetch("publish", true)
22
+ end
23
+
24
+ def http_port
25
+ run_config.fetch("http_port", DEFAULT_HTTP_PORT)
26
+ end
27
+
28
+ def https_port
29
+ run_config.fetch("https_port", DEFAULT_HTTPS_PORT)
30
+ end
31
+
32
+ def bind_ips
33
+ run_config.fetch("bind_ips", nil)
34
+ end
35
+
36
+ def publish_args
37
+ if publish?
38
+ (bind_ips || [ nil ]).map do |bind_ip|
39
+ bind_ip = format_bind_ip(bind_ip)
40
+ publish_http = [ bind_ip, http_port, DEFAULT_HTTP_PORT ].compact.join(":")
41
+ publish_https = [ bind_ip, https_port, DEFAULT_HTTPS_PORT ].compact.join(":")
42
+
43
+ argumentize "--publish", [ publish_http, publish_https ]
44
+ end.join(" ")
45
+ end
46
+ end
47
+
48
+ def log_max_size
49
+ run_config.fetch("log_max_size", DEFAULT_LOG_MAX_SIZE)
50
+ end
51
+
52
+ def logging_args
53
+ argumentize "--log-opt", "max-size=#{log_max_size}" if log_max_size.present?
54
+ end
55
+
56
+ def version
57
+ run_config.fetch("version", MINIMUM_VERSION)
58
+ end
59
+
60
+ def registry
61
+ run_config.fetch("registry", nil)
62
+ end
63
+
64
+ def repository
65
+ run_config.fetch("repository", "basecamp/kamal-proxy")
66
+ end
67
+
68
+ def image
69
+ "#{[ registry, repository ].compact.join("/")}:#{version}"
70
+ end
71
+
72
+ def container_name
73
+ "kamal-proxy"
74
+ end
75
+
76
+ def options_args
77
+ if args = run_config["options"]
78
+ optionize args
79
+ end
80
+ end
81
+
82
+ def run_command
83
+ [ "kamal-proxy", "run", *optionize(run_command_options) ].join(" ")
84
+ end
85
+
86
+ def metrics_port
87
+ run_config["metrics_port"]
88
+ end
89
+
90
+ def run_command_options
91
+ { debug: debug? || nil, "metrics-port": metrics_port }.compact
92
+ end
93
+
94
+ def docker_options_args
95
+ [
96
+ *apps_volume_args,
97
+ *publish_args,
98
+ *logging_args,
99
+ *("--expose=#{metrics_port}" if metrics_port.present?),
100
+ *options_args
101
+ ].compact
102
+ end
103
+
104
+ def host_directory
105
+ File.join config.run_directory, "proxy"
106
+ end
107
+
108
+ def apps_directory
109
+ File.join host_directory, "apps-config"
110
+ end
111
+
112
+ def apps_container_directory
113
+ "/home/kamal-proxy/.apps-config"
114
+ end
115
+
116
+ def apps_volume
117
+ Kamal::Configuration::Volume.new \
118
+ host_path: apps_directory,
119
+ container_path: apps_container_directory
120
+ end
121
+
122
+ def apps_volume_args
123
+ [ apps_volume.docker_args ]
124
+ end
125
+
126
+ def app_directory
127
+ File.join apps_directory, config.service_and_destination
128
+ end
129
+
130
+ def app_container_directory
131
+ File.join apps_container_directory, config.service_and_destination
132
+ end
133
+
134
+ private
135
+ def format_bind_ip(ip)
136
+ # Ensure IPv6 address inside square brackets - e.g. [::1]
137
+ if ip =~ Resolv::IPv6::Regex && ip !~ /\A\[.*\]\z/
138
+ "[#{ip}]"
139
+ else
140
+ ip
141
+ end
142
+ end
143
+ end
@@ -6,8 +6,7 @@ class Kamal::Configuration::Proxy
6
6
 
7
7
  delegate :argumentize, :optionize, to: Kamal::Utils
8
8
 
9
- attr_reader :config, :proxy_config, :role_name, :secrets
10
-
9
+ attr_reader :config, :proxy_config, :role_name, :run, :secrets
11
10
  def initialize(config:, proxy_config:, role_name: nil, secrets:, context: "proxy")
12
11
  @config = config
13
12
  @proxy_config = proxy_config
@@ -15,6 +14,7 @@ class Kamal::Configuration::Proxy
15
14
  @role_name = role_name
16
15
  @secrets = secrets
17
16
  validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
17
+ @run = Kamal::Configuration::Proxy::Run.new(config, run_config: @proxy_config["run"], context: "#{context}/run") if @proxy_config && @proxy_config["run"].present?
18
18
  end
19
19
 
20
20
  def app_port
@@ -127,7 +127,7 @@ class Kamal::Configuration::Role
127
127
 
128
128
 
129
129
  def asset_path
130
- specializations["asset_path"] || config.asset_path
130
+ asset_path_config&.dig(0)
131
131
  end
132
132
 
133
133
  def assets?
@@ -137,10 +137,14 @@ class Kamal::Configuration::Role
137
137
  def asset_volume(version = config.version)
138
138
  if assets?
139
139
  Kamal::Configuration::Volume.new \
140
- host_path: asset_volume_directory(version), container_path: asset_path
140
+ host_path: asset_volume_directory(version), container_path: asset_path, options: asset_path_options
141
141
  end
142
142
  end
143
143
 
144
+ def asset_path_options
145
+ asset_path_config&.dig(1)
146
+ end
147
+
144
148
  def asset_extracted_directory(version = config.version)
145
149
  File.join config.assets_directory, "extracted", [ name, version ].join("-")
146
150
  end
@@ -219,4 +223,12 @@ class Kamal::Configuration::Role
219
223
  labels.merge!(specializations["labels"]) if specializations["labels"].present?
220
224
  end
221
225
  end
226
+
227
+ def asset_path_config
228
+ raw_path = specializations["asset_path"] || config.asset_path
229
+ return nil unless raw_path.present?
230
+
231
+ parts = raw_path.split(":", 2)
232
+ [ parts[0], parts[1] ]
233
+ end
222
234
  end
@@ -3,10 +3,11 @@ class Kamal::Configuration::Ssh
3
3
 
4
4
  include Kamal::Configuration::Validation
5
5
 
6
- attr_reader :ssh_config
6
+ attr_reader :ssh_config, :secrets
7
7
 
8
8
  def initialize(config:)
9
9
  @ssh_config = config.raw_config.ssh || {}
10
+ @secrets = config.secrets
10
11
  validate! ssh_config
11
12
  end
12
13
 
@@ -35,7 +36,17 @@ class Kamal::Configuration::Ssh
35
36
  end
36
37
 
37
38
  def key_data
38
- ssh_config["key_data"]
39
+ key_data = ssh_config["key_data"]
40
+ return unless key_data
41
+
42
+ key_data.map do |k|
43
+ if secrets.key?(k)
44
+ secrets[k]
45
+ else
46
+ warn "Inline key_data usage is deprecated and will be removed in Kamal 3. Please store your key_data in a secret."
47
+ k
48
+ end
49
+ end
39
50
  end
40
51
 
41
52
  def config
@@ -20,6 +20,26 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
20
20
  error "Missing certificate_pem setting (required when private_key_pem is present)"
21
21
  end
22
22
  end
23
+
24
+ if run_config = config["run"]
25
+ if run_config["bind_ips"].present?
26
+ ensure_valid_bind_ips(config["bind_ips"])
27
+ end
28
+
29
+ if run_config["publish"] == false
30
+ if run_config["bind_ips"].present? || run_config["http_port"].present? || run_config["https_port"].present?
31
+ error "Cannot set http_port, https_port or bind_ips when publish is false"
32
+ end
33
+ end
34
+ end
23
35
  end
24
36
  end
37
+
38
+ private
39
+ def ensure_valid_bind_ips(bind_ips)
40
+ bind_ips.present? && bind_ips.each do |ip|
41
+ next if ip =~ Resolv::IPv4::Regex || ip =~ Resolv::IPv6::Regex
42
+ error "Invalid publish IP address: #{ip}"
43
+ end
44
+ end
25
45
  end
@@ -36,6 +36,8 @@ class Kamal::Configuration::Validator
36
36
  validate_array_of_or_type! value, example_value.first.class
37
37
  elsif key.to_s == "config"
38
38
  validate_ssh_config!(value)
39
+ elsif key.to_s == "files" || key.to_s == "directories"
40
+ validate_paths!(value)
39
41
  else
40
42
  validate_array_of! value, example_value.first.class
41
43
  end
@@ -141,6 +143,24 @@ class Kamal::Configuration::Validator
141
143
  end
142
144
  end
143
145
 
146
+ def validate_paths!(paths)
147
+ validate_type! paths, Array
148
+
149
+ paths.each_with_index do |path, index|
150
+ with_context(index) do
151
+ validate_type! path, String, Hash
152
+
153
+ if path.is_a?(Hash)
154
+ %w[local remote mode owner options].each do |key|
155
+ with_context(key) do
156
+ validate_type! path[key], String if path.key?(key)
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+
144
164
  def validate_type!(value, *types)
145
165
  type_error(*types) unless types.any? { |type| valid_type?(value, type) }
146
166
  end
@@ -1,14 +1,21 @@
1
1
  class Kamal::Configuration::Volume
2
- attr_reader :host_path, :container_path
2
+ attr_reader :host_path, :container_path, :options
3
3
  delegate :argumentize, to: Kamal::Utils
4
4
 
5
- def initialize(host_path:, container_path:)
5
+ def initialize(host_path:, container_path:, options: nil)
6
6
  @host_path = host_path
7
7
  @container_path = container_path
8
+ @options = options
8
9
  end
9
10
 
10
11
  def docker_args
11
- argumentize "--volume", "#{host_path_for_docker_volume}:#{container_path}"
12
+ argumentize "--volume", docker_args_string
13
+ end
14
+
15
+ def docker_args_string
16
+ volume_string = "#{host_path_for_docker_volume}:#{container_path}"
17
+ volume_string += ":#{options}" if options.present?
18
+ volume_string
12
19
  end
13
20
 
14
21
  private
@@ -16,7 +23,7 @@ class Kamal::Configuration::Volume
16
23
  if Pathname.new(host_path).absolute?
17
24
  host_path
18
25
  else
19
- File.join "$(pwd)", host_path
26
+ "$PWD/#{host_path}"
20
27
  end
21
28
  end
22
29
  end