vagrant-docker-hosts-manager 0.2.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,29 +1,49 @@
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
-
7
18
  attr_accessor :domain
8
-
9
19
  attr_accessor :container_name
10
-
11
20
  attr_accessor :ip
12
-
21
+ attr_accessor :locale
13
22
  attr_accessor :verbose
14
23
 
15
24
  def initialize
16
- @domains = {}
17
- @domain = nil
18
- @container_name = nil
19
- @ip = nil
20
- @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
21
31
  end
22
32
 
23
- 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
24
41
 
25
42
  def validate(_machine)
26
43
  errors = []
44
+
45
+ return { "vagrant-docker-hosts-manager" => errors } unless configured?
46
+
27
47
  if (@domains.nil? || @domains.empty?) && (@domain.nil? || @domain.strip.empty?)
28
48
  errors << "You must configure at least one domain: " \
29
49
  "`config.docker_hosts.domain = \"example.test\"` or set " \
@@ -38,7 +58,22 @@ module VagrantDockerHostsManager
38
58
  errors << "`ip` must be IPv4 like 172.28.0.10"
39
59
  end
40
60
 
61
+ if @locale && !%w[en fr].include?(@locale.to_s[0, 2].downcase)
62
+ errors << "`locale` must be 'en' or 'fr'."
63
+ end
64
+
41
65
  { "vagrant-docker-hosts-manager" => errors }
42
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
43
78
  end
44
79
  end
@@ -4,6 +4,13 @@ require "i18n"
4
4
 
5
5
  module VagrantDockerHostsManager
6
6
  module UiHelpers
7
+ class MissingTranslationError < StandardError; end
8
+ class UnsupportedLocaleError < StandardError; end
9
+
10
+ SUPPORTED = [:en, :fr].freeze
11
+ OUR_NAMESPACES = %w[messages. errors. log. help.].freeze
12
+ NS = "vdhm"
13
+
7
14
  EMOJI = {
8
15
  success: "✅",
9
16
  info: "🔍",
@@ -18,21 +25,98 @@ module VagrantDockerHostsManager
18
25
 
19
26
  module_function
20
27
 
28
+ def setup_i18n!
29
+ return if defined?(@i18n_setup) && @i18n_setup
30
+
31
+ ::I18n.enforce_available_locales = false
32
+
33
+ base = File.expand_path("../../locales", __dir__)
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
38
+ ::I18n.load_path |= paths
39
+ ::I18n.available_locales = SUPPORTED
40
+
41
+ default = ((ENV["VDHM_LANG"] || ENV["LANG"] || "en")[0, 2] rescue "en").to_sym
42
+ ::I18n.default_locale = SUPPORTED.include?(default) ? default : :en
43
+
44
+ ::I18n.backend.load_translations
45
+ @i18n_setup = true
46
+ end
47
+
48
+ def set_locale!(lang)
49
+ setup_i18n!
50
+ sym = lang.to_s[0, 2].downcase.to_sym
51
+ unless SUPPORTED.include?(sym)
52
+ raise UnsupportedLocaleError,
53
+ "#{EMOJI[:error]} Unsupported language: #{sym}. Available: #{SUPPORTED.join(", ")}"
54
+ end
55
+ ::I18n.locale = sym
56
+ ::I18n.backend.load_translations
57
+ end
58
+
59
+ def setup_locale_from_config!(cfg)
60
+ lang = (cfg.respond_to?(:locale) ? cfg.locale : nil) || ENV["VDHM_LANG"]
61
+ return unless lang
62
+ set_locale!(lang)
63
+ rescue UnsupportedLocaleError
64
+ set_locale!("en")
65
+ end
66
+
67
+ def namespaced(key)
68
+ k = key.to_s
69
+ k.start_with?("#{NS}.") ? k : "#{NS}.#{k}"
70
+ end
71
+
72
+ def t(key, **opts)
73
+ setup_i18n!
74
+ ::I18n.t(namespaced(key), **opts)
75
+ end
76
+
77
+ def t!(key, **opts)
78
+ setup_i18n!
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}"
82
+ end
83
+ ::I18n.t(nk, **opts)
84
+ end
85
+
86
+ def t_hash(key)
87
+ setup_i18n!
88
+ v = ::I18n.t(namespaced(key), default: {})
89
+ v.is_a?(Hash) ? v : {}
90
+ end
91
+
92
+ def exists?(key)
93
+ ::I18n.exists?(key, ::I18n.locale)
94
+ end
95
+
96
+ def our_key?(k)
97
+ OUR_NAMESPACES.any? { |ns| k.start_with?(ns) }
98
+ end
99
+
21
100
  def e(key, no_emoji: false)
22
101
  return "" if no_emoji || ENV["VDHM_NO_EMOJI"] == "1"
23
102
  EMOJI[key] || ""
24
103
  end
25
104
 
26
- def debug_enabled?
27
- ENV["VDHM_DEBUG"].to_s == "1"
105
+ def say(ui, msg)
106
+ ui&.info(msg) || puts(msg)
107
+ end
108
+
109
+ def warn(ui, msg)
110
+ ui&.warn(msg) || puts(msg)
28
111
  end
29
112
 
30
- def t(key, **opts) = ::I18n.t(key, **opts)
31
- def exists?(key) = ::I18n.exists?(key, ::I18n.locale)
113
+ def error(ui, msg)
114
+ ui&.error(msg) || warn(ui, msg)
115
+ end
32
116
 
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))
117
+ def debug_enabled?
118
+ ENV["VDHM_DEBUG"].to_s == "1"
119
+ end
36
120
 
37
121
  def debug(ui, msg)
38
122
  return unless debug_enabled?
@@ -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
@@ -39,7 +41,6 @@ module VagrantDockerHostsManager
39
41
 
40
42
  [:machine_action_up, :machine_action_provision, :machine_action_reload].each do |hook_name|
41
43
  action_hook(:vdhm_apply, hook_name) do |hook|
42
- hook.after(Vagrant::Action::Builtin::Provision, Action::Apply)
43
44
  hook.append(Action::Apply)
44
45
  end
45
46
  end
@@ -48,95 +49,4 @@ module VagrantDockerHostsManager
48
49
  hook.prepend(Action::Cleanup)
49
50
  end
50
51
  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
52
  end
@@ -1,29 +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
- cmd = %(docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}" #{shell_escape(name)})
13
- out, _err, status = Open3.capture3(cmd)
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)
14
21
  return nil unless status.success?
15
22
  out.split(/\s+/).find { |ip| ip =~ /\A\d{1,3}(\.\d{1,3}){3}\z/ }
16
- rescue
23
+ rescue StandardError
17
24
  nil
18
25
  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
26
  end
28
27
  end
29
28
  end