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.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantDockerHostsManager
4
+ class Config < Vagrant.plugin("2", :config)
5
+ attr_accessor :domains
6
+
7
+ attr_accessor :domain
8
+
9
+ attr_accessor :container_name
10
+
11
+ attr_accessor :ip
12
+
13
+ attr_accessor :verbose
14
+
15
+ def initialize
16
+ @domains = {}
17
+ @domain = nil
18
+ @container_name = nil
19
+ @ip = nil
20
+ @verbose = false
21
+ end
22
+
23
+ def finalize!; end
24
+
25
+ def validate(_machine)
26
+ errors = []
27
+ if (@domains.nil? || @domains.empty?) && (@domain.nil? || @domain.strip.empty?)
28
+ errors << "You must configure at least one domain: " \
29
+ "`config.docker_hosts.domain = \"example.test\"` or set " \
30
+ "`config.docker_hosts.domains = {\"example.test\" => \"172.28.0.10\"}`"
31
+ end
32
+
33
+ unless @domains.is_a?(Hash)
34
+ errors << "`domains` must be a Hash of { \\\"domain\\\" => \\\"ip\\\" }"
35
+ end
36
+
37
+ if @ip && !@ip.to_s.match?(/\A\d{1,3}(\.\d{1,3}){3}\z/)
38
+ errors << "`ip` must be IPv4 like 172.28.0.10"
39
+ end
40
+
41
+ { "vagrant-docker-hosts-manager" => errors }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "i18n"
4
+
5
+ module VagrantDockerHostsManager
6
+ module UiHelpers
7
+ EMOJI = {
8
+ success: "โœ…",
9
+ info: "๐Ÿ”",
10
+ ongoing: "๐Ÿ”",
11
+ warning: "โš ๏ธ",
12
+ error: "โŒ",
13
+ version: "๐Ÿ’พ",
14
+ broom: "๐Ÿงน",
15
+ question: "โ“",
16
+ bug: "๐Ÿ›"
17
+ }.freeze
18
+
19
+ module_function
20
+
21
+ def e(key, no_emoji: false)
22
+ return "" if no_emoji || ENV["VDHM_NO_EMOJI"] == "1"
23
+ EMOJI[key] || ""
24
+ end
25
+
26
+ def debug_enabled?
27
+ ENV["VDHM_DEBUG"].to_s == "1"
28
+ end
29
+
30
+ def t(key, **opts) = ::I18n.t(key, **opts)
31
+ def exists?(key) = ::I18n.exists?(key, ::I18n.locale)
32
+
33
+ def say(ui, msg) = (ui&.info(msg) || puts(msg))
34
+ def warn(ui, msg) = (ui&.warn(msg) || puts(msg))
35
+ def error(ui, msg) = (ui&.error(msg) || warn(ui, msg))
36
+
37
+ def debug(ui, msg)
38
+ return unless debug_enabled?
39
+ say(ui, "#{e(:bug)} #{msg}")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "vagrant"
4
+ require "i18n"
5
+
6
+ require_relative "version"
7
+ require_relative "config"
8
+ require_relative "command"
9
+ require_relative "helpers"
10
+ require_relative "util/hosts_file"
11
+ require_relative "util/docker"
12
+ require_relative "util/json"
13
+ require_relative "util/i18n"
14
+
15
+ begin
16
+ I18n.enforce_available_locales = false
17
+ gem_root = File.expand_path("../..", __dir__)
18
+ I18n.load_path |= Dir[File.join(gem_root, "locales", "*.yml")]
19
+ I18n.backend.load_translations
20
+ rescue StandardError
21
+ end
22
+
23
+ module VagrantDockerHostsManager
24
+ class Plugin < Vagrant.plugin("2")
25
+ name "vagrant-docker-hosts-manager"
26
+
27
+ description <<~DESC
28
+ Manage /etc/hosts (or Windows hosts) entries for Docker/Vagrant projects safely,
29
+ with ownership markers, CLI, JSON output, and lifecycle hooks.
30
+ DESC
31
+
32
+ config(:docker_hosts) do
33
+ Config
34
+ end
35
+
36
+ command("hosts") do
37
+ Command
38
+ end
39
+
40
+ [:machine_action_up, :machine_action_provision, :machine_action_reload].each do |hook_name|
41
+ action_hook(:vdhm_apply, hook_name) do |hook|
42
+ hook.after(Vagrant::Action::Builtin::Provision, Action::Apply)
43
+ hook.append(Action::Apply)
44
+ end
45
+ end
46
+
47
+ action_hook(:vdhm_cleanup, :machine_action_destroy) do |hook|
48
+ hook.prepend(Action::Cleanup)
49
+ end
50
+ end
51
+
52
+ module Action
53
+ class Apply
54
+ def initialize(app, env) = (@app = app)
55
+
56
+ def call(env)
57
+ Util::I18n.setup!(env)
58
+ cfg = env[:machine].config.docker_hosts
59
+ mid = env[:machine].id || "unknown"
60
+ dry = Util::I18n.env_flag("VDHM_DRY_RUN")
61
+ ui = env[:ui]
62
+ hoster = Util::HostsFile.new(env, owner_id: mid)
63
+
64
+ entries = compute_entries(env, cfg, ui)
65
+ if entries.empty?
66
+ ui.info(::I18n.t("messages.no_entries"))
67
+ return @app.call(env)
68
+ end
69
+
70
+ if dry
71
+ Util::Json.emit(action: "apply", status: "dry-run", data: { owner: mid, entries: entries })
72
+ return @app.call(env)
73
+ end
74
+
75
+ hoster.apply(entries)
76
+ Util::Json.emit(action: "apply", status: "success", data: { owner: mid, entries: entries })
77
+ rescue => e
78
+ Util::Json.emit(action: "apply", status: "error", error: e.message, backtrace: e.backtrace&.first(3))
79
+ ui&.error("VDHM: #{e.message}") || puts("VDHM: #{e.message}")
80
+ ensure
81
+ @app.call(env)
82
+ end
83
+
84
+ private
85
+
86
+ def compute_entries(env, cfg, ui)
87
+ entries = {}
88
+
89
+ cfg.domains.each do |domain, ip|
90
+ next if domain.to_s.strip.empty?
91
+ if ip.nil? || ip.to_s.strip.empty?
92
+ ui&.warn(::I18n.t("messages.missing_ip_for", domain: domain))
93
+ next
94
+ end
95
+ entries[domain] = ip
96
+ end
97
+
98
+ if cfg.domain && !cfg.domain.strip.empty?
99
+ ip = cfg.ip || begin
100
+ if cfg.container_name && !cfg.container_name.strip.empty?
101
+ Util::Docker.ip_for_container(cfg.container_name)
102
+ end
103
+ end
104
+ if ip && !ip.strip.empty?
105
+ ui&.info(::I18n.t("messages.detected_ip", domain: cfg.domain, ip: ip))
106
+ entries[cfg.domain] = ip
107
+ else
108
+ ui&.warn(::I18n.t("messages.no_ip_found", domain: cfg.domain, container: cfg.container_name))
109
+ end
110
+ end
111
+
112
+ entries
113
+ end
114
+ end
115
+
116
+ class Cleanup
117
+ def initialize(app, env) = (@app = app)
118
+
119
+ def call(env)
120
+ Util::I18n.setup!(env)
121
+ mid = env[:machine].id || "unknown"
122
+ dry = Util::I18n.env_flag("VDHM_DRY_RUN")
123
+ ui = env[:ui]
124
+ hoster = Util::HostsFile.new(env, owner_id: mid)
125
+
126
+ if dry
127
+ Util::Json.emit(action: "cleanup", status: "dry-run", data: { owner: mid })
128
+ return @app.call(env)
129
+ end
130
+
131
+ removed = hoster.remove!
132
+ Util::Json.emit(action: "cleanup", status: "success", data: { owner: mid, removed: removed })
133
+ ui.info(::I18n.t("messages.cleaned"))
134
+ rescue => e
135
+ Util::Json.emit(action: "cleanup", status: "error", error: e.message)
136
+ ui.error("VDHM: #{e.message}")
137
+ ensure
138
+ @app.call(env)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module VagrantDockerHostsManager
6
+ module Util
7
+ module Docker
8
+ module_function
9
+
10
+ def ip_for_container(name)
11
+ return nil if name.to_s.strip.empty?
12
+ cmd = %(docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}" #{shell_escape(name)})
13
+ out, _err, status = Open3.capture3(cmd)
14
+ return nil unless status.success?
15
+ out.split(/\s+/).find { |ip| ip =~ /\A\d{1,3}(\.\d{1,3}){3}\z/ }
16
+ rescue
17
+ nil
18
+ end
19
+
20
+ def shell_escape(str)
21
+ if Gem.win_platform?
22
+ %("#{str.gsub('"', '\"')}")
23
+ else
24
+ %('#{str.gsub("'", "'\\\\''")}')
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end