vagrant-docker-hosts-manager 0.3.0 → 0.4.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.
@@ -1,6 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VagrantDockerHostsManager
4
+ # Vagrant configuration for managed host entries.
5
+ #
6
+ # @!attribute domains
7
+ # @return [Hash{String=>String}] Mapping of domain names to IP addresses.
8
+ # @!attribute domain
9
+ # @return [String, nil] Single domain to resolve from `ip` or `container_name`.
10
+ # @!attribute container_name
11
+ # @return [String, nil] Docker container used for automatic IP discovery.
12
+ # @!attribute ip
13
+ # @return [String, nil] Static IPv4 address for `domain`.
14
+ # @!attribute locale
15
+ # @return [String, nil] Optional locale code.
4
16
  class Config < Vagrant.plugin("2", :config)
5
17
  attr_accessor :domains
6
18
  attr_accessor :domain
@@ -10,19 +22,28 @@ module VagrantDockerHostsManager
10
22
  attr_accessor :verbose
11
23
 
12
24
  def initialize
13
- @domains = {}
14
- @domain = nil
15
- @container_name = nil
16
- @ip = nil
17
- @locale = nil
18
- @verbose = false
25
+ @domains = UNSET_VALUE
26
+ @domain = UNSET_VALUE
27
+ @container_name = UNSET_VALUE
28
+ @ip = UNSET_VALUE
29
+ @locale = UNSET_VALUE
30
+ @verbose = UNSET_VALUE
19
31
  end
20
32
 
21
- def finalize!; end
33
+ def finalize!
34
+ @domains = {} if @domains == UNSET_VALUE
35
+ @domain = nil if @domain == UNSET_VALUE
36
+ @container_name = nil if @container_name == UNSET_VALUE
37
+ @ip = nil if @ip == UNSET_VALUE
38
+ @locale = nil if @locale == UNSET_VALUE
39
+ @verbose = false if @verbose == UNSET_VALUE
40
+ end
22
41
 
23
42
  def validate(_machine)
24
43
  errors = []
25
44
 
45
+ return { "vagrant-docker-hosts-manager" => errors } unless configured?
46
+
26
47
  if (@domains.nil? || @domains.empty?) && (@domain.nil? || @domain.strip.empty?)
27
48
  errors << "You must configure at least one domain: " \
28
49
  "`config.docker_hosts.domain = \"example.test\"` or set " \
@@ -43,5 +64,16 @@ module VagrantDockerHostsManager
43
64
 
44
65
  { "vagrant-docker-hosts-manager" => errors }
45
66
  end
67
+
68
+ private
69
+
70
+ def configured?
71
+ (@domains.is_a?(Hash) && !@domains.empty?) ||
72
+ present?(@domain) || present?(@container_name) || present?(@ip)
73
+ end
74
+
75
+ def present?(value)
76
+ !value.nil? && !value.to_s.strip.empty?
77
+ end
46
78
  end
47
79
  end
@@ -9,6 +9,7 @@ module VagrantDockerHostsManager
9
9
 
10
10
  SUPPORTED = [:en, :fr].freeze
11
11
  OUR_NAMESPACES = %w[messages. errors. log. help.].freeze
12
+ NS = "vdhm"
12
13
 
