Skip to content

Commit 8cad27e

Browse files
authored
Merge pull request #2 from kiwicom/multiple-hosts
Various changes
2 parents 5658bfe + d3df5c6 commit 8cad27e

16 files changed

+287
-37
lines changed

README.md

+22-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,19 @@ Run
2929
composer global require kiwicom/loopbind
3030
```
3131

32-
Then if you have composer bin directory on the `PATH` you can use it by calling `loopbind` in the CLI.
32+
Then if you have composer bin directory on the `PATH` you can use it by calling `loopbind` in the CLI. So you can initialize the configuration with a CLI wizard:
33+
34+
```bash
35+
$ loopbind init
36+
> IPv4 address from local block:
37+
> 127.0.0.1
38+
> Hostname (leave empty to continue):
39+
> hostname
40+
> Hostname (leave empty to continue):
41+
>
42+
> New config file `.loopbind.json` was created.
43+
44+
```
3345

3446
## Usage
3547

@@ -41,8 +53,16 @@ In the project root define a file named `.loopbind.json` with following content:
4153
}
4254
```
4355

56+
Or when you need to bind multiple hostnames:
57+
```json
58+
{
59+
"localIPAlias": "127.11.23.1",
60+
"hostname": ["www.foobar.test","foobar.test"]
61+
}
62+
```
63+
4464
Then in this directory you can run `loopbind apply` to run commands to ensure the binding.
45-
Also, you can run `loopbind unapply` to remove it.
65+
Also, you can run `loopbind unapply` to remove it and `loopbind show` to show the configuration and its status.
4666

4767
The commands are idempotent so repeated apply/unapply does nothing (and the apply command does not even need to run the command again).
4868

bin/loopbind

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
use Contributte\Console\Application;
55
use Kiwicom\Loopbind\Commands\ApplyCommand;
6-
use Kiwicom\Loopbind\Commands\UnapplyCommand;
6+
use Kiwicom\Loopbind\Commands\InitCommand;
7+
use Kiwicom\Loopbind\Commands\ShowCommand;use Kiwicom\Loopbind\Commands\UnapplyCommand;
78
use Kiwicom\Loopbind\Constants\ExitCodes;
89
use Kiwicom\Loopbind\Helpers\PlatformHelpers;
910

@@ -27,7 +28,9 @@ $app = new Application();
2728
$app->setCatchExceptions(true);
2829
$app->setName('Loopbind');
2930
$app->addCommands([
31+
new InitCommand(null),
3032
new ApplyCommand(null),
31-
new UnapplyCommand(null)
33+
new UnapplyCommand(null),
34+
new ShowCommand(null),
3235
]);
3336
exit($app->run());

src/Commands/ApplyCommand.php

+12-5
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
use Kiwicom\Loopbind\Constants\ExitCodes;
88
use Kiwicom\Loopbind\Helpers\BindingHelpers;
99
use Kiwicom\Loopbind\Helpers\ShellHelpers;
10+
use Kiwicom\Loopbind\Trait\ConfigurationPrinter;
1011
use Symfony\Component\Console\Command\Command;
1112
use Symfony\Component\Console\Input\InputInterface;
1213
use Symfony\Component\Console\Output\OutputInterface;
1314

