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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +1 -0
- data/lib/vagrant-docker-hosts-manager/VERSION +1 -1
- data/lib/vagrant-docker-hosts-manager/actions/apply.rb +68 -0
- data/lib/vagrant-docker-hosts-manager/actions/cleanup.rb +34 -0
- data/lib/vagrant-docker-hosts-manager/command.rb +255 -248
- data/lib/vagrant-docker-hosts-manager/config.rb +45 -10
- data/lib/vagrant-docker-hosts-manager/helpers.rb +91 -7
- data/lib/vagrant-docker-hosts-manager/plugin.rb +2 -92
- data/lib/vagrant-docker-hosts-manager/util/docker.rb +10 -11
- data/lib/vagrant-docker-hosts-manager/util/hosts_file.rb +140 -132
- data/lib/vagrant-docker-hosts-manager/util/i18n.rb +20 -12
- data/lib/vagrant-docker-hosts-manager/util/verbose.rb +22 -0
- data/lib/vagrant-docker-hosts-manager/version.rb +7 -5
- data/locales/en.yml +117 -110
- data/locales/fr.yml +118 -111
- metadata +6 -2
|
@@ -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 =
|
|
18
|
-
@container_name =
|
|
19
|
-
@ip =
|
|
20
|
-
@
|
|
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
|
|
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
|
|
27
|
-
|
|
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
|
|
31
|
-
|
|
113
|
+
def error(ui, msg)
|
|
114
|
+
ui&.error(msg) || warn(ui, msg)
|
|
115
|
+
end
|
|
32
116
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|