13
14
  EMOJI = {
14
15
  success: "✅",
@@ -31,6 +32,9 @@ module VagrantDockerHostsManager
31
32
 
32
33
  base = File.expand_path("../../locales", __dir__)
33
34
  paths = Dir[File.join(base, "*.yml")]
35
+ if paths.empty? && File.directory?(base)
36
+ paths = Dir.children(base).grep(/\.ya?ml\z/).map { |file| File.join(base, file) }
37
+ end
34
38
  ::I18n.load_path |= paths
35
39
  ::I18n.available_locales = SUPPORTED
36
40
 
@@ -60,22 +64,28 @@ module VagrantDockerHostsManager
60
64
  set_locale!("en")
61
65
  end
62
66
 
67
+ def namespaced(key)
68
+ k = key.to_s
69
+ k.start_with?("#{NS}.") ? k : "#{NS}.#{k}"
70
+ end
71
+
63
72
  def t(key, **opts)
64
- ::I18n.t(key, **opts)
73
+ setup_i18n!
74
+ ::I18n.t(namespaced(key), **opts)
65
75
  end
66
76
 
67
77
  def t!(key, **opts)
68
78
  setup_i18n!
69
- k = key.to_s
70
- if our_key?(k) && !::I18n.exists?(k, ::I18n.locale)
71
- raise MissingTranslationError, "#{EMOJI[:error]} [#{::I18n.locale}] Missing translation for key: #{k}"
79
+ nk = namespaced(key)
80
+ if our_key?(key.to_s) && !::I18n.exists?(nk, ::I18n.locale)
81
+ raise MissingTranslationError, "#{EMOJI[:error]} [#{::I18n.locale}] Missing translation for key: #{nk}"
72
82
  end
73
- ::I18n.t(k, **opts)
83
+ ::I18n.t(nk, **opts)
74
84
  end
75
85
 
76
86
  def t_hash(key)
77
87
  setup_i18n!
78
- v = ::I18n.t(key, default: {})
88
+ v = ::I18n.t(namespaced(key), default: {})
79
89
  v.is_a?(Hash) ? v : {}
80
90
  end
81
91
 
@@ -11,6 +11,8 @@ require_relative "util/hosts_file"
11
11
  require_relative "util/docker"
12
12
  require_relative "util/json"
13
13
  require_relative "util/i18n"
14
+ require_relative "actions/apply"
15
+ require_relative "actions/cleanup"
14
16
 
15
17
  begin
16
18
  I18n.enforce_available_locales = false
@@ -47,94 +49,4 @@ module VagrantDockerHostsManager
47
49
  hook.prepend(Action::Cleanup)
48
50
  end
49
51
  end
50
-
51
- module Action
52
- class Apply
53
- def initialize(app, env) = (@app = app)
54
-
55
- def call(env)
56
- Util::I18n.setup!(env)
57
- cfg = env[:machine].config.docker_hosts
58
- mid = env[:machine].id || "unknown"
59
- dry = Util::I18n.env_flag("VDHM_DRY_RUN")
60
- ui = env[:ui]
61
- hoster = Util::HostsFile.new(env, owner_id: mid)
62
-
63
- entries = compute_entries(env, cfg, ui)
64
- if entries.empty?
65
- UiHelpers.say(ui, ::I18n.t("messages.no_entries"))
66
- return
67
- end
68
-
69
- if dry
70
- Util::Json.emit(action: "apply", status: "dry-run", data: { owner: mid, entries: entries })
71
- return
72
- end
73
-
74
- hoster.apply(entries)
75
- Util::Json.emit(action: "apply", status: "success", data: { owner: mid, entries: entries })
76
- rescue StandardError => e
77
- Util::Json.emit(action: "apply", status: "error", error: e.message, backtrace: e.backtrace&.first(3))
78
- UiHelpers.error(ui, "VDHM: #{e.message}")
79
- ensure
80
- @app.call(env)
81
- end
82
-
83
- private
84
-
85
- def compute_entries(env, cfg, ui)
86
- entries = {}
87
-
88
- cfg.domains.each do |domain, ip|
89
- next if domain.to_s.strip.empty?
90
- if ip.nil? || ip.to_s.strip.empty?
91
- UiHelpers.warn(ui, ::I18n.t("messages.missing_ip_for", domain: domain))
92
- next
93
- end
94
- entries[domain] = ip
95
- end
96
-
97
- if cfg.domain && !cfg.domain.strip.empty?
98
- ip = cfg.ip || begin
99
- if cfg.container_name && !cfg.container_name.strip.empty?
100
- Util::Docker.ip_for_container(cfg.container_name)
101
- end
102
- end
103
- if ip && !ip.strip.empty?
104
- UiHelpers.say(ui, ::I18n.t("messages.detected_ip", domain: cfg.domain, ip: ip))
105
- entries[cfg.domain] = ip
106
- else
107
- UiHelpers.warn(ui, ::I18n.t("messages.no_ip_found", domain: cfg.domain, container: cfg.container_name))
108
- end
109
- end
110
-
111
- entries
112
- end
113
- end
114
-
115
- class Cleanup
116
- def initialize(app, env) = (@app = app)
117
-
118
- def call(env)
119
- Util::I18n.setup!(env)
120
- mid = env[:machine].id || "unknown"
121
- dry = Util::I18n.env_flag("VDHM_DRY_RUN")
122
- ui = env[:ui]
123
- hoster = Util::HostsFile.new(env, owner_id: mid)
124
-
125
- if dry
126
- Util::Json.emit(action: "cleanup", status: "dry-run", data: { owner: mid })
127
- return
128
- end
129
-
130
- removed = hoster.remove!
131
- Util::Json.emit(action: "cleanup", status: "success", data: { owner: mid, removed: removed })
132
- rescue StandardError => e
133
- Util::Json.emit(action: "cleanup", status: "error", error: e.message)
134
- UiHelpers.error(ui, "VDHM: #{e.message}")
135
- ensure
136
- @app.call(env)
137
- end
138
- end
139
- end
140
52
  end
@@ -1,32 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require_relative "verbose"
4
5
 
5
6
  module VagrantDockerHostsManager
6
7
  module Util
7
8
  module Docker
8
9
  module_function
9
10
 
11
+ # Resolves the first IPv4 address exposed by Docker inspect for a container.
12
+ #
13
+ # @param name [String, #to_s] Docker container name or id.
14
+ # @return [String, nil] First IPv4 address, or nil when Docker cannot resolve it.
10
15
  def ip_for_container(name)
11
16
  return nil if name.to_s.strip.empty?
12
- out, _err, status = Open3.capture3(
13
- "docker", "inspect", "-f",
14
- "{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}",
15
- name.to_s
16
- )
17
+
18
+ fmt = "{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}"
19
+ Verbose.log("docker", "inspect", "-f", fmt, name.to_s)
20
+ out, _err, status = Open3.capture3("docker", "inspect", "-f", fmt, name.to_s)
17
21
  return nil unless status.success?
18
22
  out.split(/\s+/).find { |ip| ip =~ /\A\d{1,3}(\.\d{1,3}){3}\z/ }
19
23
  rescue StandardError
20
24
  nil
21
25
  end
22
-
23
- def shell_escape(str)
24
- if Gem.win_platform?
25
- %('#{str.to_s.gsub("'", "''")}')
26
- else
27
- %('#{str.to_s.gsub("'", "'\\''")}')
28
- end
29
- end
30
26
  end
31
27
  end
32
28
  end
@@ -7,6 +7,7 @@ require "open3"
7
7
 
8
8
  require_relative "../helpers"
9
9
  require_relative "docker"
10
+ require_relative "verbose"
10
11
 
11
12
  module VagrantDockerHostsManager
12
13
  module Util
@@ -16,6 +17,10 @@ module VagrantDockerHostsManager
16
17
  WIN_SYSNATIVE_PATH = "C:/Windows/Sysnative/drivers/etc/hosts"
17
18
  WIN_SYSWOW64_PATH = "C:/Windows/SysWOW64/drivers/etc/hosts"
18
19
 
20
+ BLOCK_START_RE = /^\s*#\s*>>>\s*vagrant-docker-hosts-manager\s+(.+?)\s*\(managed\)\s*>>>\s*$/i
21
+ BLOCK_STOP_RE = /^\s*#\s*<<<\s*vagrant-docker-hosts-manager\s+(.+?)\s*\(managed\)\s*<<<\s*$/i
22
+ ENTRY_RE = /\A\s*(\d{1,3}(?:\.\d{1,3}){3})\s+([^\s#]+)/
23
+
19
24
  def initialize(env, owner_id:)
20
25
  @env = env || {}
21
26
  @owner_id = owner_id.to_s
@@ -28,8 +33,13 @@ module VagrantDockerHostsManager
28
33
  end
29
34
 
30
35
  def path_candidates
31
- return [POSIX_PATH] unless Gem.win_platform?
36
+ # VDHM_HOSTS_PATH overrides the default location on every platform
37
+ # (used by tests and power users); honor it before anything else.
32
38
  return [override_path] if override_path
39
+ return [POSIX_PATH] unless Gem.win_platform?
40
+
41
+ # Sysnative lets a 32-bit Ruby process reach the real System32 hosts
42
+ # file on 64-bit Windows; keep it before System32/SysWOW64.
33
43
  [WIN_SYSNATIVE_PATH, WIN_SYS32_PATH, WIN_SYSWOW64_PATH]
34
44
  end
35
45
 
@@ -103,11 +113,20 @@ module VagrantDockerHostsManager
103
113
  str.end_with?(nl) ? str : (str + nl)
104
114
  end
105
115
 
116
+ # Applies managed host entries to the hosts file.
117
+ #
118
+ # Rewrites only this plugin's managed block and preserves unmanaged lines.
119
+ # Entries are normalized and sorted so repeated runs converge to the same
120
+ # file content.
121
+ #
122
+ # @param entries [Hash{String=>String}] Mapping of FQDN to IP address.
123
+ # @return [Integer] Number of managed entries present after applying changes.
124
+ # @raise [RuntimeError] When elevated writes fail.
106
125
  def apply(entries)
107
126
  entries = normalize_entries(entries)
108
127
  if entries.empty?
109
128
  UiHelpers.say(@ui, "#{UiHelpers.e(:info)} " +
110
- ::I18n.t("messages.no_entries", default: "No hosts entries configured."))
129
+ ::I18n.t("vdhm.messages.no_entries", default: "No hosts entries configured."))
111
130
  return 0
112
131
  end
113
132
 
@@ -137,7 +156,7 @@ module VagrantDockerHostsManager
137
156
 
138
157
  if added.empty? && updated.empty?
139
158
  UiHelpers.say(@ui, "#{UiHelpers.e(:info)} " +
140
- ::I18n.t("messages.no_change", default: "Nothing to apply. Already up-to-date."))
159
+ ::I18n.t("vdhm.messages.no_change", default: "Nothing to apply. Already up-to-date."))
141
160
  return existing_map.size
142
161
  end
143
162
 
@@ -146,14 +165,21 @@ module VagrantDockerHostsManager
146
165
  write(content)
147
166
 
148
167
  UiHelpers.say(@ui, "#{UiHelpers.e(:success)} " +
149
- ::I18n.t("messages.applied", default: "Hosts entries applied."))
168
+ ::I18n.t("vdhm.messages.applied", default: "Hosts entries applied."))
150
169
  UiHelpers.say(@ui, "#{UiHelpers.e(:info)} " +
151
- ::I18n.t("messages.apply_summary",
170
+ ::I18n.t("vdhm.messages.apply_summary",
152
171
  default: "Added: %{a}, Updated: %{u}, Unchanged: %{s}",
153
172
  a: added.size, u: updated.size, s: unchanged.size))
154
173
  merged.size
155
174
  end
156
175
 
176
+ # Removes matching entries from the current owner's managed block.
177
+ #
178
+ # With no filters this falls back to removing the whole current-owner block.
179
+ #
180
+ # @param ips [Array<String>] IP addresses to remove.
181
+ # @param domains [Array<String>] Domain names to remove.
182
+ # @return [Integer] Number of removed entries, or 1 when a whole block was removed.
157
183
  def remove_entries!(ips: [], domains: [])
158
184
  ips = Array(ips).map(&:to_s).reject(&:empty?)
159
185
  domains = Array(domains).map(&:to_s).reject(&:empty?)
@@ -170,7 +196,7 @@ module VagrantDockerHostsManager
170
196
  removed_count = before - filtered.length
171
197
  if removed_count <= 0
172
198
  UiHelpers.say(@ui, "#{UiHelpers.e(:info)} " +
173
- ::I18n.t("messages.remove_none",
199
+ ::I18n.t("vdhm.messages.remove_none",
174
200
  default: "No matching entry to remove."))
175
201
  return 0
176
202
  end
@@ -192,7 +218,7 @@ module VagrantDockerHostsManager
192
218
  @ui,
193
219
  "#{UiHelpers.e(:broom)} " +
194
220
  ::I18n.t(
195
- "messages.removed_count",
221
+ "vdhm.messages.removed_count",
196
222
  default: "%{count} entries removed.",
197
223
  count: removed_count
198
224
  )
@@ -200,6 +226,9 @@ module VagrantDockerHostsManager
200
226
  removed_count
201
227
  end
202
228
 
229
+ # Removes the current owner's managed hosts block.
230
+ #
231
+ # @return [Boolean] Whether a block was removed.
203
232
  def remove!
204
233
  content = read
205
234
  newc = remove_block_from(content)
@@ -207,12 +236,44 @@ module VagrantDockerHostsManager
207
236
  write(newc) if removed
208
237
 
209
238
  UiHelpers.say(@ui, removed ?
210
- "#{UiHelpers.e(:broom)} " + ::I18n.t("messages.cleaned", default: "Managed hosts entries removed.") :
211
- "#{UiHelpers.e(:info)} " + ::I18n.t("messages.nothing_to_clean", default: "Nothing to clean."))
239
+ "#{UiHelpers.e(:broom)} " + ::I18n.t("vdhm.messages.cleaned", default: "Managed hosts entries removed.") :
240
+ "#{UiHelpers.e(:info)} " + ::I18n.t("vdhm.messages.nothing_to_clean", default: "Nothing to clean."))
241
+ removed
242
+ end
243
+
244
+ # Removes every block managed by this plugin, regardless of owner.
245
+ #
246
+ # @return [Boolean] Whether any managed block was removed.
247
+ def remove_all_managed!
248
+ content = read
249
+ newc = strip_managed_blocks(content)
250
+ removed = (newc != content)
251
+ write(newc) if removed
252
+
253
+ UiHelpers.say(@ui, removed ?
254
+ "#{UiHelpers.e(:broom)} " + ::I18n.t("vdhm.messages.cleaned_all", default: "All managed hosts blocks removed.") :
255
+ "#{UiHelpers.e(:info)} " + ::I18n.t("vdhm.messages.nothing_to_clean", default: "Nothing to clean."))
212
256
  removed
213
257
  end
214
258
 
259
+ def strip_managed_blocks(content)
260
+ removing = false
261
+ content.lines.reject do |line|
262
+ if line.match?(BLOCK_START_RE)
263
+ removing = true
264
+ true
265
+ elsif line.match?(BLOCK_STOP_RE)
266
+ removing = false
267
+ true
268
+ else
269
+ removing
270
+ end
271
+ end.join
272
+ end
273
+
215
274
  def read
275
+ # Hosts files are often edited by Windows tools with BOMs or legacy
276
+ # encodings; normalize to UTF-8 before parsing managed blocks.
216
277
  pth = real_path
217
278
  UiHelpers.debug(@ui, "read(#{pth})")
218
279
 
@@ -229,6 +290,7 @@ module VagrantDockerHostsManager
229
290
  if (data.nil? || data.empty?) && Gem.win_platform?
230
291
  ps_path = pth.gsub("'", "''")
231
292
  ps_cmd = "Get-Content -LiteralPath '#{ps_path}' -Raw"
293
+ Verbose.log("powershell -Command #{ps_cmd}")
232
294
  out, err, st = Open3.capture3("powershell", "-NoProfile", "-NonInteractive", "-Command", ps_cmd)
233
295
  if st.success?
234
296
  data = out
@@ -275,29 +337,28 @@ module VagrantDockerHostsManager
275
337
  ""
276
338
  end
277
339
 
278
- def list_pairs(scope = :all)
279
- content = read
280
- return [] if content.to_s.empty?
340
+ def each_managed_entry(scope = :all)
341
+ # A single scanner powers both current-owner and all-owner cleanup paths
342
+ # so marker parsing rules stay identical.
343
+ return enum_for(:each_managed_entry, scope) unless block_given?
281
344
 
282
- start_re = /^\s*#\s*>>>\s*vagrant-docker-hosts-manager\s+(.+?)\s*\(managed\)\s*>>>\s*$/i
283
- stop_re = /^\s*#\s*<<<\s*vagrant-docker-hosts-manager\s+(.+?)\s*\(managed\)\s*<<<\s*$/i
284
- ip_re = /^\s*(\d{1,3}(?:\.\d{1,3}){3})\s+([^\s#]+)/
345
+ content = read
346
+ return if content.to_s.empty?
285
347
 
286
- out = []
287
348
  in_block = false
288
349
  owner = nil
289
350
 
290
351
  content.each_line.with_index(1) do |raw, idx|
291
352
  line = raw.delete_suffix("\n").delete_suffix("\r")
292
353
 
293
- if (m = line.match(start_re))
354
+ if (m = line.match(BLOCK_START_RE))
294
355
  in_block = true
295
356
  owner = m[1].to_s.strip
296
357
  UiHelpers.debug(@ui, "start block(owner=#{owner}) at line #{idx}")
297
358
  next
298
359
  end
299
360
 
300
- if (m = line.match(stop_re))
361
+ if line.match?(BLOCK_STOP_RE)
301
362
  UiHelpers.debug(@ui, "stop block(owner=#{owner}) at line #{idx}") if in_block
302
363
  in_block = false
303
364
  owner = nil
@@ -307,103 +368,25 @@ module VagrantDockerHostsManager
307
368
  next unless in_block
308
369
  next unless scope == :all || owner == @owner_id
309
370
 
310
- if (m = line.match(ip_re))
311
- ip, fqdn = m[1], m[2]
312
- out << [ip, fqdn, owner]
313
- UiHelpers.debug(@ui, " ip line: #{ip} #{fqdn} (owner=#{owner})")
371
+ if (m = line.match(ENTRY_RE))
372
+ UiHelpers.debug(@ui, " ip line: #{m[1]} #{m[2]} (owner=#{owner})")
373
+ yield m[1], m[2], owner
314
374
  end
315
375
  end
316
-
317
- UiHelpers.debug(@ui, "list_pairs found #{out.length} pair(s)")
318
- out
319
376
  end
320
377
 
321
- def entries_in_blocks(scope = :current)
322
- content = read
323
- return {} if content.to_s.empty?
324
-
325
- start_prefix = "# >>> vagrant-docker-hosts-manager "
326
- stop_prefix = "# <<< vagrant-docker-hosts-manager "
327
-
328
- in_block = false
329
- owner = nil
330
- out = {}
331
-
332
- content.each_line do |raw|
333
- line = raw.sub(/\r?\n\z/, "")
334
- lstr = line.lstrip
335
-
336
- if lstr.start_with?(start_prefix)
337
- in_block = true
338
- tail = lstr[start_prefix.length..-1].to_s
339
- owner = tail.split(" (managed)").first.to_s.strip
340
- next
341
- end
342
-
343
- if lstr.start_with?(stop_prefix)
344
- in_block = false
345
- owner = nil
346
- next
347
- end
348
-
349
- next unless in_block
350
- next unless scope == :all || owner == @owner_id
351
-
352
- if lstr =~ /\A\s*(\d{1,3}(?:\.\d{1,3}){3})\s+([^\s#]+)/
353
- ip, fqdn = $1, $2
354
- if out.key?(fqdn)
355
- arr = out[fqdn].is_a?(Array) ? out[fqdn] : [out[fqdn]]
356
- arr << ip unless arr.include?(ip)
357
- out[fqdn] = arr
358
- else
359
- out[fqdn] = [ip]
360
- end
361
- end
362
- end
363
-
364
- out
378
+ def list_pairs(scope = :all)
379
+ pairs = []
380
+ each_managed_entry(scope) { |ip, fqdn, owner| pairs << [ip, fqdn, owner] }
381
+ UiHelpers.debug(@ui, "list_pairs found #{pairs.length} pair(s)")
382
+ pairs
365
383
  end
366
384
 
367
- def managed_blocks_dump(scope = :all)
368
- content = read
369
- return "" if content.to_s.empty?
370
-
371
- start_re = /^\s*#\s*>>>\s*vagrant-docker-hosts-manager\s+(.+?)\s*\(managed\)\s*>>>\s*$/i
372
- stop_re = /^\s*#\s*<<<\s*vagrant-docker-hosts-manager\s+(.+?)\s*\(managed\)\s*<<<\s*$/i
373
-
374
- buff = []
375
- cur = []
376
- in_block = false
377
- owner = nil
378
-
379
- content.each_line do |raw|
380
- line = raw.delete_suffix("\n").delete_suffix("\r")
381
-
382
- if (m = line.match(start_re))
383
- in_block = true
384
- owner = m[1].to_s.strip
385
- cur = [line]
386
- next
387
- end
388
-
389
- if in_block
390
- cur << line
391
- if line.match?(stop_re)
392
- if scope == :all || owner == @owner_id
393
- buff << cur.join("\n")
394
- end
395
- in_block = false
396
- owner = nil
397
- cur = []
398
- end
399
- end
385
+ def entries_in_blocks(scope = :current)
386
+ each_managed_entry(scope).with_object({}) do |(ip, fqdn, _owner), out|
387
+ arr = (out[fqdn] ||= [])
388
+ arr << ip unless arr.include?(ip)
400
389
  end
401
-
402
- buff.join("\n\n\n")
403
- end
404
-
405
- def current_entries
406
- entries_in_blocks(:current)
407
390
  end
408
391
 
409
392
  def remove_block_from(content)
@@ -429,6 +412,7 @@ module VagrantDockerHostsManager
429
412
  [Security.Principal.WindowsIdentity]::GetCurrent()
430
413
  )).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
431
414
  }.strip
415
+ Verbose.log("powershell -Command (check administrator role)")
432
416
  out, _err, st = Open3.capture3("powershell", "-NoProfile", "-NonInteractive", "-Command", cmd)
433
417
  st.success? && out.to_s.strip.downcase == "true"
434
418
  else
@@ -451,6 +435,7 @@ module VagrantDockerHostsManager
451
435
  begin
452
436
  tf.binmode
453
437
  tf.write(content); tf.flush
438
+ Verbose.log("sudo", "cp", tf.path, real_path)
454
439
  system("sudo", "cp", tf.path, real_path) || raise("sudo copy failed")
455
440
  ensure
456
441
  tf.close!
@@ -458,6 +443,8 @@ module VagrantDockerHostsManager
458
443
  end
459
444
 
460
445
  def write_windows(content)
446
+ # Try a direct write first; fall back to UAC only when the hosts file
447
+ # rejects it. Base64/UTF-16LE keeps PowerShell quoting predictable.
461
448
  b64 = Base64.strict_encode64(
462
449
  content.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
463
450
  )
@@ -465,16 +452,31 @@ module VagrantDockerHostsManager
465
452
 
466
453
  ps = <<~POW
467
454
  $ErrorActionPreference = "Stop"
468
- $bytes = [System.Convert]::FromBase64String('#{b64}')
469
- [System.IO.File]::WriteAllBytes('#{dest}', $bytes)
455
+ try {
456
+ $bytes = [System.Convert]::FromBase64String('#{b64}')
457
+ [System.IO.File]::WriteAllBytes('#{dest}', $bytes)
458
+ exit 0
459
+ } catch {
460
+ exit 1
461
+ }
470
462
  POW
471
463
  encoded = Base64.strict_encode64(ps.encode("UTF-16LE"))
464
+
465
+ Verbose.log("powershell -EncodedCommand (write hosts file: #{real_path})")
472
466
  _out, _err, st = Open3.capture3("powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded)
473
467
  return if st.success?
474
468
 
475
- elev_ps = "Start-Process PowerShell -Verb RunAs -Wait " \
476
- "-ArgumentList '-NonInteractive', '-NoProfile', '-EncodedCommand', '#{encoded}'"
469
+ elev_ps = <<~POW
470
+ $ErrorActionPreference = 'Stop'
471
+ try {
472
+ $p = Start-Process PowerShell -Verb RunAs -Wait -PassThru -ArgumentList '-NonInteractive','-NoProfile','-EncodedCommand','#{encoded}'
473
+ exit $p.ExitCode
474
+ } catch {
475
+ exit 1
476
+ }
477
+ POW
477
478
  elev_encoded = Base64.strict_encode64(elev_ps.encode("UTF-16LE"))
479
+ Verbose.log("powershell -EncodedCommand (elevated write hosts file via RunAs: #{real_path})")
478
480
  system("powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", elev_encoded) ||
479
481
  raise("elevated write failed")
480
482
  end
@@ -33,7 +33,7 @@ module VagrantDockerHostsManager
33
33
  begin
34
34
  if env[:ui] && env[:machine]&.config&.docker_hosts&.respond_to?(:verbose) &&
35
35
  env[:machine].config.docker_hosts.verbose
36
- env[:ui].info(::I18n.t("messages.lang_set", lang: ::I18n.locale))
36
+ env[:ui].info(::I18n.t("vdhm.messages.lang_set", lang: ::I18n.locale))
37
37
  end
38
38
  rescue StandardError
39
39
  nil
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module VagrantDockerHostsManager
6
+ module Util
7
+ module Verbose
8
+ module_function
9
+
10
+ def enabled?
11
+ ENV["VDHM_VERBOSE"].to_s == "1"
12
+ end
13
+
14
+ def log(*args)
15
+ return unless enabled?
16
+
17
+ line = args.length == 1 && args.first.is_a?(String) ? args.first : args.map(&:to_s).shelljoin
18
+ warn("[VDHM] #{line}")
19
+ end
20
+ end
21
+ end
22
+ end