1415
final class ApplyCommand extends Command
1516
{
17+
use ConfigurationPrinter;
18+
1619
protected function configure(): void
1720
{
1821
$this->setName('apply')
@@ -30,15 +33,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
3033
return ExitCodes::NOT_READABLE_CONFIG_FILE;
3134
}
3235

33-
$this->printDesiredConfiguration($config, $output);
36+
self::printDesiredConfiguration($config, $output);
3437

3538
$shellCommands = [];
3639
if (!(BindingHelpers::isLocalInterfaceAliased($config))) {
3740
$shellCommands[] = ShellHelpers::getCommandLocalhostAlias($config);
3841
}
39-
if (!(BindingHelpers::isHostnameBinded($config))) {
40-
$shellCommands[] = ShellHelpers::getCommandUnbindHostname($config);
41-
$shellCommands[] = ShellHelpers::getCommandBindHostname($config);
42+
foreach ($config->getHostname() as $hostname) {
43+
if (!(BindingHelpers::isHostnameBinded($config, $hostname))) {
44+
$shellCommands[] = ShellHelpers::getCommandUnbindHostname($config, $hostname);
45+
$shellCommands[] = ShellHelpers::getCommandBindHostname($config, $hostname);
46+
}
4247
}
4348

4449
if (count($shellCommands) === 0) {
@@ -64,6 +69,8 @@ private function printDesiredConfiguration(Config $config, OutputInterface $outp
6469
$output->writeln('----------------');
6570
$output->writeln('Desired configuration:');
6671
$output->writeln("{$config->getLocalAliasIP()} -> 127.0.0.1\t\t" . (BindingHelpers::isLocalInterfaceAliased($config) ? '[<fg=green>READY</>]' : '[<fg=red>MISSING</>]'));
67-
$output->writeln("{$config->getHostname()} -> {$config->getLocalAliasIP()}\t\t" . (BindingHelpers::isHostnameBinded($config) ? '[<fg=green>READY</>]' : '[<fg=red>MISSING</>]'));
72+
foreach ($config->getHostname() as $hostname) {
73+
$output->writeln("{$hostname} -> {$config->getLocalAliasIP()}\t\t" . (BindingHelpers::isHostnameBinded($config, $hostname) ? '[<fg=green>READY</>]' : '[<fg=red>MISSING</>]'));
74+
}
6875
}
6976
}

src/Commands/InitCommand.php

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Kiwicom\Loopbind\Commands;
4+
5+
use Kiwicom\Loopbind\Config\Config;
6+
use Kiwicom\Loopbind\Config\ConfigLoader;
7+
use Kiwicom\Loopbind\Constants\ExitCodes;
8+
use Kiwicom\Loopbind\Exceptions\InvalidHostnameException;
9+
use Kiwicom\Loopbind\Exceptions\InvalidIPAddressException;
10+
use Kiwicom\Loopbind\Trait\ConfigurationPrinter;
11+
use Nette\Utils\Json;
12+
use Nette\Utils\JsonException;
13+
use Symfony\Component\Console\Command\Command;
14+
use Symfony\Component\Console\Helper\QuestionHelper;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Output\OutputInterface;
17+
use Symfony\Component\Console\Question\Question;
18+
use function file_put_contents;
19+
use function filter_var;
20+
use function in_array;
21+
use const FILTER_FLAG_HOSTNAME;
22+
use const FILTER_FLAG_IPV4;
23+
use const FILTER_VALIDATE_DOMAIN;
24+
use const FILTER_VALIDATE_IP;
25+
26+
final class InitCommand extends Command
27+
{
28+
use ConfigurationPrinter;
29+
30+
protected function configure(): void
31+
{
32+
$this->setName('init')
33+
->setDescription('Init localhost configuration from config in current working directory.');
34+
}
35+
36+
protected function execute(InputInterface $input, OutputInterface $output): int
37+
{
38+
$configLoader = new ConfigLoader();
39+
if ($configLoader->exists('.loopbind.json')) {
40+
return ExitCodes::CONFIG_ALREADY_EXISTS;
41+
}
42+
43+
$questionHelper = new QuestionHelper();
44+
45+
$valid = false;
46+
do {
47+
$IPAddress = $questionHelper->ask($input, $output, new Question("<question>IPv4 address from local block:</question>\n"));
48+
if (!is_string($IPAddress) || filter_var($IPAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) {
49+
$output->writeln('<error>Invalid IPv4 address. Please try again.</error>');
50+
} else {
51+
$valid = true;
52+
}
53+
} while (!$valid);
54+
55+
$askNext = true;
56+
$hostnames = [];
57+
do {
58+
$hostname = $questionHelper->ask($input, $output, new Question("<question>Hostname (leave empty to continue):</question>\n"));
59+
if ($hostname === null) {
60+
$askNext = false;
61+
continue;
62+
}
63+
if (!is_string($hostname)) {
64+
continue;
65+
}
66+
if (filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) {
67+
$output->writeln('<error>Invalid hostname. Please try again.</error>');
68+
} elseif (in_array($hostname, $hostnames, true)) {
69+
$output->writeln('<error>This hostname was already provided. Please try again.</error>');
70+
} else {
71+
$hostnames[] = $hostname;
72+
}
73+
} while (count($hostnames) === 0 || $askNext);
74+
75+
try {
76+
/** @var string $IPAddress */
77+
$config = new Config($IPAddress, $hostnames);
78+
$encoded = Json::encode($config, Json::PRETTY);
79+
} catch (InvalidIPAddressException | InvalidHostnameException | JsonException) {
80+
return ExitCodes::NEW_CONFIG_INVALID;
81+
}
82+
83+
file_put_contents('.loopbind.json', $encoded);
84+
85+
$output->writeln('<fg=green>New config file `.loopbind.json` was created.</>');
86+
87+
return ExitCodes::SUCCESS;
88+
}
89+
}

src/Commands/ShowCommand.php

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Kiwicom\Loopbind\Commands;
4+
5+
use Kiwicom\Loopbind\Config\ConfigLoader;
6+
use Kiwicom\Loopbind\Constants\ExitCodes;
7+
use Kiwicom\Loopbind\Trait\ConfigurationPrinter;
8+
use Symfony\Component\Console\Command\Command;
9+
use Symfony\Component\Console\Input\InputInterface;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
12+
final class ShowCommand extends Command
13+
{
14+
use ConfigurationPrinter;
15+
16+
protected function configure(): void
17+
{
18+
$this->setName('show')
19+
->setDescription('Show localhost configuration from config in current working directory and its state.');
20+
}
21+
22+
protected function execute(InputInterface $input, OutputInterface $output): int
23+
{
24+
$configLoader = new ConfigLoader();
25+
try {
26+
$config = $configLoader->loadAndParse('.loopbind.json');
27+
} catch (\Kiwicom\Loopbind\Exceptions\InvalidConfigFileException $e) {
28+
return ExitCodes::INVALID_CONFIG_FILE;
29+
} catch (\Kiwicom\Loopbind\Exceptions\UnreadableConfigFileException $e) {
30+
return ExitCodes::NOT_READABLE_CONFIG_FILE;
31+
}
32+
33+
self::printDesiredConfiguration($config, $output);
34+
35+
return ExitCodes::SUCCESS;
36+
}
37+
}

src/Commands/UnapplyCommand.php

+8-3
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int
3737
if (PlatformHelpers::isOSX() && (BindingHelpers::isLocalInterfaceAliased($config))) {
3838
$shellCommands[] = ShellHelpers::getCommandLocalhostUnalias($config);
3939
}
40-
if ((BindingHelpers::isHostnameBinded($config))) {
41-
$shellCommands[] = ShellHelpers::getCommandUnbindHostname($config);
40+
foreach ($config->getHostname() as $hostname) {
41+
if ((BindingHelpers::isHostnameBinded($config, $hostname))) {
42+
$shellCommands[] = ShellHelpers::getCommandUnbindHostname($config, $hostname);
43+
}
4244
}
4345

46+
4447
if (count($shellCommands) === 0) {
4548
$output->writeln('<options=bold>No changes needed, nothing is applied.</>');
4649
return ExitCodes::SUCCESS;
@@ -69,6 +72,8 @@ private function printCurrentConfiguration(Config $config, OutputInterface $outp
6972
if (PlatformHelpers::isLinux()) {
7073
$output->writeln("{$config->getLocalAliasIP()} -> 127.0.0.1\t\t" . '[<fg=blue>IRRELEVANT</>]');
7174
}
72-
$output->writeln("{$config->getHostname()} -> {$config->getLocalAliasIP()}\t\t" . (BindingHelpers::isHostnameBinded($config) ? '[<fg=red>TO BE REMOVED</>]' : '[<fg=green>NOT PRESENT</>]'));
75+
foreach ($config->getHostname() as $hostname) {
76+
$output->writeln("{$hostname} -> {$config->getLocalAliasIP()}\t\t" . (BindingHelpers::isHostnameBinded($config, $hostname) ? '[<fg=red>TO BE REMOVED</>]' : '[<fg=green>NOT PRESENT</>]'));
77+
}
7378
}
7479
}

src/Config/Config.php

+38-8
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,43 @@
22

33
namespace Kiwicom\Loopbind\Config;
44

5+
use JsonSerializable;
6+
use function array_map;
7+
use function filter_var;
8+
use function is_array;
9+
use function is_string;
510
use const FILTER_FLAG_HOSTNAME;
611
use const FILTER_VALIDATE_DOMAIN;
712

8-
final class Config
13+
final class Config implements JsonSerializable
914
{
1015
private string $localAliasIP;
1116

12-
private string $hostname;
17+
/** @var string|string[] */
18+
private string|array $hostname;
1319

20+
/**
21+
* @param string $localAliasIP
22+
* @param string|array<string> $hostname
23+
*/
1424
public function __construct(
1525
string $localAliasIP,
16-
string $hostname
26+
string|array $hostname
1727
) {
1828
if (filter_var($localAliasIP, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) {
19-
throw new \Kiwicom\Loopbind\Exceptions\InvalidIPAddressException("Value `${localAliasIP}` is not valid IPv4 address.");
29+
throw new \Kiwicom\Loopbind\Exceptions\InvalidIPAddressException("Value `{$localAliasIP}` is not valid IPv4 address.");
2030
}
21-
if (filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) {
22-
throw new \Kiwicom\Loopbind\Exceptions\InvalidHostnameException("Value `${hostname}` is not valid hostname.");
31+
if (is_string($hostname)) {
32+
if (filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) {
33+
throw new \Kiwicom\Loopbind\Exceptions\InvalidHostnameException("Value `{$hostname}` is not valid hostname.");
34+
}
35+
}
36+
if (is_array($hostname)) {
37+
array_map(fn (string $host): bool => filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false ?
38+
throw new \Kiwicom\Loopbind\Exceptions\InvalidHostnameException("Value `{$host}` is not valid hostname.") : true, $hostname);
2339
}
2440
if ($hostname === 'localhost') {
25-
throw new \Kiwicom\Loopbind\Exceptions\InvalidHostnameException("Hostname `${hostname}` is forbidden by this tool.");
41+
throw new \Kiwicom\Loopbind\Exceptions\InvalidHostnameException("Hostname `{$hostname}` is forbidden by this tool.");
2642
}
2743

2844
$this->localAliasIP = $localAliasIP;
@@ -34,8 +50,22 @@ public function getLocalAliasIP(): string
3450
return $this->localAliasIP;
3551
}
3652

37-
public function getHostname(): string
53+
/**
54+
* @return array<string>
55+
*/
56+
public function getHostname(): array
3857
{
58+
if (is_string($this->hostname)) {
59+
return [$this->hostname];
60+
}
3961
return $this->hostname;
4062
}
63+
64+
public function jsonSerialize(): mixed
65+
{
66+
return [
67+
'localIPAlias' => $this->localAliasIP,
68+
'hostname' => $this->hostname
69+
];
70+
}
4171
}

src/Config/ConfigLoader.php

+17-7
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,25 @@ public function loadAndParse(string $filePath): Config
3636
public function load(string $filePath): string
3737
{
3838
if (!file_exists($filePath) || !is_readable($filePath)) {
39-
throw new \Kiwicom\Loopbind\Exceptions\UnreadableConfigFileException("File `${filePath}` is not readable or does not exist.");
39+
throw new \Kiwicom\Loopbind\Exceptions\UnreadableConfigFileException("File `{$filePath}` is not readable or does not exist.");
4040
}
4141
$content = file_get_contents($filePath);
4242
if ($content === false) {
43-
throw new \Kiwicom\Loopbind\Exceptions\UnreadableConfigFileException("File `${filePath}` is not readable or does not exist.");
43+
throw new \Kiwicom\Loopbind\Exceptions\UnreadableConfigFileException("File `{$filePath}` is not readable or does not exist.");
4444
}
4545
return $content;
4646
}
4747

48+
/**
49+
* @param string $filePath
50+
*
51+
* @return bool
52+
*/
53+
public function exists(string $filePath): bool
54+
{
55+
return file_exists($filePath);
56+
}
57+
4858
/**
4959
* @param string $content
5060
* @param string $filePath
@@ -58,26 +68,26 @@ public function parse(string $content, string $filePath): Config
5868
try {
5969
$data = Json::decode($content, Json::FORCE_ARRAY);
6070
} catch (\Nette\Utils\JsonException $exception) {
61-
throw new \Kiwicom\Loopbind\Exceptions\InvalidConfigFileException("File `${filePath}` is not a valid JSON file: {$exception->getMessage()}");
71+
throw new \Kiwicom\Loopbind\Exceptions\InvalidConfigFileException("File `{$filePath}` is not a valid JSON file: {$exception->getMessage()}");
6272
}
6373

6474
$schema = Expect::structure([
6575
'localIPAlias' => Expect::string()->required(),
66-
'hostname' => Expect::string()->required(),
76+
'hostname' => Expect::anyOf(Expect::string(), Expect::arrayOf(Expect::string()))->required(),
6777
])->castTo('array');
6878

6979
$processor = new Processor();
7080
try {
71-
/** @var array{localIPAlias: string, hostname: string} $normalized */
81+
/** @var array{localIPAlias: string, hostname: string|array<string>} $normalized */
7282
$normalized = $processor->process($schema, $data);
7383
} catch (\Nette\Schema\ValidationException $exception) {
74-
throw new \Kiwicom\Loopbind\Exceptions\InvalidConfigFileException("File `${filePath}` does not contain valid configuration: {$exception->getMessage()}");
84+
throw new \Kiwicom\Loopbind\Exceptions\InvalidConfigFileException("File `{$filePath}` does not contain valid configuration: {$exception->getMessage()}");
7585
}
7686

7787
try {
7888
$config = new Config($normalized['localIPAlias'], $normalized['hostname']);
7989
} catch (\Kiwicom\Loopbind\Exceptions\InvalidIPAddressException|\Kiwicom\Loopbind\Exceptions\InvalidHostnameException $exception) {
80-
throw new \Kiwicom\Loopbind\Exceptions\InvalidConfigFileException("File `${filePath}` does not contain valid configuration: {$exception->getMessage()}");
90+
throw new \Kiwicom\Loopbind\Exceptions\InvalidConfigFileException("File `{$filePath}` does not contain valid configuration: {$exception->getMessage()}");
8191
}
8292
return $config;
8393
}

0 commit comments

Comments
 (0)