From cf5135659052ade0d1d8a848af16cdfa6a13caf3 Mon Sep 17 00:00:00 2001 From: Keoghan Litchfield Date: Sat, 21 Nov 2020 18:32:21 +0000 Subject: [PATCH] Add ability to turn Xdebug on and off, easter egg to play nice with Laravel Valet (#95) * Add functionality to turn xdebug on or off with porter php:xdebug on/off * Cheeky addition of functionality to make Porter play nice with Laravel Valet. Sites can co-exist with each other. --- README.md | 80 ++++-- app/Commands/DockerCompose/SoftRestart.php | 32 +++ app/Commands/Images/Build.php | 2 +- app/Commands/MySql/Open.php | 2 +- app/Commands/Php/XdebugStatus.php | 47 ++++ app/Commands/Valet/Off.php | 33 +++ app/Commands/Valet/On.php | 33 +++ app/Events/SiteRemoved.php | 15 + app/Events/SiteSecured.php | 15 + app/Events/SiteUnsecured.php | 15 + app/Models/Site.php | 9 + app/Porter.php | 11 + app/Providers/EventServiceProvider.php | 6 +- app/Support/Console/Cli.php | 8 +- .../Console/DockerCompose/CliCommand.php | 6 +- .../Console/DockerCompose/YamlBuilder.php | 4 +- app/Support/Contracts/Cli.php | 4 +- app/Support/DockerSync/EventSubscriber.php | 8 +- app/Support/Mutagen/EventSubscriber.php | 8 +- app/Support/Valet/EventSubscriber.php | 59 ++++ app/Support/Valet/Valet.php | 143 ++++++++++ app/Support/XDebug/EventSubscriber.php | 39 +++ app/Support/XDebug/XDebug.php | 101 +++++++ .../docker_compose/nginx.blade.php | 4 +- tests/Unit/PorterTest.php | 13 + tests/Unit/Support/Console/CliTest.php | 2 +- .../Support/DockerSync/DockerSyncTest.php | 2 +- tests/Unit/Support/Mutagen/MutagenTest.php | 2 +- .../Nginx/AvailableConfigurationsTest.php | 2 +- tests/Unit/Support/Valet/ValetTest.php | 260 ++++++++++++++++++ tests/Unit/Support/Xdebug/XdebugTest.php | 173 ++++++++++++ 31 files changed, 1086 insertions(+), 52 deletions(-) create mode 100644 app/Commands/DockerCompose/SoftRestart.php create mode 100644 app/Commands/Php/XdebugStatus.php create mode 100644 app/Commands/Valet/Off.php create mode 100644 app/Commands/Valet/On.php create mode 100644 app/Events/SiteRemoved.php create mode 100644 app/Events/SiteSecured.php create mode 100644 app/Events/SiteUnsecured.php create mode 100644 app/Support/Valet/EventSubscriber.php create mode 100644 app/Support/Valet/Valet.php create mode 100644 app/Support/XDebug/EventSubscriber.php create mode 100644 app/Support/XDebug/XDebug.php create mode 100644 tests/Unit/Support/Valet/ValetTest.php create mode 100644 tests/Unit/Support/Xdebug/XdebugTest.php diff --git a/README.md b/README.md index 3b32904..82714f9 100644 --- a/README.md +++ b/README.md @@ -43,21 +43,34 @@ Contributions are welcome. We are a small company, so please be patient if your - Set up DNS resolution... you have some options. - 1. Use the DNS container shipped with Porter. Update your machine's network DNS settings to point to 127.0.0.1 before other name servers. The container will resolve the domain for Porter. You will need to turn off locally any installed DNSmasq since the DNS container opens to port 53 on localhost. (e.g. `brew services stop dnsmasq`) - - 2. Use your existing Laravel Valet domain - which uses DNSmasq installed locally on a Mac. + 1. If you want to use Porter alongside Laravel Valet, you can use DNSmasq installed by Valet. + Do nothing here. Make sure that the same TLD is used for Porter and Valet. + + 2. Use the DNS container shipped with Porter. + Update your machine's network DNS settings to point to 127.0.0.1 before other name servers. + The container will resolve the domain for Porter. + You will need to turn off locally any installed DNSmasq since the DNS container opens to port 53 on localhost. + (e.g. `brew services stop dnsmasq`) 3. Manually edit your `/etc/hosts` file for each domain. 4. Roll your own solution. - - Porter binds to ports 80 and 443, so you need to turn Valet off (`valet stop`) or any other services that are bound to them before using it. - +> If you choose options ii, iii or iv, you will need to turn Valet off (`valet stop`) +> or any other services that are bound to them before using it. +> Porter will bind it's Nginx container to ports 80 and 443. + - In your terminal `cd` to the directory where your sites are located, and run `porter begin`. This command will ask for your home directory (the root of your sites) and will generate a CA Certificate (and ask your permission to trust it on Mac). - Finally run `porter start` + +> If you intend to run Porter alongside Valet, you need to let Porter know. +> +> Run `porter valet:on` and it will adjust it's settings to make this happen. +> +> Porter will then set up and maintain 'proxy' records with Valet so that traffic for Porter sites ends up in the right place. ## Usage @@ -92,6 +105,15 @@ We have deliberately chosen not to make this automatic, nor permanent to avoid d - `porter stop` - `porter restart` - Restart existing containers (e.g. pick up config changes for PHP FPM) - `porter logs {service}` - Show container logs, optionally pass in the service + + + - `porter valet:on` - Let Porter know it is running alongside Laravel Valet + - `porter valet:off` - Let Porter know it is NOT running alongside Laravel Valet + +> When running alongside Valet: +> * Porter turns off its DNS container +> * It binds the Nginx container to alternative ports (http 8008 and https 8443) +> * It maintains a list of `proxies` on Valet for Porter sites ### Basic settings @@ -123,10 +145,23 @@ In Firefox, you will need to manually add the certificate, which is located in ` - `porter php:list` - List the available PHP versions - `porter php:open {run?} {--p|php-version?}` - Open the PHP cli for the project, if run from a project directory, it will select the associated version. Otherwise, you can select a version or use the default. Optionally run a command, such as `vendor/bin/phpunit` (if you need to pass arguments, wrap in quotes). - `porter php:tinker` - Run Artisan Tinker in the project directory + - `porter php:xdebug {on/off}` - Turn xdebug on/off in the container (by enabling/disabling the extension) - enabled by default. `php.ini` files are stored in `~/.porter/config` by PHP version. If you change one, you'll need to run `porter php:restart` for changes to be picked up. -We currently ship with containers for PHP 5.6, 7.0, 7.1, 7.2 and 7.3. +We currently ship with containers for PHP 5.6, 7.0, 7.1, 7.2, 7.3 and 7.4. + +## PHP Extensions + +We have added a number of PHP extensions to the containers that we use frequently. Notable ones are Imagick and Xdebug. + +### Xdebug + +Xdebug is available on each PHP container. `xdebug.ini` files are stored in `storage/config` by PHP version. + +It is set up for use with PhpStorm, and on demand - you can use an extension such as Xdebug helper in Chrome to send the Cookie required to activate a debugging session ([Jetbrains article](https://confluence.jetbrains.com/display/PhpStorm/Configure+Xdebug+Helper+for+Chrome+to+be+used+with+PhpStorm)). + +Xdebug is set up to communicate with the host machine on port 9001 to avoid clashes with any locally installed PHP-fpm. ### Node (npm/yarn) - `porter node:open {run?}` - Open Node cli, run in project dir. Optionally run a command, such as `npm run production` (if you need to pass arguments, wrap in quotes). @@ -201,18 +236,6 @@ We have a [MailHog](https://github.com/mailhog/MailHog) container; all emails ar You can review received emails in MailHog's UI at [http://localhost:8025](http://localhost:8025/). Or, you can use the MailHog API to inspect received emails. -## PHP Extensions - -We have added a number of PHP extensions to the containers that we use frequently. Notable ones are Imagick and Xdebug. - -### Xdebug - -Xdebug is available on each PHP container. `xdebug.ini` files are stored in `storage/config` by PHP version. - -It is set up for use with PhpStorm, and on demand - you can use an extension such as Xdebug helper in Chrome to send the Cookie required to activate a debugging session ([Jetbrains article](https://confluence.jetbrains.com/display/PhpStorm/Configure+Xdebug+Helper+for+Chrome+to+be+used+with+PhpStorm)). - -Xdebug is set up to communicate with the host machine on port 9001 to avoid clashes with any locally installed PHP-fpm. - ## Browser Testing We like [Laravel Dusk](https://laravel.com/docs/5.6/dusk), and also help with [Orchestra Testbench Dusk](https://github.com/orchestral/testbench-dusk) for package development. Porter provides a browser container with Chrome and Chromedriver for browser testing. @@ -250,14 +273,6 @@ We do not recommend using them at the moment, due to the way Porter is set up, t - `porter mutagen:on` - `porter mutagen:off` -Adding these items required the addition of some core events to allow intercepting the docker-compose.yaml file production process and the starting/stopping of containers. To do this a number of events were introduced. - - - `App\Events\StartingPorter` - - `App\Events\StartingPorterService($service)` - - `App\Events\StoppingPorter` - - `App\Events\StoppingPorterService($service)` - - `App\Events\BuiltDockerCompose($dockerComposeFilePath)` - ## Tweaking things As Porter is based on Docker, it is easy to add new containers as required or to adjust the way the existing containers are built. @@ -282,3 +297,16 @@ We store personal config in the `.porter` directory in your home directory - kee - `ssl` - the generated SSL certificates used by Porter - `views` - allows the override and addition of views for building NGiNX configurations for example - an `image_sets` directory can be added to include alternative docker scripts similar to the original `konsulting/porter-ubuntu` in the project's `resources/image_sets` directory, and the `docker-compose.yaml` views + +### Events + +Porter emits certain events as it performs actions. We use these to hook in for the DockerSync, Mutagen, Valet and Xdebug - using event subscribers. + +- `App\Events\StartingPorter` +- `App\Events\StartingPorterService($service)` +- `App\Events\StoppingPorter` +- `App\Events\StoppingPorterService($service)` +- `App\Events\BuiltDockerCompose($dockerComposeFilePath)` +- `App\Events\SiteSecured($site)` +- `App\Events\SiteUnsecured($site)` +- `App\Events\SiteRemoved($site)` diff --git a/app/Commands/DockerCompose/SoftRestart.php b/app/Commands/DockerCompose/SoftRestart.php new file mode 100644 index 0000000..a1b81f0 --- /dev/null +++ b/app/Commands/DockerCompose/SoftRestart.php @@ -0,0 +1,32 @@ +porter->softRestart((string) $this->argument('service')); + } +} diff --git a/app/Commands/Images/Build.php b/app/Commands/Images/Build.php index 8e9ab7f..258e335 100644 --- a/app/Commands/Images/Build.php +++ b/app/Commands/Images/Build.php @@ -30,6 +30,6 @@ class Build extends BaseCommand */ public function handle(): void { - app(Organiser::class)->buildImages((string) $this->argument('service'), (string) $this->option('fresh')); + app(Organiser::class)->buildImages((string) $this->argument('service'), (bool) $this->option('fresh')); } } diff --git a/app/Commands/MySql/Open.php b/app/Commands/MySql/Open.php index e26f4ad..4a166e6 100644 --- a/app/Commands/MySql/Open.php +++ b/app/Commands/MySql/Open.php @@ -27,7 +27,7 @@ class Open extends BaseCommand */ public function handle(): void { - if (setting('use_mysql') !== 'on') { + if (setting('use_mysql', 'on') !== 'on') { $this->error('Not using docker mysql'); return; diff --git a/app/Commands/Php/XdebugStatus.php b/app/Commands/Php/XdebugStatus.php new file mode 100644 index 0000000..807bcd1 --- /dev/null +++ b/app/Commands/Php/XdebugStatus.php @@ -0,0 +1,47 @@ +argument('status')); + + if (!in_array($status, ['on', 'off'])) { + throw new \Exception('Xdebug can only be turned on or off.'); + } + + if ($status === 'on') { + app(XDebug::class)->turnOn(); + + return; + } + + app(XDebug::class)->turnOff(); + } +} diff --git a/app/Commands/Valet/Off.php b/app/Commands/Valet/Off.php new file mode 100644 index 0000000..429faf4 --- /dev/null +++ b/app/Commands/Valet/Off.php @@ -0,0 +1,33 @@ +turnOff(); + } +} diff --git a/app/Commands/Valet/On.php b/app/Commands/Valet/On.php new file mode 100644 index 0000000..562eba6 --- /dev/null +++ b/app/Commands/Valet/On.php @@ -0,0 +1,33 @@ +turnOn(); + } +} diff --git a/app/Events/SiteRemoved.php b/app/Events/SiteRemoved.php new file mode 100644 index 0000000..01294d2 --- /dev/null +++ b/app/Events/SiteRemoved.php @@ -0,0 +1,15 @@ +site = $site; + } +} diff --git a/app/Events/SiteSecured.php b/app/Events/SiteSecured.php new file mode 100644 index 0000000..55c4fd8 --- /dev/null +++ b/app/Events/SiteSecured.php @@ -0,0 +1,15 @@ +site = $site; + } +} diff --git a/app/Events/SiteUnsecured.php b/app/Events/SiteUnsecured.php new file mode 100644 index 0000000..5ede6c4 --- /dev/null +++ b/app/Events/SiteUnsecured.php @@ -0,0 +1,15 @@ +site = $site; + } +} diff --git a/app/Models/Site.php b/app/Models/Site.php index ed7800e..ebe163e 100644 --- a/app/Models/Site.php +++ b/app/Models/Site.php @@ -2,6 +2,9 @@ namespace App\Models; +use App\Events\SiteRemoved; +use App\Events\SiteSecured; +use App\Events\SiteUnsecured; use App\Porter; use App\PorterLibrary; use App\Support\Contracts\Cli; @@ -135,6 +138,8 @@ public function secure() $this->buildFiles(); + event(new SiteSecured($this)); + $this->getPorter()->restartServing(); } @@ -149,6 +154,8 @@ public function unsecure() $this->buildFiles(); + event(new SiteUnsecured($this)); + $this->getPorter()->restartServing(); } @@ -166,6 +173,8 @@ public function remove() $this->delete(); + event(new SiteRemoved($this)); + $this->getPorter()->restartServing(); } diff --git a/app/Porter.php b/app/Porter.php index 0fc6f97..8b223ec 100644 --- a/app/Porter.php +++ b/app/Porter.php @@ -143,6 +143,17 @@ public function restart($service = null) $this->start($service, true); } + /** + * Soft restart Porter containers. + * Restart without getting config changes. + * + * @param string|null $service + */ + public function softRestart($service = null) + { + $this->dockerCompose->command("restart {$service}")->realTime()->perform(); + } + /** * Restart serving, picking up changes in used PHP versions and NGiNX. */ diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 93f3def..bb149a3 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -4,6 +4,8 @@ use App\Support\DockerSync\EventSubscriber as DockerSyncSubscriber; use App\Support\Mutagen\EventSubscriber as MutagenSubscriber; +use App\Support\Valet\EventSubscriber as ValetSubscriber; +use App\Support\XDebug\EventSubscriber as XdebugSubscriber; class EventServiceProvider extends \Illuminate\Foundation\Support\Providers\EventServiceProvider { @@ -20,7 +22,9 @@ class EventServiceProvider extends \Illuminate\Foundation\Support\Providers\Even * @var array */ protected $subscribe = [ - MutagenSubscriber::class, DockerSyncSubscriber::class, + MutagenSubscriber::class, + ValetSubscriber::class, + XdebugSubscriber::class, ]; } diff --git a/app/Support/Console/Cli.php b/app/Support/Console/Cli.php index 0808a17..9b33285 100644 --- a/app/Support/Console/Cli.php +++ b/app/Support/Console/Cli.php @@ -35,7 +35,7 @@ public function exec($command) * * @param string $command * - * @return string + * @return int */ public function execRealTime($command) { @@ -48,6 +48,8 @@ public function execRealTime($command) } catch (ProcessFailedException $e) { echo $e->getMessage(); } + + return $process->getExitCode(); } /** @@ -55,7 +57,7 @@ public function execRealTime($command) * * @param string $command * - * @return void + * @return int */ public function passthru($command) { @@ -69,6 +71,8 @@ public function passthru($command) } catch (ProcessFailedException $e) { echo $e->getMessage(); } + + return $process->getExitCode(); } /** diff --git a/app/Support/Console/DockerCompose/CliCommand.php b/app/Support/Console/DockerCompose/CliCommand.php index 598d95f..8ec94ee 100644 --- a/app/Support/Console/DockerCompose/CliCommand.php +++ b/app/Support/Console/DockerCompose/CliCommand.php @@ -141,14 +141,12 @@ public function prepare() /** * Execute the command. * - * @return string|null + * @return string|int */ public function perform() { if ($this->isInteractive()) { - $this->cli->passthru($this->prepare()); - - return; + return $this->cli->passthru($this->prepare()); } if ($this->isRealTime()) { diff --git a/app/Support/Console/DockerCompose/YamlBuilder.php b/app/Support/Console/DockerCompose/YamlBuilder.php index 7153d4c..5fe6791 100644 --- a/app/Support/Console/DockerCompose/YamlBuilder.php +++ b/app/Support/Console/DockerCompose/YamlBuilder.php @@ -53,7 +53,7 @@ public function build(ImageRepository $imageSet) */ public function renderDockerComposeFile(ImageRepository $imageSet) { - return view("{$imageSet->getName()}::base")->with([ + return (string) view("{$imageSet->getName()}::base")->with([ 'home' => setting('home'), 'host_machine_name' => setting('host_machine_name'), 'activePhpVersions' => PhpVersion::active()->get(), @@ -61,6 +61,8 @@ public function renderDockerComposeFile(ImageRepository $imageSet) 'useRedis' => setting('use_redis') === 'on', 'useBrowser' => setting('use_browser') === 'on', 'useDns' => setting('use_dns') === 'on' || setting_missing('use_dns'), + 'httpPort' => setting('http_port', 80), + 'httpsPort' => setting('https_port', 443), 'imageSet' => $imageSet, 'libraryPath' => $this->porterLibrary->path(), ])->render(); diff --git a/app/Support/Contracts/Cli.php b/app/Support/Contracts/Cli.php index 50a7f35..3bb2194 100644 --- a/app/Support/Contracts/Cli.php +++ b/app/Support/Contracts/Cli.php @@ -18,7 +18,7 @@ public function exec($command); * * @param string $command * - * @return string + * @return int */ public function execRealTime($command); @@ -27,7 +27,7 @@ public function execRealTime($command); * * @param string $command * - * @return void + * @return int */ public function passthru($command); diff --git a/app/Support/DockerSync/EventSubscriber.php b/app/Support/DockerSync/EventSubscriber.php index f8b30b7..c8c56c1 100644 --- a/app/Support/DockerSync/EventSubscriber.php +++ b/app/Support/DockerSync/EventSubscriber.php @@ -19,22 +19,22 @@ public function __construct(DockerSync $dockerSync) $this->dockerSync = $dockerSync; } - public function startAll(StartingPorter $event) + public function startAll() { $this->dockerSync->startDaemon(); } - public function startForService(StartingPorterService $event) + public function startForService() { $this->dockerSync->startDaemon(); } - public function stopAll(StoppingPorter $event) + public function stopAll() { $this->dockerSync->stopDaemon(); } - public function stopForService(StoppingPorterService $event) + public function stopForService() { $this->dockerSync->stopDaemon(); } diff --git a/app/Support/Mutagen/EventSubscriber.php b/app/Support/Mutagen/EventSubscriber.php index a89a53c..b1ced71 100644 --- a/app/Support/Mutagen/EventSubscriber.php +++ b/app/Support/Mutagen/EventSubscriber.php @@ -19,24 +19,24 @@ public function __construct(Mutagen $mutagen) $this->mutagen = $mutagen; } - public function startAll(StartedPorter $event) + public function startAll() { $this->mutagen->startDaemon(); $this->mutagen->syncVolumes(); } - public function startForService(StartedPorterService $event) + public function startForService() { $this->mutagen->startDaemon(); $this->mutagen->syncVolumes(); } - public function stopAll(StoppedPorter $event) + public function stopAll() { $this->mutagen->stopDaemon(); } - public function stopForService(StoppedPorterService $event) + public function stopForService() { $this->mutagen->stopDaemon(); } diff --git a/app/Support/Valet/EventSubscriber.php b/app/Support/Valet/EventSubscriber.php new file mode 100644 index 0000000..bcde361 --- /dev/null +++ b/app/Support/Valet/EventSubscriber.php @@ -0,0 +1,59 @@ +valet = $valet; + } + + public function siteSecured(SiteSecured $event) + { + if (setting('use_valet', 'off') === 'off') { + return; + } + + $this->valet->addSite($event->site); + } + + public function siteUnsecured(SiteUnsecured $event) + { + if (setting('use_valet', 'off') === 'off') { + return; + } + + $this->valet->addSite($event->site); + } + + public function siteRemoved(SiteRemoved $event) + { + if (setting('use_valet', 'off') === 'off') { + return; + } + + $this->valet->removeSite($event->site); + } + + /** + * Register the listeners for the subscriber. + * + * @param \Illuminate\Events\Dispatcher $events + * + * @return void + */ + public function subscribe($events) + { + $events->listen(SiteSecured::class, static::class.'@siteSecured'); + $events->listen(SiteUnsecured::class, static::class.'@siteUnsecured'); + $events->listen(SiteRemoved::class, static::class.'@siteRemoved'); + } +} diff --git a/app/Support/Valet/Valet.php b/app/Support/Valet/Valet.php new file mode 100644 index 0000000..bd66366 --- /dev/null +++ b/app/Support/Valet/Valet.php @@ -0,0 +1,143 @@ +porter = $porter; + $this->cli = $cli; + $this->writer = $writer; + } + + public function turnOn() + { + if (setting('use_valet', 'off') === 'on') { + $this->writer->info('Valet compatibility already complete'); + + return; + } + + $this->sudoWarning(); + + Setting::updateOrCreate('valet', 'on'); + Setting::updateOrCreate('http_port', static::COMPAT_HTTP_PORT); + Setting::updateOrCreate('https_port', static::COMPAT_HTTPS_PORT); + + Artisan::call('dns:off'); + $this->cli->execRealTime('valet start'); + + $this->porter->compose(); + $this->porter->restart(); + + // Run through all the porter sites and set up a proxy though valet... + Site::all()->each(function (Site $site) { + $this->addSite($site); + }); + + $this->writer->line('Completed setting up valet compatibility'); + } + + public function turnOff() + { + if (setting('use_valet', 'off') === 'off') { + $this->writer->info('Valet compatibility already off'); + + return; + } + + $this->sudoWarning(); + + // Run through all the porter sites and set up a proxy though valet... + Site::all()->each(function (Site $site) { + $this->removeSite($site); + }); + + Setting::updateOrCreate('valet', 'off'); + Setting::updateOrCreate('http_port', static::HTTP_PORT); + Setting::updateOrCreate('https_port', static::HTTPS_PORT); + + $this->cli->execRealTime('valet stop'); + $this->cli->execRealTime('sudo brew services stop dnsmasq'); + + Artisan::call('dns:on'); + + $this->porter->compose(); + $this->porter->restart(); + + $this->writer->line('Completed removing valet compatibility'); + } + + public function addSite(Site $site) + { + $this->sudoWarning(); + + if ($this->isProxied($site)) { + $this->removeSite($site); + } + + $port = $site->secure ? static::COMPAT_HTTPS_PORT : static::COMPAT_HTTP_PORT; + $protocol = $site->secure ? 'https://' : 'http://'; + + $this->cli->exec("valet proxy {$site->name} {$protocol}127.0.0.1:{$port}"); + + $this->writer->line("Added {$site->name} proxy for Valet"); + } + + public function removeSite(Site $site) + { + $this->sudoWarning(); + + $this->cli->exec("valet unproxy {$site->name}"); + + $this->writer->line("Removed Valet proxy for {$site->name}"); + } + + public function listSites() + { + $this->sudoWarning(); + + return $this->cli->exec('valet proxies'); + } + + public function isProxied(Site $site) + { + return Str::contains($this->listSites(), $site->name); + } + + protected function sudoWarning() + { + if ($this->hasWarned) { + return; + } + + $this->writer->info('Requires Sudo permissions for Valet'); + $this->hasWarned = true; + } +} diff --git a/app/Support/XDebug/EventSubscriber.php b/app/Support/XDebug/EventSubscriber.php new file mode 100644 index 0000000..c4b08e6 --- /dev/null +++ b/app/Support/XDebug/EventSubscriber.php @@ -0,0 +1,39 @@ +xdebug = $xdebug; + } + + public function setXDebug() + { + if (setting('use_xdebug', 'on') === 'off') { + $this->xdebug->turnOff(); + } + + $this->xdebug->turnOn(); + } + + /** + * Register the listeners for the subscriber. + * + * @param \Illuminate\Events\Dispatcher $events + * + * @return void + */ + public function subscribe($events) + { + $events->listen(StartedPorter::class, static::class.'@setXDebug'); + $events->listen(StartedPorterService::class, static::class.'@setXDebug'); + } +} diff --git a/app/Support/XDebug/XDebug.php b/app/Support/XDebug/XDebug.php new file mode 100644 index 0000000..5b2650b --- /dev/null +++ b/app/Support/XDebug/XDebug.php @@ -0,0 +1,101 @@ +porter = $porter; + $this->dockerCompose = $dockerCompose; + } + + public function turnOn() + { + Setting::updateOrCreate('use_xdebug', 'on'); + + foreach (PhpVersion::active()->get() as $version) { + if (!$this->enable($version)) { + // If enable fails it's because it was already enabled + continue; + } + + $this->porter->softRestart($version->getFpmNameAttribute()); + } + } + + public function turnOff() + { + Setting::updateOrCreate('use_xdebug', 'off'); + + foreach (PhpVersion::active()->get() as $version) { + if (!$this->disable($version)) { + // If disable fails it's because it was already disabled + continue; + } + + $this->porter->softRestart($version->getFpmNameAttribute()); + } + } + + /** + * Move the ini file to .ini to enable. + * + * @param PhpVersion $version + * + * @return bool + */ + protected function enable(PhpVersion $version) + { + return $this->moveIniFile($version, 'xdebug.bak', 'xdebug.ini'); + } + + /** + * Move the ini file to .bak to disable. + * + * @param PhpVersion $version + * + * @return bool + */ + protected function disable(PhpVersion $version) + { + return $this->moveIniFile($version, 'xdebug.ini', 'xdebug.bak'); + } + + /** + * Move the ini file, If it fails to move, return false;. + * + * @param PhpVersion $version + * @param $from + * @param $to + * + * @return bool + */ + protected function moveIniFile(PhpVersion $version, $from, $to) + { + $move = "mv /etc/php/{$version->version_number}/mods-available/{$from} /etc/php/{$version->version_number}/mods-available/{$to}"; + + // We wrap in a buffer because we don't want everything output to the screen. + // If exit code !== 0, its because the move failed since the starting file + // didn't exist + ob_start(); + $exitCode = $this->dockerCompose->execContainer($version->getFpmNameAttribute()) + ->append($move) + ->interactive() + ->perform(); + ob_end_clean(); + + return !(bool) $exitCode; + } +} diff --git a/resources/image_sets/konsulting/porter-ubuntu/docker_compose/nginx.blade.php b/resources/image_sets/konsulting/porter-ubuntu/docker_compose/nginx.blade.php index 1dbcbd1..b67157d 100644 --- a/resources/image_sets/konsulting/porter-ubuntu/docker_compose/nginx.blade.php +++ b/resources/image_sets/konsulting/porter-ubuntu/docker_compose/nginx.blade.php @@ -6,8 +6,8 @@ networks: - porter ports: - - 80:80 - - 443:443 + - {{ $httpPort }}:80 + - {{ $httpsPort }}:443 volumes: - {{ $home }}:/srv/app:delegated - {{ $libraryPath }}/config/nginx/nginx.conf:/etc/nginx/nginx.conf diff --git a/tests/Unit/PorterTest.php b/tests/Unit/PorterTest.php index f9416ef..62dae2a 100644 --- a/tests/Unit/PorterTest.php +++ b/tests/Unit/PorterTest.php @@ -11,6 +11,7 @@ use App\Support\Contracts\Cli; use App\Support\Images\ImageSetRepository; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Facades\Event; use Mockery\MockInterface; use Tests\BaseTestCase; @@ -96,9 +97,21 @@ public function it_restarts_the_porter_containers() $this->porter->restart('myService'); } + /** @test */ + public function it_soft_restarts_the_porter_containers() + { + $this->expectCommand('docker-compose -f '.$this->composeFile.' -p porter restart', 'execRealTime'); + $this->porter->softRestart(); + + $this->expectCommand('docker-compose -f '.$this->composeFile.' -p porter restart myService', 'execRealTime'); + $this->porter->softRestart('myService'); + } + /** @test */ public function it_restarts_serving() { + Event::fake(); + factory(PhpVersion::class)->create([ 'version_number' => '7.2', 'default' => true, diff --git a/tests/Unit/Support/Console/CliTest.php b/tests/Unit/Support/Console/CliTest.php index 52f852b..0840d11 100644 --- a/tests/Unit/Support/Console/CliTest.php +++ b/tests/Unit/Support/Console/CliTest.php @@ -19,7 +19,7 @@ public function it_sets_the_timeout_on_the_process_object($method) $this->assertSame(config('porter.process_timeout'), $args['timeout']); return Mockery::mock(Process::class) - ->shouldReceive('run', 'mustRun', 'getOutput', 'setTty') + ->shouldReceive('run', 'mustRun', 'getOutput', 'setTty', 'getExitCode') ->getMock(); }); diff --git a/tests/Unit/Support/DockerSync/DockerSyncTest.php b/tests/Unit/Support/DockerSync/DockerSyncTest.php index 6f9f7dc..47552bd 100644 --- a/tests/Unit/Support/DockerSync/DockerSyncTest.php +++ b/tests/Unit/Support/DockerSync/DockerSyncTest.php @@ -1,6 +1,6 @@ cli = \Mockery::mock(Cli::class); + $this->writer = \Mockery::mock(ConsoleWriter::class); + } + + /** @test */ + public function it_will_turn_on_valet_compat() + { + factory(Site::class)->create(['name' => 'dummy']); + + $valet = new Valet($this->porter, $this->cli, $this->writer); + + $this->writer->shouldReceive('info') + ->with('Requires Sudo permissions for Valet') + ->once(); + + Artisan::shouldReceive('call')->with('dns:off')->once(); + + $this->cli->shouldReceive('execRealTime') + ->with('valet start') + ->once(); + + $this->porter->shouldReceive('compose')->once(); + $this->porter->shouldReceive('restart')->once(); + + $this->cli->shouldReceive('exec') + ->with('valet proxies') + ->once(); + + $this->cli->shouldReceive('exec') + ->with('valet proxy dummy http://127.0.0.1:8008') + ->once(); + + $this->writer->shouldReceive('line') + ->with('Added dummy proxy for Valet') + ->once(); + + $this->writer->shouldReceive('line') + ->with('Completed setting up valet compatibility') + ->once(); + + $valet->turnOn(); + } + + /** @test */ + public function it_will_not_turn_on_valet_if_already_on() + { + Setting::updateOrCreate('use_valet', 'on'); + + $valet = new Valet($this->porter, $this->cli, $this->writer); + + $this->writer->shouldReceive('info') + ->with('Valet compatibility already complete') + ->once(); + + $valet->turnOn(); + } + + /** @test */ + public function it_will_not_turn_off_valet_if_already_off() + { + Setting::updateOrCreate('use_valet', 'off'); + + $valet = new Valet($this->porter, $this->cli, $this->writer); + + $this->writer->shouldReceive('info') + ->with('Valet compatibility already off') + ->once(); + + $valet->turnOff(); + } + + /** @test */ + public function it_will_turn_off_valet_compat() + { + Setting::updateOrCreate('use_valet', 'on'); + + factory(Site::class)->create(['name' => 'dummy']); + + $valet = new Valet($this->porter, $this->cli, $this->writer); + + $this->writer->shouldReceive('info') + ->with('Requires Sudo permissions for Valet') + ->once(); + + $this->cli->shouldReceive('exec') + ->with('valet unproxy dummy') + ->once(); + + $this->writer->shouldReceive('line') + ->with('Removed Valet proxy for dummy') + ->once(); + + $this->cli->shouldReceive('execRealTime') + ->with('valet stop') + ->once(); + + $this->cli->shouldReceive('execRealTime') + ->with('sudo brew services stop dnsmasq') + ->once(); + + Artisan::shouldReceive('call')->with('dns:on')->once(); + + $this->porter->shouldReceive('compose')->once(); + $this->porter->shouldReceive('restart')->once(); + + $this->writer->shouldReceive('line') + ->with('Completed removing valet compatibility') + ->once(); + + $valet->turnOff(); + } + + /** @test */ + public function it_will_add_a_secure_site() + { + $site = factory(Site::class)->create(['name' => 'dummy', 'secure' => 1]); + + $valet = new Valet($this->porter, $this->cli, $this->writer); + + $this->writer->shouldReceive('info') + ->with('Requires Sudo permissions for Valet') + ->once(); + + $this->cli->shouldReceive('exec') + ->with('valet proxies') + ->once(); + + $this->cli->shouldReceive('exec') + ->with('valet proxy dummy https://127.0.0.1:8443') + ->once(); + + $this->writer->shouldReceive('line') + ->with('Added dummy proxy for Valet') + ->once(); + + $valet->addSite($site); + } + + /** @test */ + public function it_will_remove_an_existing_proxy_when_adding_a_site() + { + $site = factory(Site::class)->create(['name' => 'dummy', 'secure' => 1]); + + $valet = new Valet($this->porter, $this->cli, $this->writer); + + $this->writer->shouldReceive('info') + ->with('Requires Sudo permissions for Valet') + ->once(); + + $this->cli->shouldReceive('exec') + ->with('valet proxies') + ->once() + ->andReturn('dummy'); + + $this->cli->shouldReceive('exec') + ->with('valet unproxy dummy') + ->once(); + + $this->writer->shouldReceive('line') + ->with('Removed Valet proxy for dummy') + ->once(); + + $this->cli->shouldReceive('exec') + ->with('valet proxy dummy https://127.0.0.1:8443') + ->once(); + + $this->writer->shouldReceive('line') + ->with('Added dummy proxy for Valet') + ->once(); + + $valet->addSite($site); + } + + /** @test */ + public function it_will_remove_a_proxy() + { + $site = factory(Site::class)->create(['name' => 'dummy', 'secure' => 1]); + + $valet = new Valet($this->porter, $this->cli, $this->writer); + + $this->writer->shouldReceive('info') + ->with('Requires Sudo permissions for Valet') + ->once(); + + $this->cli->shouldReceive('exec') + ->with('valet unproxy dummy') + ->once(); + + $this->writer->shouldReceive('line') + ->with('Removed Valet proxy for dummy') + ->once(); + + $valet->removeSite($site); + } + + /** @test */ + public function it_will_list_sites_that_are_proxied_with_valet() + { + $valet = new Valet($this->porter, $this->cli, $this->writer); + + $this->writer->shouldReceive('info') + ->with('Requires Sudo permissions for Valet') + ->once(); + + $this->cli->shouldReceive('exec') + ->with('valet proxies') + ->once() + ->andReturn('dummy'); + + $this->assertEquals('dummy', $valet->listSites()); + } + + /** @test */ + public function it_can_check_if_a_site_is_proxied() + { + $site = factory(Site::class)->create(['name' => 'dummy', 'secure' => 1]); + $valet = new Valet($this->porter, $this->cli, $this->writer); + + $this->writer->shouldReceive('info') + ->with('Requires Sudo permissions for Valet') + ->once(); + + $this->cli->shouldReceive('exec') + ->with('valet proxies') + ->once(); + + $this->assertEquals(false, $valet->isProxied($site)); + + $this->cli->shouldReceive('exec') + ->with('valet proxies') + ->once() + ->andReturn('dummy'); + + $this->assertEquals(true, $valet->isProxied($site)); + } +} diff --git a/tests/Unit/Support/Xdebug/XdebugTest.php b/tests/Unit/Support/Xdebug/XdebugTest.php new file mode 100644 index 0000000..a42927a --- /dev/null +++ b/tests/Unit/Support/Xdebug/XdebugTest.php @@ -0,0 +1,173 @@ +mockDockerCompose(); + } + + /** @test */ + public function it_turns_xdebug_on() + { + $version = factory(PhpVersion::class) + ->create(['version_number' => '7.4', 'default' => true]); + + $command = \Mockery::mock(CliCommand::class); + + $xdebug = new XDebug($this->porter, $this->dockerCompose); + + $from = 'xdebug.bak'; + $to = 'xdebug.ini'; + + $this->dockerCompose->shouldReceive('execContainer') + ->with('php_fpm_7-4') + ->once() + ->andReturn($command); + + $command->shouldReceive('append') + ->with("mv /etc/php/{$version->version_number}/mods-available/{$from} /etc/php/{$version->version_number}/mods-available/{$to}") + ->once() + ->andReturn($command); + + $command->shouldReceive('interactive') + ->once() + ->andReturn($command); + + $command->shouldReceive('perform') + ->once() + ->andReturn(0); + + $this->porter->shouldReceive('softRestart') + ->with('php_fpm_7-4') + ->once(); + + $xdebug->turnOn(); + } + + /** @test */ + public function if_xdebug_is_already_on_we_dont_restart_the_containers() + { + $version = factory(PhpVersion::class) + ->create(['version_number' => '7.4', 'default' => true]); + + $command = \Mockery::mock(CliCommand::class); + + $xdebug = new XDebug($this->porter, $this->dockerCompose); + + $from = 'xdebug.bak'; + $to = 'xdebug.ini'; + + $this->dockerCompose->shouldReceive('execContainer') + ->with('php_fpm_7-4') + ->once() + ->andReturn($command); + + $command->shouldReceive('append') + ->with("mv /etc/php/{$version->version_number}/mods-available/{$from} /etc/php/{$version->version_number}/mods-available/{$to}") + ->once() + ->andReturn($command); + + $command->shouldReceive('interactive') + ->once() + ->andReturn($command); + + $command->shouldReceive('perform') + ->once() + ->andReturn(1); + + $this->porter->shouldNotReceive('softRestart') + ->with('php_fpm_7-4'); + + $xdebug->turnOn(); + } + + /** @test */ + public function it_turns_xdebug_off() + { + $version = factory(PhpVersion::class) + ->create(['version_number' => '7.4', 'default' => true]); + + $command = \Mockery::mock(CliCommand::class); + + $xdebug = new XDebug($this->porter, $this->dockerCompose); + + $from = 'xdebug.ini'; + $to = 'xdebug.bak'; + + $this->dockerCompose->shouldReceive('execContainer') + ->with('php_fpm_7-4') + ->once() + ->andReturn($command); + + $command->shouldReceive('append') + ->with("mv /etc/php/{$version->version_number}/mods-available/{$from} /etc/php/{$version->version_number}/mods-available/{$to}") + ->once() + ->andReturn($command); + + $command->shouldReceive('interactive') + ->once() + ->andReturn($command); + + $command->shouldReceive('perform') + ->once() + ->andReturn(0); + + $this->porter->shouldReceive('softRestart') + ->with('php_fpm_7-4') + ->once(); + + $xdebug->turnOff(); + } + + /** @test */ + public function if_xdebug_is_already_off_we_dont_restart_the_containers() + { + $version = factory(PhpVersion::class) + ->create(['version_number' => '7.4', 'default' => true]); + + $command = \Mockery::mock(CliCommand::class); + + $xdebug = new XDebug($this->porter, $this->dockerCompose); + + $from = 'xdebug.ini'; + $to = 'xdebug.bak'; + + $this->dockerCompose->shouldReceive('execContainer') + ->with('php_fpm_7-4') + ->once() + ->andReturn($command); + + $command->shouldReceive('append') + ->with("mv /etc/php/{$version->version_number}/mods-available/{$from} /etc/php/{$version->version_number}/mods-available/{$to}") + ->once() + ->andReturn($command); + + $command->shouldReceive('interactive') + ->once() + ->andReturn($command); + + $command->shouldReceive('perform') + ->once() + ->andReturn(1); + + $this->porter->shouldNotReceive('softRestart') + ->with('php_fpm_7-4'); + + $xdebug->turnOff(); + } +}