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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 90a6a5b049e33dce80821b66d57d4c4ca4edd35d7e51f1125d96f4211adb3eb1
|
4
|
+
data.tar.gz: 634910fe60c21185160b1269bdc6b3cbb77e0d6e6e7df8d5a2cb6eb6115762e1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a1523d6ca1643bac26e2040020b9207a62d6e1b1328b89f56eae7e6d33ad69e6f2c136cee1c2c8e4824388ec126be962ad898107ab08cac66aa310f1a33f7c7b
|
7
|
+
data.tar.gz: da1215b0b930905713b6d9e202035e6523dd1878d9fbdf535a55bbfbc6706200a006c8bc2935ea4c2213efaed23584d7a98a6d5ce31fe7c168cf59ff5e01471a
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## [0.2.0](https://github.com/julienpoirou/vagrant-docker-hosts-manager/compare/v0.1.0...v0.2.0) (2025-08-20)
|
4
|
+
|
5
|
+
|
6
|
+
### Fonctionnalités ✨
|
7
|
+
|
8
|
+
* **init:** V1.0.0 ([32b9d53](https://github.com/julienpoirou/vagrant-docker-hosts-manager/commit/32b9d53654859f07e2f1f55f2acfcb2fd0322a0b))
|
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
===========
|
3
|
+
|
4
|
+
Copyright (c) 2025 Julien Poirou <julienpoirou@protonmail.com>
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the “Software”), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
14
|
+
all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
# vagrant-docker-hosts-manager
|
2
|
+
|
3
|
+
[](https://github.com/julienpoirou/vagrant-docker-hosts-manager/actions/workflows/ci.yml)
|
4
|
+
[](https://github.com/julienpoirou/vagrant-docker-hosts-manager/actions/workflows/codeql.yml)
|
5
|
+
[](https://github.com/julienpoirou/vagrant-docker-hosts-manager/releases)
|
6
|
+
[](https://rubygems.org/gems/vagrant-docker-hosts-manager)
|
7
|
+
[](LICENSE.md)
|
8
|
+
[](https://www.conventionalcommits.org)
|
9
|
+
[](https://renovatebot.com)
|
10
|
+
|
11
|
+
Vagrant plugin to **manage custom hostnames** by updating the local `hosts` file (Windows/Linux/macOS) for Docker‑based environments.
|
12
|
+
|
13
|
+
- Adds/removes host entries on `vagrant up` / `vagrant destroy`
|
14
|
+
- Optional **auto‑detect** of a Docker container IP for a single domain
|
15
|
+
- Safe, idempotent updates: replaces previous entries for configured domains
|
16
|
+
- Works across platforms (uses `sudo` on Unix, UAC elevation on Windows)
|
17
|
+
|
18
|
+
> Requirements: **Vagrant ≥ 2.2**, **Ruby ≥ 3.1**, **Docker (CLI + daemon) for auto‑detect**
|
19
|
+
|
20
|
+
---
|
21
|
+
|
22
|
+
## Table of contents
|
23
|
+
|
24
|
+
- [Why this plugin?](#why-this-plugin)
|
25
|
+
- [Installation](#installation)
|
26
|
+
- [Quick start](#quick-start)
|
27
|
+
- [Vagrantfile configuration](#vagrantfile-configuration)
|
28
|
+
- [CLI usage](#cli-usage)
|
29
|
+
- [How it works](#how-it-works)
|
30
|
+
- [Permissions & OS notes](#permissions--os-notes)
|
31
|
+
- [Troubleshooting](#troubleshooting)
|
32
|
+
- [Contributing & Development](#contributing--development)
|
33
|
+
- [License](#license)
|
34
|
+
|
35
|
+
> 🇫🇷 **Français :** voir [README.fr.md](README.fr.md)
|
36
|
+
|
37
|
+
---
|
38
|
+
|
39
|
+
## Why this plugin?
|
40
|
+
|
41
|
+
When working with Docker + Vagrant, you often need **friendly hostnames** (e.g. `app.local`) that resolve to a container IP. Doing this manually in `/etc/hosts` or `C:\Windows\System32\drivers\etc\hosts` is error‑prone and not portable across teammates.
|
42
|
+
|
43
|
+
This plugin:
|
44
|
+
- **Writes** the required entries during `vagrant up`.
|
45
|
+
- **Cleans** them safely on `vagrant destroy`.
|
46
|
+
- Can **auto‑resolve** a container IP (`docker inspect`) if you only provide one `domain` and a `container_name`.
|
47
|
+
|
48
|
+
---
|
49
|
+
|
50
|
+
## Installation
|
51
|
+
|
52
|
+
From RubyGems (once published):
|
53
|
+
|
54
|
+
```bash
|
55
|
+
vagrant plugin install vagrant-docker-hosts-manager
|
56
|
+
```
|
57
|
+
|
58
|
+
From source (local path):
|
59
|
+
|
60
|
+
```bash
|
61
|
+
git clone https://github.com/julienpoirou/vagrant-docker-hosts-manager
|
62
|
+
cd vagrant-docker-hosts-manager
|
63
|
+
bundle install
|
64
|
+
rake
|
65
|
+
vagrant plugin install . # install from the local gemspec
|
66
|
+
```
|
67
|
+
|
68
|
+
Check it’s available:
|
69
|
+
|
70
|
+
```bash
|
71
|
+
vagrant hosts --help
|
72
|
+
```
|
73
|
+
|
74
|
+
---
|
75
|
+
|
76
|
+
## Quick start
|
77
|
+
|
78
|
+
### Minimal Vagrantfile (auto‑detect single container IP)
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
Vagrant.configure("2") do |config|
|
82
|
+
config.vm.box = "hashicorp/bionic64"
|
83
|
+
|
84
|
+
# Auto-detect the IP of a running Docker container and map it
|
85
|
+
# to a single domain (e.g. http://app.local)
|
86
|
+
config.docker_hosts.domain = "app.local"
|
87
|
+
config.docker_hosts.container_name = "my_app_container" # docker ps --format '{{.Names}}'
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
- On `vagrant up`, the plugin runs
|
92
|
+
`docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" my_app_container`
|
93
|
+
and adds the entry `X.X.X.X app.local` to your hosts file.
|
94
|
+
|
95
|
+
### Multiple static domains
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
Vagrant.configure("2") do |config|
|
99
|
+
config.vm.box = "hashicorp/bionic64"
|
100
|
+
|
101
|
+
# Explicit mappings (no Docker inspect)
|
102
|
+
config.docker_hosts.domains = {
|
103
|
+
"api.local" => "172.28.10.10",
|
104
|
+
"web.local" => "172.28.10.11",
|
105
|
+
"db.local" => "172.28.10.12"
|
106
|
+
}
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
---
|
111
|
+
|
112
|
+
## Vagrantfile configuration
|
113
|
+
|
114
|
+
All options:
|
115
|
+
|
116
|
+
| Key | Type | Default | Notes |
|
117
|
+
|-------------------|--------|----------------------|-------|
|
118
|
+
| `domains` | Hash | `{}` | Map of `domain => ip`. When set, **no auto‑detect** is performed. |
|
119
|
+
| `domain` | String | `nil` | Single domain to map via **auto‑detect** (requires `container_name`). |
|
120
|
+
| `container_name` | String | `"noesi-flowfind"` | Docker container name used for auto‑detect. |
|
121
|
+
|
122
|
+
**Rules**
|
123
|
+
|
124
|
+
- If `domains` is **empty** and `domain` is **set**, the plugin will try to **inspect the container IP** and map it to `domain`.
|
125
|
+
- If `domains` has entries, they are used **as is**; `domain`/`container_name` are ignored.
|
126
|
+
|
127
|
+
---
|
128
|
+
|
129
|
+
## CLI usage
|
130
|
+
|
131
|
+
```
|
132
|
+
vagrant hosts <command>
|
133
|
+
|
134
|
+
Commands:
|
135
|
+
add # Add configured entries to the hosts file
|
136
|
+
remove # Remove configured entries from the hosts file
|
137
|
+
view # Print configured entries (from Vagrantfile)
|
138
|
+
```
|
139
|
+
|
140
|
+
Examples:
|
141
|
+
|
142
|
+
```bash
|
143
|
+
vagrant hosts view
|
144
|
+
vagrant hosts add
|
145
|
+
vagrant hosts remove
|
146
|
+
```
|
147
|
+
|
148
|
+
---
|
149
|
+
|
150
|
+
## How it works
|
151
|
+
|
152
|
+
- **During `vagrant up`**
|
153
|
+
The plugin reads `config.docker_hosts`. If only `domain` is provided, it runs `docker inspect` to get an IP. Then it **removes any previous lines** containing those domains from the hosts file and **appends** new lines (`<ip> <domain>`).
|
154
|
+
|
155
|
+
- **During `vagrant destroy`**
|
156
|
+
The plugin **removes** any hosts lines that contain your configured domain names.
|
157
|
+
|
158
|
+
> The update is **idempotent**: running `add` multiple times won’t duplicate entries for the same domain.
|
159
|
+
|
160
|
+
---
|
161
|
+
|
162
|
+
## Permissions & OS notes
|
163
|
+
|
164
|
+
- **Linux / macOS**: modifying `/etc/hosts` requires privileges. The plugin pipes through `sudo tee -a` when appending, and writes the file when removing. You may be prompted for your password.
|
165
|
+
- **Windows**: the plugin uses **PowerShell elevation** (`Start-Process -Verb RunAs`) when needed to append or rewrite the hosts file.
|
166
|
+
|
167
|
+
> If your shell is already elevated (root/Admin), no prompts appear.
|
168
|
+
|
169
|
+
---
|
170
|
+
|
171
|
+
## Troubleshooting
|
172
|
+
|
173
|
+
- **Container IP empty with auto‑detect**: Ensure the Docker container `container_name` is running and attached to a network with an IP (check `docker inspect` output).
|
174
|
+
- **Permission denied**: Run your terminal as Administrator (Windows) or ensure `sudo` is available (Linux/macOS).
|
175
|
+
- **Entry not removed**: The cleanup matches **by domain substring**. If your hosts line contains extra comments or formatting, make sure the domain string appears on the line.
|
176
|
+
|
177
|
+
---
|
178
|
+
|
179
|
+
## Contributing & Development
|
180
|
+
|
181
|
+
```bash
|
182
|
+
git clone https://github.com/julienpoirou/vagrant-docker-hosts-manager
|
183
|
+
cd vagrant-docker-hosts-manager
|
184
|
+
bundle install
|
185
|
+
rake # runs RSpec
|
186
|
+
```
|
187
|
+
|
188
|
+
- Conventional Commits enforced in PRs.
|
189
|
+
- CI runs RuboCop, tests, and builds the gem.
|
190
|
+
- See `docs/en/CONTRIBUTING.md` and `docs/en/DEVELOPMENT.md` if present.
|
191
|
+
|
192
|
+
---
|
193
|
+
|
194
|
+
## License
|
195
|
+
|
196
|
+
MIT © 2025 [Julien Poirou](mailto:julienpoirou@protonmail.com)
|
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
@@ -0,0 +1,457 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require_relative "helpers"
|
5
|
+
|
6
|
+
module VagrantDockerHostsManager
|
7
|
+
class Command < Vagrant.plugin("2", :command)
|
8
|
+
def execute
|
9
|
+
opts = {
|
10
|
+
json: false,
|
11
|
+
dry: false,
|
12
|
+
lang: nil,
|
13
|
+
no_emoji: false
|
14
|
+
}
|
15
|
+
|
16
|
+
raw_argv = @argv.dup
|
17
|
+
help_topic = begin
|
18
|
+
i = raw_argv.index { |x| x =~ /\A(?:help|--help|-h)\z/ }
|
19
|
+
cand = (i && raw_argv[i + 1]) ? raw_argv[i + 1] : nil
|
20
|
+
(cand && cand !~ /\A-/) ? cand : nil
|
21
|
+
rescue
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
parser = OptionParser.new do |o|
|
26
|
+
o.banner = "Usage: vagrant hosts <apply|remove|view|help|version> [options]"
|
27
|
+
o.on("--json", "Machine-readable JSON output") { opts[:json] = true }
|
28
|
+
o.on("--lang LANG", "Force language (en|fr)") { |v| opts[:lang] = v }
|
29
|
+
o.on("--no-emoji", "Disable emoji in CLI output") { opts[:no_emoji] = true }
|
30
|
+
o.on("-h", "--help", "Show help") { print_help(@env.ui); return 0 }
|
31
|
+
end
|
32
|
+
|
33
|
+
argv = parse_options(parser)
|
34
|
+
return 0 unless argv
|
35
|
+
|
36
|
+
action = argv.shift
|
37
|
+
env = @env
|
38
|
+
|
39
|
+
Util::I18n.setup!(env, forced: opts[:lang])
|
40
|
+
Util::I18n.set_json_mode(opts[:json])
|
41
|
+
ENV["VDHM_DRY_RUN"] = "1" if opts[:dry]
|
42
|
+
ENV["VDHM_NO_EMOJI"] = "1" if opts[:no_emoji]
|
43
|
+
|
44
|
+
case action
|
45
|
+
when nil, "", "help", "--help", "-h"
|
46
|
+
topic = help_topic || argv.shift
|
47
|
+
print_help(@env.ui, topic: topic, no_emoji: opts[:no_emoji])
|
48
|
+
return 0
|
49
|
+
when "version"
|
50
|
+
if opts[:json]
|
51
|
+
Util::Json.emit(action: "version", status: "success", data: { version: VagrantDockerHostsManager::VERSION })
|
52
|
+
else
|
53
|
+
emoji = UiHelpers.e(:version, no_emoji: opts[:no_emoji])
|
54
|
+
line = ::I18n.t("log.version_line",
|
55
|
+
default: "vagrant-docker-hosts-manager version %{version}",
|
56
|
+
version: VagrantDockerHostsManager::VERSION)
|
57
|
+
UiHelpers.say(@env.ui, "#{emoji} #{line}")
|
58
|
+
end
|
59
|
+
return 0
|
60
|
+
end
|
61
|
+
|
62
|
+
machine = nil
|
63
|
+
|
64
|
+
case action
|
65
|
+
when "apply"
|
66
|
+
begin
|
67
|
+
ip_for_apply, host_for_apply =
|
68
|
+
parse_strict_mapping_from_argv!(argv, @env.ui, opts[:no_emoji], json: opts[:json])
|
69
|
+
rescue ArgumentError => e
|
70
|
+
Util::Json.emit(action: "apply", status: "error", error: e.message) if opts[:json]
|
71
|
+
return 1
|
72
|
+
end
|
73
|
+
|
74
|
+
with_target_vms(argv, single_target: true) { |m| machine = m }
|
75
|
+
|
76
|
+
unless machine
|
77
|
+
UiHelpers.error(
|
78
|
+
@env.ui,
|
79
|
+
"#{UiHelpers.e(:error, no_emoji: opts[:no_emoji])} " +
|
80
|
+
::I18n.t(
|
81
|
+
"errors.no_machine",
|
82
|
+
default: "No target machine found. Run this inside a Vagrant project or pass a VM name."
|
83
|
+
)
|
84
|
+
)
|
85
|
+
Util::Json.emit(action: "apply", status: "error", error: "No target machine found") if opts[:json]
|
86
|
+
return 1
|
87
|
+
end
|
88
|
+
|
89
|
+
if ip_for_apply || host_for_apply
|
90
|
+
unless ip_for_apply && host_for_apply
|
91
|
+
UiHelpers.error(@env.ui, "#{UiHelpers.e(:error, no_emoji: opts[:no_emoji])} " +
|
92
|
+
::I18n.t("messages.missing_mapping",
|
93
|
+
default: "Provide both IP and FQDN (e.g. `vagrant hosts apply 1.2.3.4 example.test`)."))
|
94
|
+
return 1
|
95
|
+
end
|
96
|
+
unless ipv4?(ip_for_apply)
|
97
|
+
UiHelpers.error(@env.ui, "#{UiHelpers.e(:error, no_emoji: opts[:no_emoji])} " +
|
98
|
+
::I18n.t("messages.invalid_ip", default: "Invalid IPv4 address: %{ip}", ip: ip_for_apply))
|
99
|
+
return 1
|
100
|
+
end
|
101
|
+
unless fqdn?(host_for_apply)
|
102
|
+
UiHelpers.error(@env.ui, "#{UiHelpers.e(:error, no_emoji: opts[:no_emoji])} " +
|
103
|
+
::I18n.t("messages.invalid_host", default: "Invalid host/FQDN: %{host}", host: host_for_apply))
|
104
|
+
return 1
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
cfg = machine.config.docker_hosts
|
109
|
+
mid = machine.id || "unknown"
|
110
|
+
hoster = Util::HostsFile.new({ machine: machine, ui: @env.ui }, owner_id: mid)
|
111
|
+
|
112
|
+
unless ENV["VDHM_DRY_RUN"] == "1" || hoster.elevated?
|
113
|
+
UiHelpers.error(@env.ui, "#{UiHelpers.e(:error, no_emoji: opts[:no_emoji])} " +
|
114
|
+
::I18n.t("errors.not_admin",
|
115
|
+
default: "Administrator/root privileges required to modify hosts at %{path}.",
|
116
|
+
path: hoster.printable_path))
|
117
|
+
return 1
|
118
|
+
end
|
119
|
+
|
120
|
+
entries = compute_entries(machine, cfg)
|
121
|
+
entries[host_for_apply] = ip_for_apply if ip_for_apply && host_for_apply
|
122
|
+
|
123
|
+
if entries.empty?
|
124
|
+
UiHelpers.say(@env.ui, "#{UiHelpers.e(:info, no_emoji: opts[:no_emoji])} " +
|
125
|
+
::I18n.t("messages.no_entries", default: "No hosts entries configured."))
|
126
|
+
Util::Json.emit(action: "apply", status: "noop", data: { reason: "no entries" })
|
127
|
+
return 0
|
128
|
+
end
|
129
|
+
|
130
|
+
return 0 if opts[:dry] and Util::Json.emit(action: "apply", status: "dry-run", data: { entries: entries })
|
131
|
+
|
132
|
+
hoster.apply(entries)
|
133
|
+
Util::Json.emit(action: "apply", status: "success", data: { entries: entries })
|
134
|
+
return 0
|
135
|
+
|
136
|
+
when "remove"
|
137
|
+
remove_key = parse_remove_key_from_argv!(argv)
|
138
|
+
|
139
|
+
with_target_vms(argv, single_target: true) { |m| machine = m }
|
140
|
+
|
141
|
+
unless machine
|
142
|
+
UiHelpers.error(
|
143
|
+
@env.ui,
|
144
|
+
"#{UiHelpers.e(:error, no_emoji: opts[:no_emoji])} " +
|
145
|
+
::I18n.t(
|
146
|
+
"errors.no_machine",
|
147
|
+
default: "No target machine found. Run this inside a Vagrant project or pass a VM name."
|
148
|
+
)
|
149
|
+
)
|
150
|
+
Util::Json.emit(action: "remove", status: "error", error: "No target machine found") if opts[:json]
|
151
|
+
return 1
|
152
|
+
end
|
153
|
+
|
154
|
+
if remove_key && !(ipv4?(remove_key) || fqdn?(remove_key))
|
155
|
+
UiHelpers.error(@env.ui, "#{UiHelpers.e(:error, no_emoji: opts[:no_emoji])} " +
|
156
|
+
::I18n.t("messages.invalid_key",
|
157
|
+
default: "Invalid IP or FQDN: %{key}", key: remove_key))
|
158
|
+
UiHelpers.error(@env.ui, " " + ::I18n.t("messages.remove_expected_format",
|
159
|
+
default: "Expected IPv4 (e.g. 172.28.100.2) or FQDN (e.g. example.test)."))
|
160
|
+
Util::Json.emit(action: "remove", status: "error",
|
161
|
+
error: "invalid parameter format") if opts[:json]
|
162
|
+
return 1
|
163
|
+
end
|
164
|
+
|
165
|
+
mid = machine.id || "unknown"
|
166
|
+
hoster = Util::HostsFile.new({ machine: machine, ui: @env.ui }, owner_id: mid)
|
167
|
+
|
168
|
+
unless ENV["VDHM_DRY_RUN"] == "1" || hoster.elevated?
|
169
|
+
UiHelpers.error(@env.ui, "#{UiHelpers.e(:error, no_emoji: opts[:no_emoji])} " +
|
170
|
+
::I18n.t("errors.not_admin",
|
171
|
+
default: "Administrator/root privileges required to modify hosts at %{path}.",
|
172
|
+
path: hoster.printable_path))
|
173
|
+
return 1
|
174
|
+
end
|
175
|
+
|
176
|
+
if remove_key
|
177
|
+
return 0 if opts[:dry] && Util::Json.emit(action: "remove", status: "dry-run",
|
178
|
+
data: { owner: mid, key: remove_key })
|
179
|
+
|
180
|
+
removed = if ipv4?(remove_key)
|
181
|
+
hoster.remove_entries!(ips: [remove_key], domains: [])
|
182
|
+
else
|
183
|
+
hoster.remove_entries!(ips: [], domains: [remove_key])
|
184
|
+
end
|
185
|
+
Util::Json.emit(action: "remove", status: "success",
|
186
|
+
data: { removed: removed, mode: "filtered" })
|
187
|
+
return 0
|
188
|
+
end
|
189
|
+
|
190
|
+
return 0 if opts[:dry] && Util::Json.emit(action: "remove", status: "dry-run", data: { owner: mid })
|
191
|
+
|
192
|
+
vm_label = (machine.name rescue nil) || mid
|
193
|
+
unless confirm_all_removal?(@env.ui, vm_label, no_emoji: opts[:no_emoji])
|
194
|
+
UiHelpers.say(@env.ui, "#{UiHelpers.e(:info, no_emoji: opts[:no_emoji])} " +
|
195
|
+
::I18n.t("messages.remove_all_cancelled", default: "Cancelled. Nothing removed."))
|
196
|
+
Util::Json.emit(action: "remove", status: "cancelled", data: { owner: mid })
|
197
|
+
return 0
|
198
|
+
end
|
199
|
+
|
200
|
+
removed_block = hoster.remove!
|
201
|
+
Util::Json.emit(action: "remove", status: "success", data: { removed: removed_block })
|
202
|
+
return 0
|
203
|
+
|
204
|
+
when "view"
|
205
|
+
hoster = Util::HostsFile.new({ ui: @env.ui }, owner_id: "any")
|
206
|
+
managed_map = hoster.entries_in_blocks(:all)
|
207
|
+
planned_map = {}
|
208
|
+
|
209
|
+
begin
|
210
|
+
vm_for_view = nil
|
211
|
+
with_target_vms(argv.dup, single_target: true) { |m| vm_for_view = m }
|
212
|
+
if vm_for_view
|
213
|
+
cfg = vm_for_view.config.docker_hosts
|
214
|
+
planned_map = compute_entries(vm_for_view, cfg)
|
215
|
+
end
|
216
|
+
rescue StandardError
|
217
|
+
planned_map = {}
|
218
|
+
end
|
219
|
+
|
220
|
+
pairs = []
|
221
|
+
seen = {}
|
222
|
+
|
223
|
+
planned_map.each do |fqdn, ip|
|
224
|
+
next if fqdn.to_s.empty? || ip.to_s.empty?
|
225
|
+
key = "#{ip}\0#{fqdn}"
|
226
|
+
next if seen[key]
|
227
|
+
pairs << [ip.to_s, fqdn.to_s]
|
228
|
+
seen[key] = true
|
229
|
+
end
|
230
|
+
|
231
|
+
managed_map.each do |fqdn, ips|
|
232
|
+
next if fqdn.to_s.empty?
|
233
|
+
Array(ips).each do |ip|
|
234
|
+
next if ip.to_s.empty?
|
235
|
+
key = "#{ip}\0#{fqdn}"
|
236
|
+
next if seen[key]
|
237
|
+
pairs << [ip.to_s, fqdn.to_s]
|
238
|
+
seen[key] = true
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
if opts[:json]
|
243
|
+
Util::Json.emit(
|
244
|
+
action: "view",
|
245
|
+
status: "success",
|
246
|
+
data: { entries: pairs.map { |ip, fqdn| { ip: ip, host: fqdn } } }
|
247
|
+
)
|
248
|
+
return 0
|
249
|
+
end
|
250
|
+
|
251
|
+
emoji_info = UiHelpers.e(:info, no_emoji: opts[:no_emoji])
|
252
|
+
|
253
|
+
if pairs.empty?
|
254
|
+
UiHelpers.say(@env.ui, "#{emoji_info} " + ::I18n.t("messages.no_entries",
|
255
|
+
default: "No hosts entries configured."))
|
256
|
+
return 0
|
257
|
+
end
|
258
|
+
|
259
|
+
header =
|
260
|
+
if planned_map.any?
|
261
|
+
::I18n.t("messages.view_header", default: "Planned hosts entries:")
|
262
|
+
else
|
263
|
+
::I18n.t("messages.view_managed_header", default: "Managed hosts entries:")
|
264
|
+
end
|
265
|
+
|
266
|
+
UiHelpers.say(@env.ui, "#{emoji_info} #{header}")
|
267
|
+
pad = pairs.map { |ip, _| ip.length }.max || 0
|
268
|
+
pairs.each do |ip, fqdn|
|
269
|
+
UiHelpers.say(@env.ui, " • #{ip.ljust(pad)} -> #{fqdn}")
|
270
|
+
end
|
271
|
+
return 0
|
272
|
+
|
273
|
+
else
|
274
|
+
print_help(@env.ui, no_emoji: opts[:no_emoji])
|
275
|
+
return 0
|
276
|
+
end
|
277
|
+
rescue => e
|
278
|
+
Util::Json.emit(action: "command", status: "error", error: e.message)
|
279
|
+
1
|
280
|
+
end
|
281
|
+
|
282
|
+
private
|
283
|
+
|
284
|
+
def confirm_all_removal?(ui, vm_label, no_emoji:)
|
285
|
+
prompt = ::I18n.t(
|
286
|
+
"messages.confirm_remove_all",
|
287
|
+
default: "This will remove ALL managed hosts entries for %{vm}. Continue? (yes/No)",
|
288
|
+
vm: vm_label.to_s
|
289
|
+
)
|
290
|
+
|
291
|
+
line = "#{UiHelpers.e(:question, no_emoji: no_emoji)} #{prompt} "
|
292
|
+
|
293
|
+
begin
|
294
|
+
$stdout.print(line)
|
295
|
+
$stdout.flush
|
296
|
+
answer = ($stdin.gets || "").to_s
|
297
|
+
rescue
|
298
|
+
answer = ""
|
299
|
+
ensure
|
300
|
+
$stdout.puts ""
|
301
|
+
end
|
302
|
+
|
303
|
+
%w[y yes].include?(answer.strip.downcase)
|
304
|
+
end
|
305
|
+
|
306
|
+
def parse_strict_mapping_from_argv!(argv, ui, no_emoji, json: false)
|
307
|
+
return [nil, nil] if argv.empty?
|
308
|
+
|
309
|
+
if argv.length >= 2
|
310
|
+
cand_ip, cand_host = argv[0], argv[1]
|
311
|
+
|
312
|
+
unless ipv4?(cand_ip)
|
313
|
+
msg = ::I18n.t("messages.invalid_ip", default: "Invalid IPv4 address: %{ip}", ip: cand_ip)
|
314
|
+
UiHelpers.error(ui, "#{UiHelpers.e(:error, no_emoji: no_emoji)} #{msg}")
|
315
|
+
raise ArgumentError, msg
|
316
|
+
end
|
317
|
+
|
318
|
+
unless fqdn?(cand_host)
|
319
|
+
msg = ::I18n.t("messages.invalid_host", default: "Invalid host/FQDN: %{host}", host: cand_host)
|
320
|
+
UiHelpers.error(ui, "#{UiHelpers.e(:error, no_emoji: no_emoji)} #{msg}")
|
321
|
+
raise ArgumentError, msg
|
322
|
+
end
|
323
|
+
|
324
|
+
argv.shift(2)
|
325
|
+
return [cand_ip, cand_host]
|
326
|
+
end
|
327
|
+
|
328
|
+
if ipv4?(argv[0])
|
329
|
+
msg = ::I18n.t("messages.missing_mapping",
|
330
|
+
default: "Provide both IP and FQDN (e.g. `vagrant hosts apply 1.2.3.4 example.test`).")
|
331
|
+
UiHelpers.error(ui, "#{UiHelpers.e(:error, no_emoji: no_emoji)} #{msg}")
|
332
|
+
raise ArgumentError, msg
|
333
|
+
end
|
334
|
+
|
335
|
+
[nil, nil]
|
336
|
+
end
|
337
|
+
|
338
|
+
def ipv4?(s)
|
339
|
+
return false unless s.is_a?(String) && s =~ /\A\d{1,3}(?:\.\d{1,3}){3}\z/
|
340
|
+
s.split(".").all? { |x| (0..255).cover?(x.to_i) }
|
341
|
+
end
|
342
|
+
|
343
|
+
def fqdn?(s)
|
344
|
+
return false unless s.is_a?(String)
|
345
|
+
s = s.strip
|
346
|
+
return false if s.empty? || s.size > 253 || s.start_with?(".") || s.end_with?(".")
|
347
|
+
s.split(".").all? { |lab| lab =~ /\A[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\z/ }
|
348
|
+
end
|
349
|
+
|
350
|
+
def parse_mapping_from_argv!(argv)
|
351
|
+
return [nil, nil] if argv.length < 2
|
352
|
+
cand_ip, cand_host = argv[0], argv[1]
|
353
|
+
if ipv4?(cand_ip) && cand_host !~ /\A-/
|
354
|
+
argv.shift(2)
|
355
|
+
[cand_ip, cand_host]
|
356
|
+
else
|
357
|
+
[nil, nil]
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def parse_remove_key_from_argv!(argv)
|
362
|
+
return nil if argv.empty?
|
363
|
+
cand = argv[0]
|
364
|
+
if ipv4?(cand) || cand =~ /\A[a-zA-Z0-9\.\-]+\z/
|
365
|
+
argv.shift(1)
|
366
|
+
cand
|
367
|
+
else
|
368
|
+
nil
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def compute_entries(machine, cfg)
|
373
|
+
entries = {}
|
374
|
+
|
375
|
+
(cfg.domains || {}).each do |domain, ip|
|
376
|
+
next if domain.to_s.strip.empty?
|
377
|
+
next if ip.to_s.strip.empty?
|
378
|
+
entries[domain] = ip
|
379
|
+
end
|
380
|
+
|
381
|
+
if cfg.domain && !cfg.domain.strip.empty? && !entries.key?(cfg.domain)
|
382
|
+
ip = cfg.ip.to_s.strip
|
383
|
+
ip = Util::Docker.ip_for_container(cfg.container_name).to_s.strip if ip.empty? && !cfg.container_name.to_s.strip.empty?
|
384
|
+
entries[cfg.domain] = ip unless ip.empty?
|
385
|
+
end
|
386
|
+
|
387
|
+
entries
|
388
|
+
end
|
389
|
+
|
390
|
+
def print_help(ui, topic: nil, no_emoji: false)
|
391
|
+
return print_topic_help(ui, topic.to_s.downcase.strip, no_emoji: no_emoji) if topic && !topic.to_s.strip.empty?
|
392
|
+
|
393
|
+
emoji_info = UiHelpers.e(:info, no_emoji: no_emoji)
|
394
|
+
title = ::I18n.t("help.title", default: "Vagrant Docker Hosts Manager")
|
395
|
+
UiHelpers.say(ui, "#{emoji_info} #{title}")
|
396
|
+
|
397
|
+
UiHelpers.say(ui, ::I18n.t("help.usage",
|
398
|
+
default: "Usage: vagrant hosts <apply|remove|view|help|version> [options]"))
|
399
|
+
UiHelpers.say(ui, "")
|
400
|
+
|
401
|
+
UiHelpers.say(ui, ::I18n.t("help.commands_header", default: "Commands:"))
|
402
|
+
cmds = ::I18n.t("help.commands", default: {})
|
403
|
+
cmds = {} unless cmds.is_a?(Hash)
|
404
|
+
cmds.each_value { |line| UiHelpers.say(ui, " #{line}") }
|
405
|
+
|
406
|
+
UiHelpers.say(ui, "")
|
407
|
+
UiHelpers.say(ui, ::I18n.t("help.options_header", default: "Options:"))
|
408
|
+
optsh = ::I18n.t("help.options", default: {})
|
409
|
+
optsh = {} unless optsh.is_a?(Hash)
|
410
|
+
optsh.each_value { |line| UiHelpers.say(ui, " #{line}") }
|
411
|
+
|
412
|
+
topics = (::I18n.t("help.topic", default: {}).is_a?(Hash) ? ::I18n.t("help.topic").keys.map(&:to_s) : [])
|
413
|
+
topics = %w[apply remove view version help] if topics.empty?
|
414
|
+
|
415
|
+
UiHelpers.say(ui, "")
|
416
|
+
UiHelpers.say(ui, ::I18n.t("help.topics_header", default: "Help topics:"))
|
417
|
+
UiHelpers.say(ui, " vagrant hosts help <#{topics.join('|')}>")
|
418
|
+
end
|
419
|
+
|
420
|
+
def print_topic_help(ui, topic, no_emoji: false)
|
421
|
+
base = "help.topic.#{topic}"
|
422
|
+
wrench = UiHelpers.e(:info, no_emoji: no_emoji)
|
423
|
+
|
424
|
+
title = ::I18n.t("#{base}.title", default: topic)
|
425
|
+
usage = ::I18n.t("#{base}.usage", default: nil)
|
426
|
+
desc = ::I18n.t("#{base}.description", default: nil)
|
427
|
+
opts_hash = ::I18n.t("#{base}.options", default: {})
|
428
|
+
examples = ::I18n.t("#{base}.examples", default: [])
|
429
|
+
|
430
|
+
t_head = ::I18n.t("help.topic_header", default: "Help: vagrant hosts %{topic}", topic: topic)
|
431
|
+
t_usage = ::I18n.t("help.usage_label", default: "Usage:")
|
432
|
+
t_desc = ::I18n.t("help.description_label", default: "Description:")
|
433
|
+
t_opts = ::I18n.t("help.options_label", default: "Options:")
|
434
|
+
t_exs = ::I18n.t("help.examples_label", default: "Examples:")
|
435
|
+
|
436
|
+
UiHelpers.say(ui, "#{wrench} #{t_head}")
|
437
|
+
|
438
|
+
UiHelpers.say(ui, " #{t_usage}")
|
439
|
+
UiHelpers.say(ui, " #{usage || "vagrant hosts #{topic} [options]"}")
|
440
|
+
|
441
|
+
if desc && !desc.strip.empty?
|
442
|
+
UiHelpers.say(ui, " #{t_desc}")
|
443
|
+
UiHelpers.say(ui, " #{desc}")
|
444
|
+
end
|
445
|
+
|
446
|
+
if opts_hash.is_a?(Hash) && !opts_hash.empty?
|
447
|
+
UiHelpers.say(ui, " #{t_opts}")
|
448
|
+
opts_hash.each_value { |line| UiHelpers.say(ui, " #{line}") }
|
449
|
+
end
|
450
|
+
|
451
|
+
if examples.is_a?(Array) && !examples.empty?
|
452
|
+
UiHelpers.say(ui, " #{t_exs}")
|
453
|
+
examples.each { |ex| UiHelpers.say(ui, " #{ex}") }
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|