vagrant-docker-hosts-manager 0.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE.md +22 -0
- data/README.md +196 -0
- data/lib/vagrant-docker-hosts-manager/VERSION +1 -0
- data/lib/vagrant-docker-hosts-manager/command.rb +457 -0
- data/lib/vagrant-docker-hosts-manager/config.rb +44 -0
- data/lib/vagrant-docker-hosts-manager/helpers.rb +42 -0
- data/lib/vagrant-docker-hosts-manager/plugin.rb +142 -0
- data/lib/vagrant-docker-hosts-manager/util/docker.rb +29 -0
- data/lib/vagrant-docker-hosts-manager/util/hosts_file.rb +477 -0
- data/lib/vagrant-docker-hosts-manager/util/i18n.rb +40 -0
- data/lib/vagrant-docker-hosts-manager/util/json.rb +15 -0
- data/lib/vagrant-docker-hosts-manager/version.rb +10 -0
- data/lib/vagrant-docker-hosts-manager.rb +3 -0
- data/locales/en.yml +114 -0
- data/locales/fr.yml +115 -0
- metadata +108 -0
@@ -0,0 +1,477 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
require "time"
|
5
|
+
require "open3"
|
6
|
+
|
7
|
+
require_relative "../helpers"
|
8
|
+
require_relative "docker"
|
9
|
+
|
10
|
+
module VagrantDockerHostsManager
|
11
|
+
module Util
|
12
|
+
class HostsFile
|
13
|
+
POSIX_PATH = "/etc/hosts"
|
14
|
+
WIN_SYS32_PATH = "C:/Windows/System32/drivers/etc/hosts"
|
15
|
+
WIN_SYSNATIVE_PATH = "C:/Windows/Sysnative/drivers/etc/hosts"
|
16
|
+
WIN_SYSWOW64_PATH = "C:/Windows/SysWOW64/drivers/etc/hosts"
|
17
|
+
|
18
|
+
def initialize(env, owner_id:)
|
19
|
+
@env = env || {}
|
20
|
+
@owner_id = owner_id.to_s
|
21
|
+
@ui = env[:ui]
|
22
|
+
end
|
23
|
+
|
24
|
+
def override_path
|
25
|
+
p = ENV["VDHM_HOSTS_PATH"].to_s.strip
|
26
|
+
p.empty? ? nil : p
|
27
|
+
end
|
28
|
+
|
29
|
+
def path_candidates
|
30
|
+
return [POSIX_PATH] unless Gem.win_platform?
|
31
|
+
return [override_path] if override_path
|
32
|
+
[WIN_SYSNATIVE_PATH, WIN_SYS32_PATH, WIN_SYSWOW64_PATH]
|
33
|
+
end
|
34
|
+
|
35
|
+
def real_path
|
36
|
+
cand = path_candidates
|
37
|
+
UiHelpers.debug(@ui, "Candidates: #{cand.inspect}")
|
38
|
+
found = cand.find { |p| File.exist?(p) } || cand.first
|
39
|
+
UiHelpers.debug(@ui, "Selected path: #{found}")
|
40
|
+
found
|
41
|
+
end
|
42
|
+
|
43
|
+
def printable_path
|
44
|
+
p = real_path
|
45
|
+
Gem.win_platform? ? p.tr("/", "\\") : p
|
46
|
+
end
|
47
|
+
|
48
|
+
def block_name
|
49
|
+
"vagrant-docker-hosts-manager #{@owner_id}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def block_markers
|
53
|
+
start = "# >>> #{block_name} (managed) >>>"
|
54
|
+
stop = "# <<< #{block_name} (managed) <<<"
|
55
|
+
[start, stop]
|
56
|
+
end
|
57
|
+
|
58
|
+
def detect_newline(str)
|
59
|
+
return "\r\n" if Gem.win_platform?
|
60
|
+
str.include?("\r\n") ? "\r\n" : "\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
def compose_block(entries, newline: "\n")
|
64
|
+
start, stop = block_markers
|
65
|
+
ts = (Time.now.utc.iso8601 rescue Time.now.utc.to_s)
|
66
|
+
|
67
|
+
header = [
|
68
|
+
start,
|
69
|
+
"# Managed by Vagrant - do not edit manually",
|
70
|
+
"# Timestamp: #{ts}"
|
71
|
+
]
|
72
|
+
body = entries.map { |d, ip| "#{ip} #{d}" }
|
73
|
+
|
74
|
+
(header + body + [stop]).join(newline) + newline
|
75
|
+
end
|
76
|
+
|
77
|
+
def pairs_to_hash(pairs)
|
78
|
+
h = {}
|
79
|
+
pairs.each do |ip, fqdn, _owner|
|
80
|
+
ip = ip.to_s.strip
|
81
|
+
fqdn = fqdn.to_s.strip
|
82
|
+
next if ip.empty? || fqdn.empty?
|
83
|
+
h[fqdn] = ip
|
84
|
+
end
|
85
|
+
h
|
86
|
+
end
|
87
|
+
|
88
|
+
def normalize_entries(entries)
|
89
|
+
entries
|
90
|
+
.each_with_object({}) { |(d, ip), h| h[d.to_s.strip] = ip.to_s.strip }
|
91
|
+
.reject { |d, ip| d.empty? || ip.empty? }
|
92
|
+
.sort_by { |d, _ip| d }
|
93
|
+
.to_h
|
94
|
+
end
|
95
|
+
|
96
|
+
def ensure_trailing_newline(str, nl)
|
97
|
+
return "" if str.nil? || str.empty?
|
98
|
+
str.end_with?(nl) ? str : (str + nl)
|
99
|
+
end
|
100
|
+
|
101
|
+
def apply(entries)
|
102
|
+
entries = normalize_entries(entries)
|
103
|
+
if entries.empty?
|
104
|
+
UiHelpers.say(@ui, "#{UiHelpers.e(:info)} " +
|
105
|
+
::I18n.t("messages.no_entries", default: "No hosts entries configured."))
|
106
|
+
return 0
|
107
|
+
end
|
108
|
+
|
109
|
+
base = read
|
110
|
+
newline = detect_newline(base)
|
111
|
+
cleaned = remove_block_from(base)
|
112
|
+
cleaned = ensure_trailing_newline(cleaned, newline)
|
113
|
+
|
114
|
+
existing_map = pairs_to_hash(list_pairs(:current))
|
115
|
+
|
116
|
+
added = {}
|
117
|
+
updated = {}
|
118
|
+
unchanged = {}
|
119
|
+
|
120
|
+
entries.each do |fqdn, ip|
|
121
|
+
prev = existing_map[fqdn]
|
122
|
+
if prev.nil?
|
123
|
+
added[fqdn] = ip
|
124
|
+
existing_map[fqdn] = ip
|
125
|
+
elsif prev == ip
|
126
|
+
unchanged[fqdn] = ip
|
127
|
+
else
|
128
|
+
updated[fqdn] = { from: prev, to: ip }
|
129
|
+
existing_map[fqdn] = ip
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
if added.empty? && updated.empty?
|
134
|
+
UiHelpers.say(@ui, "#{UiHelpers.e(:info)} " +
|
135
|
+
::I18n.t("messages.no_change", default: "Nothing to apply. Already up-to-date."))
|
136
|
+
return existing_map.size
|
137
|
+
end
|
138
|
+
|
139
|
+
merged = normalize_entries(existing_map)
|
140
|
+
content = cleaned + compose_block(merged, newline: newline)
|
141
|
+
write(content)
|
142
|
+
|
143
|
+
UiHelpers.say(@ui, "#{UiHelpers.e(:success)} " +
|
144
|
+
::I18n.t("messages.applied", default: "Hosts entries applied."))
|
145
|
+
UiHelpers.say(@ui, "#{UiHelpers.e(:info)} " +
|
146
|
+
::I18n.t("messages.apply_summary",
|
147
|
+
default: "Added: %{a}, Updated: %{u}, Unchanged: %{s}",
|
148
|
+
a: added.size, u: updated.size, s: unchanged.size))
|
149
|
+
merged.size
|
150
|
+
end
|
151
|
+
|
152
|
+
def remove_entries!(ips: [], domains: [])
|
153
|
+
ips = Array(ips).map(&:to_s).reject(&:empty?)
|
154
|
+
domains = Array(domains).map(&:to_s).reject(&:empty?)
|
155
|
+
return (remove! ? 1 : 0) if ips.empty? && domains.empty?
|
156
|
+
|
157
|
+
pairs = list_pairs(:current)
|
158
|
+
before = pairs.length
|
159
|
+
return 0 if before.zero?
|
160
|
+
|
161
|
+
filtered = pairs.reject do |ip, fqdn, _|
|
162
|
+
ips.include?(ip.to_s) || domains.include?(fqdn.to_s)
|
163
|
+
end
|
164
|
+
|
165
|
+
removed_count = before - filtered.length
|
166
|
+
if removed_count <= 0
|
167
|
+
UiHelpers.say(@ui, "#{UiHelpers.e(:info)} " +
|
168
|
+
::I18n.t("messages.remove_none",
|
169
|
+
default: "No matching entry to remove."))
|
170
|
+
return 0
|
171
|
+
end
|
172
|
+
|
173
|
+
base = read
|
174
|
+
newline = detect_newline(base)
|
175
|
+
cleaned = remove_block_from(base)
|
176
|
+
|
177
|
+
if filtered.empty?
|
178
|
+
write(cleaned)
|
179
|
+
else
|
180
|
+
cleaned = ensure_trailing_newline(cleaned, newline)
|
181
|
+
remaining_map = normalize_entries(pairs_to_hash(filtered))
|
182
|
+
content = cleaned + compose_block(remaining_map, newline: newline)
|
183
|
+
write(content)
|
184
|
+
end
|
185
|
+
|
186
|
+
UiHelpers.say(
|
187
|
+
@ui,
|
188
|
+
"#{UiHelpers.e(:broom)} " +
|
189
|
+
::I18n.t(
|
190
|
+
"messages.removed_count",
|
191
|
+
default: "%{count} entries removed.",
|
192
|
+
count: removed_count
|
193
|
+
)
|
194
|
+
)
|
195
|
+
removed_count
|
196
|
+
end
|
197
|
+
|
198
|
+
def remove!
|
199
|
+
content = read
|
200
|
+
newc = remove_block_from(content)
|
201
|
+
removed = (newc != content)
|
202
|
+
write(newc) if removed
|
203
|
+
|
204
|
+
UiHelpers.say(@ui, removed ?
|
205
|
+
"#{UiHelpers.e(:broom)} " + ::I18n.t("messages.cleaned", default: "Managed hosts entries removed.") :
|
206
|
+
"#{UiHelpers.e(:info)} " + ::I18n.t("messages.nothing_to_clean", default: "Nothing to clean."))
|
207
|
+
removed
|
208
|
+
end
|
209
|
+
|
210
|
+
def read
|
211
|
+
pth = real_path
|
212
|
+
UiHelpers.debug(@ui, "read(#{pth})")
|
213
|
+
|
214
|
+
data = nil
|
215
|
+
|
216
|
+
begin
|
217
|
+
data = File.binread(pth)
|
218
|
+
UiHelpers.debug(@ui, "File.binread ok, bytes=#{data.bytesize}, encoding=#{data.encoding}")
|
219
|
+
rescue => e
|
220
|
+
UiHelpers.debug(@ui, "File.binread error: #{e.class}: #{e.message}")
|
221
|
+
data = nil
|
222
|
+
end
|
223
|
+
|
224
|
+
if (data.nil? || data.empty?) && Gem.win_platform?
|
225
|
+
ps_path = pth.gsub("'", "''")
|
226
|
+
ps_cmd = "Get-Content -LiteralPath '#{ps_path}' -Raw"
|
227
|
+
out, err, st = Open3.capture3("powershell", "-NoProfile", "-NonInteractive", "-Command", ps_cmd)
|
228
|
+
if st.success?
|
229
|
+
data = out
|
230
|
+
UiHelpers.debug(@ui, "Fallback PS read ok, bytes=#{data.bytesize}, encoding=#{data.encoding}")
|
231
|
+
else
|
232
|
+
UiHelpers.debug(@ui, "Fallback PS read failed: #{err}")
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
return "" if data.nil?
|
237
|
+
|
238
|
+
data = data.dup
|
239
|
+
data.force_encoding(Encoding::BINARY)
|
240
|
+
|
241
|
+
if data.start_with?("\xEF\xBB\xBF".b)
|
242
|
+
UiHelpers.debug(@ui, "BOM detected, stripping")
|
243
|
+
data = data.byteslice(3, data.bytesize - 3) || "".b
|
244
|
+
end
|
245
|
+
|
246
|
+
begin
|
247
|
+
data = data.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "")
|
248
|
+
UiHelpers.debug(@ui, "Transcoded to UTF-8 (direct), encoding=#{data.encoding}")
|
249
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
|
250
|
+
UiHelpers.debug(@ui, "Direct UTF-8 encode failed: #{e.class}: #{e.message}, trying Windows-1252")
|
251
|
+
begin
|
252
|
+
data = data
|
253
|
+
.force_encoding("Windows-1252")
|
254
|
+
.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "")
|
255
|
+
UiHelpers.debug(@ui, "Transcoded via Windows-1252 -> UTF-8, encoding=#{data.encoding}")
|
256
|
+
rescue => e2
|
257
|
+
UiHelpers.debug(@ui, "Windows-1252 fallback failed: #{e2.class}: #{e2.message}")
|
258
|
+
data = data.force_encoding(Encoding::UTF_8)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
if UiHelpers.debug_enabled?
|
263
|
+
head = data.lines.first(8) rescue []
|
264
|
+
UiHelpers.debug(@ui, "Head(8):\n" + head.join)
|
265
|
+
end
|
266
|
+
|
267
|
+
data
|
268
|
+
rescue => e
|
269
|
+
UiHelpers.debug(@ui, "read() fatal: #{e.class}: #{e.message}")
|
270
|
+
""
|
271
|
+
end
|
272
|
+
|
273
|
+
def list_pairs(scope = :all)
|
274
|
+
content = read
|
275
|
+
return [] if content.to_s.empty?
|
276
|
+
|
277
|
+
start_re = /^\s*#\s*>>>\s*vagrant-docker-hosts-manager\s+(.+?)\s*\(managed\)\s*>>>\s*$/i
|
278
|
+
stop_re = /^\s*#\s*<<<\s*vagrant-docker-hosts-manager\s+(.+?)\s*\(managed\)\s*<<<\s*$/i
|
279
|
+
ip_re = /^\s*(\d{1,3}(?:\.\d{1,3}){3})\s+([^\s#]+)/
|
280
|
+
|
281
|
+
out = []
|
282
|
+
in_block = false
|
283
|
+
owner = nil
|
284
|
+
|
285
|
+
content.each_line.with_index(1) do |raw, idx|
|
286
|
+
line = raw.delete_suffix("\n").delete_suffix("\r")
|
287
|
+
|
288
|
+
if (m = line.match(start_re))
|
289
|
+
in_block = true
|
290
|
+
owner = m[1].to_s.strip
|
291
|
+
UiHelpers.debug(@ui, "start block(owner=#{owner}) at line #{idx}")
|
292
|
+
next
|
293
|
+
end
|
294
|
+
|
295
|
+
if (m = line.match(stop_re))
|
296
|
+
UiHelpers.debug(@ui, "stop block(owner=#{owner}) at line #{idx}") if in_block
|
297
|
+
in_block = false
|
298
|
+
owner = nil
|
299
|
+
next
|
300
|
+
end
|
301
|
+
|
302
|
+
next unless in_block
|
303
|
+
next unless scope == :all || owner == @owner_id
|
304
|
+
|
305
|
+
if (m = line.match(ip_re))
|
306
|
+
ip, fqdn = m[1], m[2]
|
307
|
+
out << [ip, fqdn, owner]
|
308
|
+
UiHelpers.debug(@ui, " ip line: #{ip} #{fqdn} (owner=#{owner})")
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
UiHelpers.debug(@ui, "list_pairs found #{out.length} pair(s)")
|
313
|
+
out
|
314
|
+
end
|
315
|
+
|
316
|
+
def entries_in_blocks(scope = :current)
|
317
|
+
content = read
|
318
|
+
return {} if content.to_s.empty?
|
319
|
+
|
320
|
+
start_prefix = "# >>> vagrant-docker-hosts-manager "
|
321
|
+
stop_prefix = "# <<< vagrant-docker-hosts-manager "
|
322
|
+
|
323
|
+
in_block = false
|
324
|
+
owner = nil
|
325
|
+
out = {}
|
326
|
+
|
327
|
+
content.each_line do |raw|
|
328
|
+
line = raw.sub(/\r?\n\z/, "")
|
329
|
+
lstr = line.lstrip
|
330
|
+
|
331
|
+
if lstr.start_with?(start_prefix)
|
332
|
+
in_block = true
|
333
|
+
tail = lstr[start_prefix.length..-1].to_s
|
334
|
+
owner = tail.split(" (managed)").first.to_s.strip
|
335
|
+
next
|
336
|
+
end
|
337
|
+
|
338
|
+
if lstr.start_with?(stop_prefix)
|
339
|
+
in_block = false
|
340
|
+
owner = nil
|
341
|
+
next
|
342
|
+
end
|
343
|
+
|
344
|
+
next unless in_block
|
345
|
+
next unless scope == :all || owner == @owner_id
|
346
|
+
|
347
|
+
if lstr =~ /\A\s*(\d{1,3}(?:\.\d{1,3}){3})\s+([^\s#]+)/
|
348
|
+
ip, fqdn = $1, $2
|
349
|
+
if out.key?(fqdn)
|
350
|
+
arr = out[fqdn].is_a?(Array) ? out[fqdn] : [out[fqdn]]
|
351
|
+
arr << ip unless arr.include?(ip)
|
352
|
+
out[fqdn] = arr
|
353
|
+
else
|
354
|
+
out[fqdn] = [ip]
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
out
|
360
|
+
end
|
361
|
+
|
362
|
+
def managed_blocks_dump(scope = :all)
|
363
|
+
content = read
|
364
|
+
return "" if content.to_s.empty?
|
365
|
+
|
366
|
+
start_re = /^\s*#\s*>>>\s*vagrant-docker-hosts-manager\s+(.+?)\s*\(managed\)\s*>>>\s*$/i
|
367
|
+
stop_re = /^\s*#\s*<<<\s*vagrant-docker-hosts-manager\s+(.+?)\s*\(managed\)\s*<<<\s*$/i
|
368
|
+
|
369
|
+
buff = []
|
370
|
+
cur = []
|
371
|
+
in_block = false
|
372
|
+
owner = nil
|
373
|
+
|
374
|
+
content.each_line do |raw|
|
375
|
+
line = raw.delete_suffix("\n").delete_suffix("\r")
|
376
|
+
|
377
|
+
if (m = line.match(start_re))
|
378
|
+
in_block = true
|
379
|
+
owner = m[1].to_s.strip
|
380
|
+
cur = [line]
|
381
|
+
next
|
382
|
+
end
|
383
|
+
|
384
|
+
if in_block
|
385
|
+
cur << line
|
386
|
+
if line.match?(stop_re)
|
387
|
+
if scope == :all || owner == @owner_id
|
388
|
+
buff << cur.join("\n")
|
389
|
+
end
|
390
|
+
in_block = false
|
391
|
+
owner = nil
|
392
|
+
cur = []
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
buff.join("\n\n\n")
|
398
|
+
end
|
399
|
+
|
400
|
+
def current_entries
|
401
|
+
entries_in_blocks(:current)
|
402
|
+
end
|
403
|
+
|
404
|
+
def remove_block_from(content)
|
405
|
+
start, stop = block_markers
|
406
|
+
removing = false
|
407
|
+
content.lines.reject do |line|
|
408
|
+
if line.start_with?(start)
|
409
|
+
removing = true
|
410
|
+
true
|
411
|
+
elsif line.start_with?(stop)
|
412
|
+
removing = false
|
413
|
+
true
|
414
|
+
else
|
415
|
+
removing
|
416
|
+
end
|
417
|
+
end.join
|
418
|
+
end
|
419
|
+
|
420
|
+
def elevated?
|
421
|
+
if Gem.win_platform?
|
422
|
+
cmd = %q{
|
423
|
+
(New-Object Security.Principal.WindowsPrincipal(
|
424
|
+
[Security.Principal.WindowsIdentity]::GetCurrent()
|
425
|
+
)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
426
|
+
}.strip
|
427
|
+
out, _err, st = Open3.capture3("powershell", "-NoProfile", "-NonInteractive", "-Command", cmd)
|
428
|
+
st.success? && out.to_s.strip.downcase == "true"
|
429
|
+
else
|
430
|
+
begin
|
431
|
+
Process.euid == 0
|
432
|
+
rescue
|
433
|
+
false
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
def write(content)
|
439
|
+
Gem.win_platform? ? write_windows(content) : write_posix(content)
|
440
|
+
end
|
441
|
+
|
442
|
+
def write_posix(content)
|
443
|
+
File.binwrite(real_path, content)
|
444
|
+
rescue Errno::EACCES
|
445
|
+
tf = Tempfile.new("vdhm-hosts")
|
446
|
+
begin
|
447
|
+
tf.binmode
|
448
|
+
tf.write(content); tf.flush
|
449
|
+
system("sudo cp #{tf.path} #{real_path}") || raise("sudo copy failed")
|
450
|
+
ensure
|
451
|
+
tf.close!
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
def write_windows(content)
|
456
|
+
tf = Tempfile.new("vdhm-hosts")
|
457
|
+
begin
|
458
|
+
tf.binmode
|
459
|
+
tf.write(content); tf.flush
|
460
|
+
ps = <<~POW
|
461
|
+
$ErrorActionPreference = "Stop"
|
462
|
+
Copy-Item -LiteralPath '#{tf.path.gsub("'", "''")}' -Destination '#{real_path.gsub("'", "''")}' -Force
|
463
|
+
POW
|
464
|
+
system(%{powershell -NoProfile -NonInteractive -Command #{Util::Docker.shell_escape(ps)}}) ||
|
465
|
+
system(
|
466
|
+
%{
|
467
|
+
powershell -NoProfile -NonInteractive -Command
|
468
|
+
"Start-Process PowerShell -Verb RunAs -ArgumentList #{Util::Docker.shell_escape(ps)}"
|
469
|
+
}.strip
|
470
|
+
) || raise("elevated copy failed")
|
471
|
+
ensure
|
472
|
+
tf.close!
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "i18n"
|
4
|
+
|
5
|
+
module VagrantDockerHostsManager
|
6
|
+
module Util
|
7
|
+
module I18n
|
8
|
+
SUPPORTED = [:en, :fr].freeze
|
9
|
+
@json = false
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def setup!(env, forced: nil)
|
14
|
+
::I18n.enforce_available_locales = false
|
15
|
+
|
16
|
+
gem_root = File.expand_path("../..", __dir__)
|
17
|
+
paths = Dir[File.join(gem_root, "locales", "*.yml")]
|
18
|
+
::I18n.load_path |= paths
|
19
|
+
::I18n.available_locales = SUPPORTED
|
20
|
+
::I18n.backend.load_translations
|
21
|
+
|
22
|
+
lang = forced || ENV["VDHM_LANG"] || ENV["LANG"] || "en"
|
23
|
+
sym = lang.to_s[0, 2].downcase.to_sym
|
24
|
+
::I18n.locale = SUPPORTED.include?(sym) ? sym : :en
|
25
|
+
|
26
|
+
begin
|
27
|
+
if env[:ui] && env[:machine] && env[:machine].config.docker_hosts.respond_to?(:verbose) &&
|
28
|
+
env[:machine].config.docker_hosts.verbose
|
29
|
+
env[:ui].info(::I18n.t("messages.lang_set", lang: ::I18n.locale))
|
30
|
+
end
|
31
|
+
rescue StandardError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_json_mode(v) = (@json = !!v)
|
36
|
+
def json? = !!@json
|
37
|
+
def env_flag(name) = ENV[name].to_s == "1"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/locales/en.yml
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
en:
|
2
|
+
help:
|
3
|
+
title: "Vagrant Docker Hosts Manager"
|
4
|
+
usage: "Usage: vagrant hosts <apply|remove|view|help|version> [options]"
|
5
|
+
commands_header: "Commands:"
|
6
|
+
options_header: "Options:"
|
7
|
+
topics_header: "Help topics:"
|
8
|
+
topic_header: "Help: vagrant hosts %{topic}"
|
9
|
+
usage_label: "Usage:"
|
10
|
+
description_label: "Description:"
|
11
|
+
options_label: "Options:"
|
12
|
+
examples_label: "Examples:"
|
13
|
+
commands:
|
14
|
+
apply: "apply [IP] [DOMAIN] Add/update entries in hosts file"
|
15
|
+
remove: "remove [IP|DOMAIN] Remove entries or the whole managed block"
|
16
|
+
view: "view [DOMAIN] Show planned/managed hosts entries"
|
17
|
+
version: "version Display plugin version"
|
18
|
+
help: "help [DOMAIN] Show help or subcommands help"
|
19
|
+
options:
|
20
|
+
json: "--json Machine-readable JSON output"
|
21
|
+
lang: "--lang LANG Force language (en|fr)"
|
22
|
+
no_emoji: "--no-emoji Disable emoji in CLI output"
|
23
|
+
help: "-h, --help Show help"
|
24
|
+
topic:
|
25
|
+
apply:
|
26
|
+
title: "apply"
|
27
|
+
usage: "vagrant hosts apply <IP> <DOMAIN>"
|
28
|
+
description: >
|
29
|
+
Add or update the given mapping in the managed block for the target VM.
|
30
|
+
If no mapping is given, values from the Vagrantfile (docker_hosts) are used.
|
31
|
+
The command validates both parameters: IPv4 (e.g. 172.28.100.2) and DOMAIN (e.g. example.test).
|
32
|
+
options:
|
33
|
+
json: "--json Output machine-readable JSON"
|
34
|
+
dryrun: "--dry-run Do not modify hosts file"
|
35
|
+
lang: "--lang <en|fr> Force language"
|
36
|
+
noemoji: "--no-emoji Disable emoji in CLI output"
|
37
|
+
examples:
|
38
|
+
- "vagrant hosts apply 172.28.100.2 flowfind.noesi.local"
|
39
|
+
- "vagrant hosts apply 172.28.100.3 flowfind3.noesi.local default"
|
40
|
+
remove:
|
41
|
+
title: "remove"
|
42
|
+
usage: "vagrant hosts remove [<IP>|<DOMAIN>]"
|
43
|
+
description: >
|
44
|
+
Remove one mapping by IP or DOMAIN. If no parameter is provided, you will be asked
|
45
|
+
to confirm removing ALL managed entries for the VM (prompt: yes/No).
|
46
|
+
The parameter format is validated (IPv4 or DOMAIN).
|
47
|
+
options:
|
48
|
+
json: "--json Output machine-readable JSON"
|
49
|
+
dryrun: "--dry-run Do not modify hosts file"
|
50
|
+
lang: "--lang <en|fr> Force language"
|
51
|
+
noemoji: "--no-emoji Disable emoji in CLI output"
|
52
|
+
examples:
|
53
|
+
- "vagrant hosts remove 172.28.100.2"
|
54
|
+
- "vagrant hosts remove flowfind.noesi.local"
|
55
|
+
- "vagrant hosts remove # will ask for confirmation to remove ALL"
|
56
|
+
view:
|
57
|
+
title: "view"
|
58
|
+
usage: "vagrant hosts view"
|
59
|
+
description: >
|
60
|
+
Display planned entries (from Vagrantfile) and managed entries currently present
|
61
|
+
in the hosts file. Shows unique pairs IP → DOMAIN.
|
62
|
+
options:
|
63
|
+
json: "--json Output machine-readable JSON"
|
64
|
+
dryrun: "--dry-run Do not modify hosts file"
|
65
|
+
lang: "--lang <en|fr> Force language"
|
66
|
+
noemoji: "--no-emoji Disable emoji in CLI output"
|
67
|
+
examples:
|
68
|
+
- "vagrant hosts view"
|
69
|
+
- "vagrant hosts view default"
|
70
|
+
version:
|
71
|
+
title: "version"
|
72
|
+
usage: "vagrant hosts version"
|
73
|
+
description: "Print the plugin version."
|
74
|
+
options: {}
|
75
|
+
examples:
|
76
|
+
- "vagrant hosts version"
|
77
|
+
help:
|
78
|
+
title: "help"
|
79
|
+
usage: "vagrant hosts help [apply|remove|view|version|help]"
|
80
|
+
description: "Show general help or detailed help for a specific subcommand."
|
81
|
+
options: {}
|
82
|
+
examples:
|
83
|
+
- "vagrant hosts help"
|
84
|
+
- "vagrant hosts help apply"
|
85
|
+
|
86
|
+
messages:
|
87
|
+
no_entries: "No hosts entries configured."
|
88
|
+
applied: "Hosts entries applied."
|
89
|
+
removed_count: "%{count} entries removed."
|
90
|
+
cleaned: "Managed hosts entries removed."
|
91
|
+
nothing_to_clean: "Nothing to clean."
|
92
|
+
missing_ip_for: "No IP configured for %{domain}; skipping."
|
93
|
+
detected_ip: "Detected IP for %{domain}: %{ip}."
|
94
|
+
no_ip_found: "No IP found for %{domain} (container: %{container})."
|
95
|
+
lang_set: "Language set to %{lang}."
|
96
|
+
no_change: "Nothing to apply. Already up-to-date."
|
97
|
+
invalid_ip: "Invalid IPv4 address: %{ip}"
|
98
|
+
invalid_host: "Invalid host/DOMAIN: %{host}"
|
99
|
+
missing_mapping: "Provide both IP and DOMAIN (e.g. `vagrant hosts apply 1.2.3.4 example.test`)."
|
100
|
+
invalid_key: "Invalid IP or DOMAIN: %{key}"
|
101
|
+
view_header: "Planned hosts entries:"
|
102
|
+
view_managed_header: "Managed hosts entries:"
|
103
|
+
apply_summary: "Added: %{a}, Updated: %{u}, Unchanged: %{s}"
|
104
|
+
remove_expected_format: "Expected IPv4 (e.g. 172.28.100.2) or DOMAIN (e.g. example.test)."
|
105
|
+
confirm_remove_all: "This will remove ALL managed hosts entries for %{vm}. Continue? (yes/NO)"
|
106
|
+
remove_all_cancelled: "Cancelled. Nothing removed."
|
107
|
+
remove_none: "No matching entry to remove."
|
108
|
+
|
109
|
+
errors:
|
110
|
+
no_machine: "No target machine found. Run this inside a Vagrant project or pass a VM name."
|
111
|
+
not_admin: "Administrator/root privileges required to modify hosts at %{path}."
|
112
|
+
|
113
|
+
log:
|
114
|
+
version_line: "vagrant-docker-hosts-manager version %{version}"
|