diff --git a/README.md b/README.md index 862da24..c8133a8 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,10 @@ Redis data is stored in `~/.porter/data/redis`. ## DNS + - `porter dns:on` - Turn the DNS container on (the default status). + + - `porter dns:off` - Turn the DNS container off. + - `porter dns:flush` - flush your local machine's DNS in cases where it's getting a bit confused, saves you looking up the command we hope. - `porter dns:set-host {--restore}` - see below. The `--restore` will remove the setup. diff --git a/app/Commands/Dns/Off.php b/app/Commands/Dns/Off.php new file mode 100644 index 0000000..6a90fd4 --- /dev/null +++ b/app/Commands/Dns/Off.php @@ -0,0 +1,32 @@ +porter->turnOffService('dns'); + } +} diff --git a/app/Commands/Dns/On.php b/app/Commands/Dns/On.php new file mode 100644 index 0000000..a9772d3 --- /dev/null +++ b/app/Commands/Dns/On.php @@ -0,0 +1,32 @@ +porter->turnOnService('dns'); + } +} diff --git a/app/Commands/Ngrok/Open.php b/app/Commands/Ngrok/Open.php index bc8f136..bf1a613 100644 --- a/app/Commands/Ngrok/Open.php +++ b/app/Commands/Ngrok/Open.php @@ -22,6 +22,13 @@ class Open extends BaseCommand */ protected $description = 'Open ngrok connection to forward your dev environment to an external url'; + /** + * Was the site secure at the start of the command? + * + * @var bool + */ + protected $wasSecure = false; + /** * Execute the console command. * @@ -30,7 +37,6 @@ class Open extends BaseCommand public function handle(): void { $site = Site::resolveFromPathOrCurrentWorkingDirectory($this->argument('site')); - $wasSecure = false; if (!$site) { $this->error('No site at this location, and no site path provided.'); @@ -42,28 +48,17 @@ public function handle(): void return; } - if ($site->secure) { - $this->info('Removing SSL for site (required for free ngrok version)'); - $site->unsecure(); - $wasSecure = true; - } + $this->removeSSLIfNeeded($site); $this->porter->stop('ngrok'); - $tls = ' -bind-tls='.($wasSecure ? 'true' : 'false'); - $region = ' -region='.$this->option('region'); - $inspect = ' -inspect='.($this->option('no-inspection') ? 'false' : 'true'); - $this->dockerCompose ->runContainer('ngrok') - ->append("ngrok http -host-header=rewrite{$region}{$tls}{$inspect} {$site->url}:80") + ->append($this->constructNgrokCommand($site)) ->interactive() ->perform(); - if ($wasSecure) { - $this->info('Restoring SSL for site'); - $site->secure(); - } + $this->restoreSSLIfNeeded($site); $this->porter->stop('ngrok'); } @@ -92,4 +87,51 @@ public function checkItWillResolveProperly() return true; } + + /** + * Remove SSL from the site if it was secured. + * + * @param Site $site + */ + protected function removeSSLIfNeeded(Site $site): void + { + if (!$site->secure) { + return; + } + + $this->info('Removing SSL for site (required for free ngrok version)'); + $this->wasSecure = true; + $site->unsecure(); + } + + /** + * Add SSL back to the site if it was previously secured. + * + * @param Site $site + */ + protected function restoreSSLIfNeeded(Site $site): void + { + if (!$this->wasSecure) { + return; + } + + $this->info('Restoring SSL for site'); + $site->secure(); + } + + /** + * Construct the ngrok command. + * + * @param Site $site + * + * @return string + */ + protected function constructNgrokCommand(Site $site): string + { + $tls = ' -bind-tls='.($this->wasSecure ? 'true' : 'false'); + $region = ' -region='.$this->option('region'); + $inspect = ' -inspect='.($this->option('no-inspection') ? 'false' : 'true'); + + return "ngrok http -host-header=rewrite{$region}{$tls}{$inspect} {$site->url}:80"; + } } diff --git a/app/Support/Console/DockerCompose/YamlBuilder.php b/app/Support/Console/DockerCompose/YamlBuilder.php index c7e6578..4ed15ed 100644 --- a/app/Support/Console/DockerCompose/YamlBuilder.php +++ b/app/Support/Console/DockerCompose/YamlBuilder.php @@ -32,19 +32,34 @@ public function build(ImageRepository $imageSet) { $this->files->put( $this->porterLibrary->dockerComposeFile(), - view("{$imageSet->getName()}::base")->with([ - 'home' => setting('home'), - 'host_machine_name' => setting('host_machine_name'), - 'activePhpVersions' => PhpVersion::active()->get(), - 'useMysql' => setting('use_mysql') == 'on', - 'useRedis' => setting('use_redis') == 'on', - 'useBrowser' => setting('use_browser') == 'on', - 'imageSet' => $imageSet, - 'libraryPath' => $this->porterLibrary->path(), - ])->render() + $this->renderDockerComposeFile($imageSet) ); } + /** + * Render the docker compose file. + * + * @param ImageRepository $imageSet + * + * @throws \Throwable + * + * @return string + */ + public function renderDockerComposeFile(ImageRepository $imageSet) + { + return view("{$imageSet->getName()}::base")->with([ + 'home' => setting('home'), + 'host_machine_name' => setting('host_machine_name'), + 'activePhpVersions' => PhpVersion::active()->get(), + 'useMysql' => setting('use_mysql') === 'on', + 'useRedis' => setting('use_redis') === 'on', + 'useBrowser' => setting('use_browser') === 'on', + 'useDns' => setting('use_dns') === 'on' || setting_missing('use_dns'), + 'imageSet' => $imageSet, + 'libraryPath' => $this->porterLibrary->path(), + ])->render(); + } + /** * Destroy the docker-compose.yaml file. */ diff --git a/app/Support/helpers.php b/app/Support/helpers.php index 94ba5a1..8e9b65a 100644 --- a/app/Support/helpers.php +++ b/app/Support/helpers.php @@ -8,7 +8,7 @@ * @param string|null $key * @param string|null $default * - * @return mixed + * @return mixed|\Illuminate\Support\Collection */ function setting($key = null, $default = null) { @@ -19,10 +19,34 @@ function setting($key = null, $default = null) return Setting::where('name', $key)->value('value') ?? $default; } +/** + * Check if a setting exists. + * + * @param string $key + * + * @return bool + */ +function setting_exists($key) +{ + return !is_null(setting($key)); +} + +/** + * Check if a setting is missing. + * + * @param string $key + * + * @return bool + */ +function setting_missing($key) +{ + return !setting_exists($key); +} + /** * Check if we're running tests since environment is limited to production/development. */ function running_tests() { - return config('app.running_tests'); + return (bool) config('app.running_tests'); } diff --git a/resources/image_sets/konsulting/porter-ubuntu/docker_compose/base.blade.php b/resources/image_sets/konsulting/porter-ubuntu/docker_compose/base.blade.php index bfddd3d..f45f84e 100644 --- a/resources/image_sets/konsulting/porter-ubuntu/docker_compose/base.blade.php +++ b/resources/image_sets/konsulting/porter-ubuntu/docker_compose/base.blade.php @@ -6,7 +6,7 @@ services: - @include("{$imageSet->getName()}::dns") + @includeWhen($useDns, "{$imageSet->getName()}::dns") @include("{$imageSet->getName()}::mailhog") @@ -26,14 +26,8 @@ # END PHP version {!! $version->version_number !!} @endforeach -@if($useMysql) - @include("{$imageSet->getName()}::mysql") -@endif + @includeWhen($useMysql, "{$imageSet->getName()}::mysql") -@if($useRedis) - @include("{$imageSet->getName()}::redis") -@endif + @includeWhen($useRedis, "{$imageSet->getName()}::redis") -@if ($useBrowser) - @include("{$imageSet->getName()}::browser") -@endif + @includeWhen($useBrowser, "{$imageSet->getName()}::browser") diff --git a/tests/Unit/Commands/Browser/OffTest.php b/tests/Unit/Commands/Browser/OffTest.php index e1b9413..b897d02 100644 --- a/tests/Unit/Commands/Browser/OffTest.php +++ b/tests/Unit/Commands/Browser/OffTest.php @@ -10,7 +10,7 @@ class OffTest extends BaseTestCase use MocksPorter; /** @test */ - public function it_turns_the_browser_on() + public function it_turns_the_browser_off() { $this->porter->shouldReceive('turnOffService')->with('browser')->once(); diff --git a/tests/Unit/Commands/Dns/FlushTest.php b/tests/Unit/Commands/Dns/FlushTest.php new file mode 100644 index 0000000..ec183ea --- /dev/null +++ b/tests/Unit/Commands/Dns/FlushTest.php @@ -0,0 +1,25 @@ +shouldIgnoreMissing() + ->shouldReceive('flushDns')->withNoArgs()->once(); + + $this->app->instance(Mechanic::class, $mechanicMock); + $this->app->get(PorterLibrary::class)->setMechanic($mechanicMock); + + $this->artisan('dns:flush'); + } +} diff --git a/tests/Unit/Commands/Dns/OffTest.php b/tests/Unit/Commands/Dns/OffTest.php new file mode 100644 index 0000000..8a12ebc --- /dev/null +++ b/tests/Unit/Commands/Dns/OffTest.php @@ -0,0 +1,19 @@ +porter->shouldReceive('turnOffService')->with('dns')->once(); + + $this->artisan('dns:off'); + } +} diff --git a/tests/Unit/Commands/Dns/OnTest.php b/tests/Unit/Commands/Dns/OnTest.php new file mode 100644 index 0000000..3ae7744 --- /dev/null +++ b/tests/Unit/Commands/Dns/OnTest.php @@ -0,0 +1,19 @@ +porter->shouldReceive('turnOnService')->with('dns')->once(); + + $this->artisan('dns:on'); + } +} diff --git a/tests/Unit/Commands/Dns/SetHostTest.php b/tests/Unit/Commands/Dns/SetHostTest.php new file mode 100644 index 0000000..5f07519 --- /dev/null +++ b/tests/Unit/Commands/Dns/SetHostTest.php @@ -0,0 +1,46 @@ +shouldIgnoreMissing(); + $mechanicMock->shouldReceive('addAlternativeLoopbackAddress')->once(); + $mechanicMock->shouldReceive('getAlternativeLoopback')->andReturn('1.1.1.1')->once(); + + $this->app->instance(Mechanic::class, $mechanicMock); + $this->app->get(PorterLibrary::class)->setMechanic($mechanicMock); + + $this->porter->shouldReceive('restart')->with('dns')->once(); + + $this->artisan('dns:set-host'); + } + + /** @test */ + public function it_will_restore_the_host() + { + $mechanicMock = Mockery::mock(Mechanic::class); + $mechanicMock->shouldIgnoreMissing(); + $mechanicMock->shouldReceive('removeAlternativeLoopbackAddress')->once(); + $mechanicMock->shouldReceive('getStandardLoopback')->andReturn('127.0.0.1')->once(); + + $this->app->instance(Mechanic::class, $mechanicMock); + $this->app->get(PorterLibrary::class)->setMechanic($mechanicMock); + + $this->porter->shouldReceive('restart')->with('dns')->once(); + + $this->artisan('dns:set-host', ['--restore' => 1]); + } +} diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php new file mode 100644 index 0000000..8183711 --- /dev/null +++ b/tests/Unit/HelpersTest.php @@ -0,0 +1,94 @@ +assertSame([ + 'test1' => 'testa', + 'test2' => 'testb', + ], setting()->toArray()); + } + + /** @test */ + public function setting_returns_a_setting() + { + Setting::updateOrCreate('test1', 'testa'); + Setting::updateOrCreate('test2', 'testb'); + + $this->assertSame('testa', setting('test1')); + } + + /** @test */ + public function setting_returns_null_by_default_when_a_setting_doesnt_exist() + { + Setting::updateOrCreate('test1', 'testa'); + Setting::updateOrCreate('test2', 'testb'); + + $this->assertSame(null, setting('test3')); + } + + /** @test */ + public function setting_returns_a_default_when_a_setting_doesnt_exist() + { + Setting::updateOrCreate('test1', 'testa'); + Setting::updateOrCreate('test2', 'testb'); + + $this->assertSame('default', setting('test3', 'default')); + } + + /** @test */ + public function setting_exists_returns_true_if_a_setting_exists() + { + Setting::updateOrCreate('test1', 'testa'); + + $this->assertTrue(setting_exists('test1')); + } + + /** @test */ + public function setting_exists_returns_false_if_a_setting_exists() + { + Setting::updateOrCreate('test1', 'testa'); + + $this->assertFalse(setting_exists('test2')); + } + + /** @test */ + public function setting_missing_returns_false_if_a_setting_exists() + { + Setting::updateOrCreate('test1', 'testa'); + + $this->assertFalse(setting_missing('test1')); + } + + /** @test */ + public function setting_missing_returns_true_if_a_setting_exists() + { + Setting::updateOrCreate('test1', 'testa'); + + $this->assertTrue(setting_missing('test2')); + } + + /** @test */ + public function running_tests_is_true_if_running_tests() + { + $this->assertTrue(running_tests()); + } + + /** @test */ + public function running_tests_is_false_if_no_running_tests() + { + config()->set('app.running_tests', 0); + + $this->assertFalse(running_tests()); + } +} diff --git a/tests/Unit/Support/Console/DockerCompose/YamlBuilderTest.php b/tests/Unit/Support/Console/DockerCompose/YamlBuilderTest.php new file mode 100644 index 0000000..a49fc84 --- /dev/null +++ b/tests/Unit/Support/Console/DockerCompose/YamlBuilderTest.php @@ -0,0 +1,114 @@ +files = \Mockery::mock(Filesystem::class); + $this->lib = \Mockery::mock(PorterLibrary::class); + $this->images = new ImageRepository(resource_path('image_sets/konsulting/porter-ubuntu')); + $this->builder = new YamlBuilder($this->files, $this->lib); + } + + /** @test */ + public function it_will_build_a_yaml_file() + { + $this->lib->shouldReceive('path')->once()->andReturn('pathtodotporter'); + + $output = $this->builder->renderDockerComposeFile($this->images); + + $this->assertContains('pathtodotporter', $output); + } + + /** @test */ + public function it_will_build_a_yaml_file_with_all_the_services() + { + Setting::updateOrCreate('use_browser', 'on'); + Setting::updateOrCreate('use_dns', 'on'); + Setting::updateOrCreate('use_redis', 'on'); + Setting::updateOrCreate('use_mysql', 'on'); + + $this->lib->shouldReceive('path')->once()->andReturn('pathtodotporter'); + + $output = $this->builder->renderDockerComposeFile($this->images); + + $this->assertContains('browser:', $output); + $this->assertContains('dns:', $output); + $this->assertContains('redis:', $output); + $this->assertContains('mysql:', $output); + $this->assertContains('pathtodotporter', $output); + } + + /** @test */ + public function it_will_build_a_yaml_file_without_all_the_services() + { + Setting::updateOrCreate('use_browser', 'off'); + Setting::updateOrCreate('use_dns', 'off'); + Setting::updateOrCreate('use_redis', 'off'); + Setting::updateOrCreate('use_mysql', 'off'); + + $this->lib->shouldReceive('path')->once()->andReturn('pathtodotporter'); + + $output = $this->builder->renderDockerComposeFile($this->images); + + $this->assertNotContains('browser:', $output); + $this->assertNotContains('dns:', $output); + $this->assertNotContains('redis:', $output); + $this->assertNotContains('mysql:', $output); + $this->assertContains('pathtodotporter', $output); + } + + /** @test */ + public function it_will_build_a_yaml_file_dns_when_the_setting_doesnt_exist() + { + $this->lib->shouldReceive('path')->once()->andReturn('pathtodotporter'); + + $output = $this->builder->renderDockerComposeFile($this->images); + + $this->assertContains('dns:', $output); + $this->assertNull(setting('use_dns')); + } + + /** @test */ + public function it_will_create_the_correct_file() + { + $this->lib->shouldReceive('path')->twice()->andReturn('pathtodotporter'); + $this->lib->shouldReceive('dockerComposeFile')->once()->andReturn('docker-compose.yaml'); + + $content = $this->builder->renderDockerComposeFile($this->images); + $this->files->shouldReceive('put')->with('docker-compose.yaml', $content)->once(); + + $this->builder->build($this->images); + } + + /** @test */ + public function it_will_remove_the_file() + { + $this->lib->shouldReceive('dockerComposeFile')->once()->andReturn('docker-compose.yaml'); + + $this->files->shouldReceive('delete')->with('docker-compose.yaml')->once(); + + $this->builder->destroy(); + } +}