From ec38bb00d665be5ede5acb9c68712afbe25844e4 Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 17 Apr 2024 19:45:57 +0100 Subject: [PATCH 01/21] Added import command --- app/Commands/ProvisionCommand.php | 2 + app/Services/Forge/ForgeService.php | 6 + app/Services/Forge/ForgeSetting.php | 18 ++ app/Services/Forge/ForgeSiteCommandWaiter.php | 68 +++++ .../Forge/Pipeline/ImportDatabaseFromSql.php | 88 +++++++ config/forge.php | 9 + .../Forge/ForgeSiteCommandWaiterTest.php | 63 +++++ .../Pipeline/ImportDatabaseFromSqlTest.php | 248 ++++++++++++++++++ 8 files changed, 502 insertions(+) create mode 100644 app/Services/Forge/ForgeSiteCommandWaiter.php create mode 100644 app/Services/Forge/Pipeline/ImportDatabaseFromSql.php create mode 100644 tests/Unit/Services/Forge/ForgeSiteCommandWaiterTest.php create mode 100644 tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php diff --git a/app/Commands/ProvisionCommand.php b/app/Commands/ProvisionCommand.php index ceafd51..ce43449 100644 --- a/app/Commands/ProvisionCommand.php +++ b/app/Commands/ProvisionCommand.php @@ -22,6 +22,7 @@ use App\Services\Forge\Pipeline\EnsureJobScheduled; use App\Services\Forge\Pipeline\FindServer; use App\Services\Forge\Pipeline\FindSite; +use App\Services\Forge\Pipeline\ImportDatabaseFromSql; use App\Services\Forge\Pipeline\InstallGitRepository; use App\Services\Forge\Pipeline\NginxTemplateSearchReplace; use App\Services\Forge\Pipeline\ObtainLetsEncryptCertification; @@ -56,6 +57,7 @@ public function handle(ForgeService $service): void EnableQuickDeploy::class, UpdateEnvironmentVariables::class, UpdateDeployScript::class, + ImportDatabaseFromSql::class, DeploySite::class, RunOptionalCommands::class, EnsureJobScheduled::class, diff --git a/app/Services/Forge/ForgeService.php b/app/Services/Forge/ForgeService.php index 10a825d..59b0fa5 100644 --- a/app/Services/Forge/ForgeService.php +++ b/app/Services/Forge/ForgeService.php @@ -20,6 +20,7 @@ use Laravel\Forge\Forge; use Laravel\Forge\Resources\Server; use Laravel\Forge\Resources\Site; +use Laravel\Forge\Resources\SiteCommand; class ForgeService { @@ -149,4 +150,9 @@ public function siteDirectory(): string { return sprintf('/home/%s/%s', $this->site->username, $this->site->name); } + + public function waitForSiteCommand(SiteCommand $site_command): SiteCommand + { + return (new ForgeSiteCommandWaiter($this->forge))->waitFor($site_command); + } } diff --git a/app/Services/Forge/ForgeSetting.php b/app/Services/Forge/ForgeSetting.php index 24173fb..20621c7 100644 --- a/app/Services/Forge/ForgeSetting.php +++ b/app/Services/Forge/ForgeSetting.php @@ -123,6 +123,21 @@ class ForgeSetting */ public ?string $dbName; + /** + * Flag to importing database via seeding. + */ + public bool $dbImportSeed; + + /** + * Flag to importing database via SQL file. + */ + public ?string $dbImportSql; + + /** + * Flag to import database on deployment. + */ + public bool $dbImportOnDeployment; + /** * Flag to auto-source environment variables in deployment. */ @@ -226,6 +241,9 @@ protected function validate(array $configurations): \Illuminate\Validation\Valid 'job_scheduler_required' => ['boolean'], 'db_creation_required' => ['boolean'], 'db_name' => ['nullable', 'string'], + 'db_import_sql' => ['nullable', 'string'], + 'db_import_seed' => ['boolean'], + 'db_import_on_deployment' => ['boolean'], 'auto_source_required' => ['boolean'], 'ssl_required' => ['boolean'], 'wait_on_ssl' => ['boolean'], diff --git a/app/Services/Forge/ForgeSiteCommandWaiter.php b/app/Services/Forge/ForgeSiteCommandWaiter.php new file mode 100644 index 0000000..9a65242 --- /dev/null +++ b/app/Services/Forge/ForgeSiteCommandWaiter.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Services\Forge; + +use Laravel\Forge\Forge; +use Laravel\Forge\Resources\SiteCommand; +use Illuminate\Support\Sleep; + +class ForgeSiteCommandWaiter +{ + + /** + * The number of seconds to wait between querying Forge for the command status. + */ + public int $retrySeconds = 10; + + /** + * The number of attempts to make before returning the command. + */ + public int $maxAttempts = 60; + + /** + * The current number of attempts. + */ + protected int $attempts = 0; + + public function __construct(public Forge $forge) + { + } + + public function waitFor(SiteCommand $site_command): SiteCommand + { + $this->attempts = 0; + + while ( + $this->commandIsRunning($site_command) + && $this->attempts++ < $this->maxAttempts + ) { + Sleep::for($this->retrySeconds)->seconds(); + + $site_command = $this->forge->getSiteCommand( + $site_command->serverId, + $site_command->siteId, + $site_command->id + ); + } + + return $site_command; + } + + protected function commandIsRunning(SiteCommand $site_command): bool + { + return !isset($site_command->status) + || $site_command->status === 'running'; + } + +} \ No newline at end of file diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php new file mode 100644 index 0000000..93da856 --- /dev/null +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Services\Forge\Pipeline; + +use App\Services\Forge\ForgeService; +use App\Traits\Outputifier; +use Closure; + +class ImportDatabaseFromSql +{ + use Outputifier; + + public function __invoke(ForgeService $service, Closure $next) + { + if (!$service->site->siteNewlyMade && !$service->setting->dbImportOnDeployment) { + return $next($service); + } + + if (!($file = $service->setting->dbImportSql)) { + return $next($service); + } + + return $this->attemptImport($service, $next, $file); + } + + public function attemptImport(ForgeService $service, Closure $next, string $file) + { + $this->information(sprintf('Importing database from %s.', $file)); + + $content = $this->buildImportCommandContent($service, $file); + + $site_command = $service->waitForSiteCommand( + $service->forge->executeSiteCommand( + $service->setting->server, + $service->site->id, + ['command' => $content] + ) + ); + + if ($site_command->status === 'failed') { + $this->fail(sprintf('---> Database import command failed with message: %s', $site_command->output)); + return $next; + + } else if ($site_command->status !== 'finished') { + $this->fail('---> Database import command did not finish in time.'); + return $next; + } + + $this->information('---> Database import command finished successfully.'); + + return $next($service); + } + + public function buildImportCommandContent(ForgeService $service, string $file): string + { + $extract = match(pathinfo($file, PATHINFO_EXTENSION)) { + 'gz' => "gunzip < {$file}", + 'zip' => "unzip -p {$file}", + default => "cat {$file}" + }; + + return collect([ + $extract, + '|', + 'mysql', + '-u', + $service->database['DB_USERNAME'], + "-p{$service->database['DB_PASSWORD']}", + '-P', + $service->database['DB_PORT'], + isset($service->database['DB_HOST']) ? '-h ' . $service->database['DB_HOST'] : '', + $service->getFormattedDatabaseName(), + ]) + ->filter() + ->implode(' '); + } +} \ No newline at end of file diff --git a/config/forge.php b/config/forge.php index 0b71ebb..140a35c 100644 --- a/config/forge.php +++ b/config/forge.php @@ -58,6 +58,15 @@ // Override default database and database username, if needed. Defaults to the site name. 'db_name' => env('FORGE_DB_NAME', null), + // Import the database via a SQL file (default: null). + 'db_import_sql' => env('FORGE_DB_IMPORT_SQL', null), + + // Import the database via seeding (default: false). + 'db_import_seed' => env('FORGE_DB_IMPORT_SEED', false), + + // Flag to perform database import on deployment (default: false). + 'db_import_on_deployment' => env('FORGE_DB_IMPORT_ON_DEPLOYMENT', false), + // Flag to enable SSL certification (default: false). 'ssl_required' => env('FORGE_SSL_REQUIRED', false), diff --git a/tests/Unit/Services/Forge/ForgeSiteCommandWaiterTest.php b/tests/Unit/Services/Forge/ForgeSiteCommandWaiterTest.php new file mode 100644 index 0000000..cdc826b --- /dev/null +++ b/tests/Unit/Services/Forge/ForgeSiteCommandWaiterTest.php @@ -0,0 +1,63 @@ +maxAttempts = 3; + $waiter->retrySeconds = 5; + + Sleep::fake(); + + $forge->shouldReceive('getSiteCommand') + ->times($waiter->maxAttempts) + ->andReturn($site_command); + + $site_command = $waiter->waitFor($site_command); + + Sleep::assertSequence([ + Sleep::for($waiter->retrySeconds)->seconds(), + Sleep::for($waiter->retrySeconds)->seconds(), + Sleep::for($waiter->retrySeconds)->seconds(), + ]); +}); + +test('it waits until the command is no longer running', function() { + + $forge = Mockery::mock(Forge::class); + $site_command = Mockery::mock(SiteCommand::class); + $finished_command = Mockery::mock(SiteCommand::class); + $finished_command->status = 'finished'; + + $waiter = new ForgeSiteCommandWaiter($forge); + $waiter->maxAttempts = 10; + $waiter->retrySeconds = 5; + + Sleep::fake(); + + $forge->shouldReceive('getSiteCommand') + ->times(3) + ->andReturn( + $site_command, + $site_command, + $finished_command + ); + + $site_command = $waiter->waitFor($site_command); + + expect($site_command->status)->toBe($finished_command->status); + + Sleep::assertSequence([ + Sleep::for($waiter->retrySeconds)->seconds(), + Sleep::for($waiter->retrySeconds)->seconds(), + Sleep::for($waiter->retrySeconds)->seconds(), + ]); + +}); \ No newline at end of file diff --git a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php new file mode 100644 index 0000000..5b981c4 --- /dev/null +++ b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php @@ -0,0 +1,248 @@ +timeoutSeconds = 0; + foreach ($settings as $name => $value) { + $setting->{$name} = $value; + } + + $forge = Mockery::mock(Forge::class); + $forge->shouldReceive('setTimeout') + ->with($setting->timeoutSeconds); + + $service = Mockery::mock(ForgeService::class, [$setting, $forge])->makePartial(); + $service->site = new Site($site_attributes); + + return $service; +} + +test('it skips import when dbImportOnDeployment is false', function () { + + $service = configureMockService([ + 'dbImportOnDeployment' => false, + ], [ + 'siteNewlyMade' => false + ]); + + $pipe = Mockery::mock(ImportDatabaseFromSql::class) + ->makePartial(); + $pipe->shouldReceive('attemptImport') + ->never(); + + $next = fn() => true; + expect($pipe($service, $next))->toBe(true); +}); + +test('it skips import when siteNewlyMade is true and file is not present', function () { + + $service = configureMockService([ + 'dbImportOnDeployment' => false, + 'dbImportSql' => null, + ], [ + 'siteNewlyMade' => true + ]); + + $pipe = Mockery::mock(ImportDatabaseFromSql::class) + ->makePartial(); + $pipe->shouldReceive('attemptImport') + ->never(); + + $next = fn() => true; + expect($pipe($service, $next))->toBe(true); +}); + +test('it attempts import when siteNewlyMade is false, dbImportOnDeployment is true, and file is present', function () { + + $service = configureMockService([ + 'dbImportOnDeployment' => true, + 'dbImportSql' => 'xyz.sql', + ], [ + 'siteNewlyMade' => false, + ]); + + $next = fn() => true; + $pipe = Mockery::mock(ImportDatabaseFromSql::class) + ->makePartial(); + $pipe->shouldReceive('attemptImport') + ->once() + ->andReturn($next()); + + expect($pipe($service, $next))->toBe(true); +}); + +test('it attempts import when siteNewlyMade is true and file is present', function () { + + $service = configureMockService([ + 'dbImportOnDeployment' => false, + 'dbImportSql' => 'xyz.sql', + ], [ + 'siteNewlyMade' => true + ]); + + $next = fn() => true; + $pipe = Mockery::mock(ImportDatabaseFromSql::class) + ->makePartial(); + $pipe->shouldReceive('attemptImport') + ->once() + ->andReturn($next()); + + expect($pipe($service, $next))->toBe(true); +}); + +test('it generates import command for file with .gz extension', function () { + + $service = configureMockService([ + 'dbName' => 'my_db', + ]); + $service->setDatabase([ + 'DB_USERNAME' => 'foo', + 'DB_PASSWORD' => 'bar', + 'DB_HOST' => '1.2.3.4', + 'DB_PORT' => 1234, + ]); + + $pipe = new ImportDatabaseFromSql; + + expect($pipe->buildImportCommandContent($service, '/path/to/db.sql.gz')) + ->toBe('gunzip < /path/to/db.sql.gz | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db'); +}); + +test('it generates import command for file with .zip extension', function () { + + $service = configureMockService([ + 'dbName' => 'my_db', + ]); + $service->setDatabase([ + 'DB_USERNAME' => 'foo', + 'DB_PASSWORD' => 'bar', + 'DB_HOST' => '1.2.3.4', + 'DB_PORT' => 1234, + ]); + + $pipe = new ImportDatabaseFromSql; + + expect($pipe->buildImportCommandContent($service, '/path/to/db.sql.zip')) + ->toBe('unzip -p /path/to/db.sql.zip | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db'); +}); + +test('it generates import command for file with .sql extension', function () { + + $service = configureMockService([ + 'dbName' => 'my_db', + ]); + $service->setDatabase([ + 'DB_USERNAME' => 'foo', + 'DB_PASSWORD' => 'bar', + 'DB_HOST' => '1.2.3.4', + 'DB_PORT' => 1234, + ]); + + $pipe = new ImportDatabaseFromSql; + + expect($pipe->buildImportCommandContent($service, '/path/to/db.sql')) + ->toBe('cat /path/to/db.sql | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db'); +}); + +test('it executes import command with finished response', function() { + + $service = configureMockService([ + 'dbName' => 'my_db', + 'server' => 1, + ], [ + 'id' => 2, + ]); + $service->setDatabase([ + 'DB_USERNAME' => 'foo', + 'DB_PASSWORD' => 'bar', + 'DB_HOST' => '1.2.3.4', + 'DB_PORT' => 1234, + ]); + + $site_command = Mockery::mock(SiteCommand::class); + $site_command->status = 'finished'; + + $service->forge->shouldReceive('executeSiteCommand') + ->with(1, 2, ['command' => 'cat x.sql | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db']) + ->once() + ->andReturn($site_command); + + $next = fn() => true; + + $pipe = new ImportDatabaseFromSql; + $result = $pipe->attemptImport( + $service, + $next, + 'x.sql' + ); + + expect($result)->toBe(true); +}); + +test('it executes import command with failure status', function() { + + $service = configureMockService([ + 'dbName' => 'my_db', + 'server' => 1, + ], [ + 'id' => 2, + ]); + + $site_command = Mockery::mock(SiteCommand::class); + $site_command->status = 'failed'; + $site_command->output = 'oops'; + + $service->forge->shouldReceive('executeSiteCommand') + ->once() + ->andReturn($site_command); + + $next = fn() => true; + + $pipe = new ImportDatabaseFromSql; + $result = $pipe->attemptImport( + $service, + $next, + 'x.sql' + ); + + expect($result)->toBe($next); +}); + +test('it executes import command with missing status', function() { + + $service = configureMockService([ + 'dbName' => 'my_db', + 'server' => 1, + ], [ + 'id' => 2, + ]); + + $site_command = Mockery::mock(SiteCommand::class); + + $service->forge->shouldReceive('executeSiteCommand') + ->once() + ->andReturn($site_command); + + $service->shouldReceive('waitForSiteCommand') + ->with($site_command) + ->andReturn($site_command); + + $next = fn() => true; + + $pipe = new ImportDatabaseFromSql; + $result = $pipe->attemptImport( + $service, + $next, + 'x.sql' + ); + + expect($result)->toBe($next); +}); \ No newline at end of file From cca9e92a4e44aed60009b5f1838a0e04c63e631a Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 09:55:44 +0100 Subject: [PATCH 02/21] Require fork --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e139f73..bfbcd26 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "mehrancodes/laravel-harbor", + "name": "gbradley/laravel-harbor", "description": "A CLI tool to Quickly create On-Demand preview environment for your apps.", "keywords": ["php", "laravel-harbor", "laravel-zero", "console", "cli", "continuous-integration", "ci", "laravel-forge", "provision", "staging", "preview", "pull-requests"], "homepage": "https://laravel-harbor.com", From 30b63cdc25a6f9fbe3b97f9cbc8ff0ef38c5ab58 Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 10:16:10 +0100 Subject: [PATCH 03/21] Fixed tests --- .../Forge/Pipeline/ImportDatabaseFromSql.php | 8 ++-- .../Pipeline/ImportDatabaseFromSqlTest.php | 47 +++++++++---------- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php index 93da856..e2c78d0 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php @@ -23,7 +23,7 @@ class ImportDatabaseFromSql public function __invoke(ForgeService $service, Closure $next) { - if (!$service->site->siteNewlyMade && !$service->setting->dbImportOnDeployment) { + if (!$service->siteNewlyMade && !$service->setting->dbImportOnDeployment) { return $next($service); } @@ -39,7 +39,7 @@ public function attemptImport(ForgeService $service, Closure $next, string $file $this->information(sprintf('Importing database from %s.', $file)); $content = $this->buildImportCommandContent($service, $file); - + $site_command = $service->waitForSiteCommand( $service->forge->executeSiteCommand( $service->setting->server, @@ -52,7 +52,7 @@ public function attemptImport(ForgeService $service, Closure $next, string $file $this->fail(sprintf('---> Database import command failed with message: %s', $site_command->output)); return $next; - } else if ($site_command->status !== 'finished') { + } elseif ($site_command->status !== 'finished') { $this->fail('---> Database import command did not finish in time.'); return $next; } @@ -85,4 +85,4 @@ public function buildImportCommandContent(ForgeService $service, string $file): ->filter() ->implode(' '); } -} \ No newline at end of file +} diff --git a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php index 5b981c4..6fee21b 100644 --- a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php +++ b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php @@ -29,8 +29,6 @@ function configureMockService(array $settings = [], array $site_attributes = []) $service = configureMockService([ 'dbImportOnDeployment' => false, - ], [ - 'siteNewlyMade' => false ]); $pipe = Mockery::mock(ImportDatabaseFromSql::class) @@ -38,7 +36,7 @@ function configureMockService(array $settings = [], array $site_attributes = []) $pipe->shouldReceive('attemptImport') ->never(); - $next = fn() => true; + $next = fn () => true; expect($pipe($service, $next))->toBe(true); }); @@ -47,16 +45,16 @@ function configureMockService(array $settings = [], array $site_attributes = []) $service = configureMockService([ 'dbImportOnDeployment' => false, 'dbImportSql' => null, - ], [ - 'siteNewlyMade' => true ]); + $service->siteNewlyMade = true; + $pipe = Mockery::mock(ImportDatabaseFromSql::class) ->makePartial(); $pipe->shouldReceive('attemptImport') ->never(); - $next = fn() => true; + $next = fn () => true; expect($pipe($service, $next))->toBe(true); }); @@ -65,17 +63,15 @@ function configureMockService(array $settings = [], array $site_attributes = []) $service = configureMockService([ 'dbImportOnDeployment' => true, 'dbImportSql' => 'xyz.sql', - ], [ - 'siteNewlyMade' => false, ]); - $next = fn() => true; + $next = fn () => true; $pipe = Mockery::mock(ImportDatabaseFromSql::class) ->makePartial(); $pipe->shouldReceive('attemptImport') ->once() ->andReturn($next()); - + expect($pipe($service, $next))->toBe(true); }); @@ -84,11 +80,10 @@ function configureMockService(array $settings = [], array $site_attributes = []) $service = configureMockService([ 'dbImportOnDeployment' => false, 'dbImportSql' => 'xyz.sql', - ], [ - 'siteNewlyMade' => true ]); + $service->siteNewlyMade = true; - $next = fn() => true; + $next = fn () => true; $pipe = Mockery::mock(ImportDatabaseFromSql::class) ->makePartial(); $pipe->shouldReceive('attemptImport') @@ -110,7 +105,7 @@ function configureMockService(array $settings = [], array $site_attributes = []) 'DB_PORT' => 1234, ]); - $pipe = new ImportDatabaseFromSql; + $pipe = new ImportDatabaseFromSql(); expect($pipe->buildImportCommandContent($service, '/path/to/db.sql.gz')) ->toBe('gunzip < /path/to/db.sql.gz | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db'); @@ -128,7 +123,7 @@ function configureMockService(array $settings = [], array $site_attributes = []) 'DB_PORT' => 1234, ]); - $pipe = new ImportDatabaseFromSql; + $pipe = new ImportDatabaseFromSql(); expect($pipe->buildImportCommandContent($service, '/path/to/db.sql.zip')) ->toBe('unzip -p /path/to/db.sql.zip | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db'); @@ -146,13 +141,13 @@ function configureMockService(array $settings = [], array $site_attributes = []) 'DB_PORT' => 1234, ]); - $pipe = new ImportDatabaseFromSql; + $pipe = new ImportDatabaseFromSql(); expect($pipe->buildImportCommandContent($service, '/path/to/db.sql')) ->toBe('cat /path/to/db.sql | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db'); }); -test('it executes import command with finished response', function() { +test('it executes import command with finished response', function () { $service = configureMockService([ 'dbName' => 'my_db', @@ -175,9 +170,9 @@ function configureMockService(array $settings = [], array $site_attributes = []) ->once() ->andReturn($site_command); - $next = fn() => true; + $next = fn () => true; - $pipe = new ImportDatabaseFromSql; + $pipe = new ImportDatabaseFromSql(); $result = $pipe->attemptImport( $service, $next, @@ -187,7 +182,7 @@ function configureMockService(array $settings = [], array $site_attributes = []) expect($result)->toBe(true); }); -test('it executes import command with failure status', function() { +test('it executes import command with failure status', function () { $service = configureMockService([ 'dbName' => 'my_db', @@ -204,9 +199,9 @@ function configureMockService(array $settings = [], array $site_attributes = []) ->once() ->andReturn($site_command); - $next = fn() => true; + $next = fn () => true; - $pipe = new ImportDatabaseFromSql; + $pipe = new ImportDatabaseFromSql(); $result = $pipe->attemptImport( $service, $next, @@ -216,7 +211,7 @@ function configureMockService(array $settings = [], array $site_attributes = []) expect($result)->toBe($next); }); -test('it executes import command with missing status', function() { +test('it executes import command with missing status', function () { $service = configureMockService([ 'dbName' => 'my_db', @@ -235,9 +230,9 @@ function configureMockService(array $settings = [], array $site_attributes = []) ->with($site_command) ->andReturn($site_command); - $next = fn() => true; + $next = fn () => true; - $pipe = new ImportDatabaseFromSql; + $pipe = new ImportDatabaseFromSql(); $result = $pipe->attemptImport( $service, $next, @@ -245,4 +240,4 @@ function configureMockService(array $settings = [], array $site_attributes = []) ); expect($result)->toBe($next); -}); \ No newline at end of file +}); From f0bafddd438b5ff3e47d97c80b97d9803cb517b6 Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 10:25:21 +0100 Subject: [PATCH 04/21] Handle unspecified port --- app/Services/Forge/Pipeline/ImportDatabaseFromSql.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php index e2c78d0..8a54190 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php @@ -77,8 +77,7 @@ public function buildImportCommandContent(ForgeService $service, string $file): '-u', $service->database['DB_USERNAME'], "-p{$service->database['DB_PASSWORD']}", - '-P', - $service->database['DB_PORT'], + isset($service->database['DB_PORT']) ? '-P ' . $service->database['DB_PORT'] : '', isset($service->database['DB_HOST']) ? '-h ' . $service->database['DB_HOST'] : '', $service->getFormattedDatabaseName(), ]) From 89ec477958dcba3fc1b23e2aba5a89f469af2156 Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 10:43:14 +0100 Subject: [PATCH 05/21] debugging site command --- app/Services/Forge/ForgeSiteCommandWaiter.php | 66 +++++++++---------- .../Forge/Pipeline/ImportDatabaseFromSql.php | 14 ++-- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/app/Services/Forge/ForgeSiteCommandWaiter.php b/app/Services/Forge/ForgeSiteCommandWaiter.php index 9a65242..cc17ef8 100644 --- a/app/Services/Forge/ForgeSiteCommandWaiter.php +++ b/app/Services/Forge/ForgeSiteCommandWaiter.php @@ -19,50 +19,50 @@ class ForgeSiteCommandWaiter { - - /** + /** * The number of seconds to wait between querying Forge for the command status. */ - public int $retrySeconds = 10; - - /** + public int $retrySeconds = 10; + + /** * The number of attempts to make before returning the command. */ - public int $maxAttempts = 60; + public int $maxAttempts = 60; - /** + /** * The current number of attempts. */ - protected int $attempts = 0; + protected int $attempts = 0; - public function __construct(public Forge $forge) - { - } + public function __construct(public Forge $forge) + { + } - public function waitFor(SiteCommand $site_command): SiteCommand - { - $this->attempts = 0; + public function waitFor(SiteCommand $site_command): SiteCommand + { + $this->attempts = 0; - while ( - $this->commandIsRunning($site_command) - && $this->attempts++ < $this->maxAttempts - ) { - Sleep::for($this->retrySeconds)->seconds(); + while ( + $this->commandIsRunning($site_command) + && $this->attempts++ < $this->maxAttempts + ) { + Sleep::for($this->retrySeconds)->seconds(); - $site_command = $this->forge->getSiteCommand( - $site_command->serverId, - $site_command->siteId, - $site_command->id - ); - } + $site_command = $this->forge->getSiteCommand( + $site_command->serverId, + $site_command->siteId, + $site_command->id + ); + } - return $site_command; - } + return $site_command; + } - protected function commandIsRunning(SiteCommand $site_command): bool - { - return !isset($site_command->status) - || $site_command->status === 'running'; - } + protected function commandIsRunning(SiteCommand $site_command): bool + { + echo sprintf('site command status: %s', $site_command->status); + return !isset($site_command->status) + || $site_command->status === 'running'; + } -} \ No newline at end of file +} diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php index 8a54190..55f2d3c 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php @@ -40,12 +40,16 @@ public function attemptImport(ForgeService $service, Closure $next, string $file $content = $this->buildImportCommandContent($service, $file); + $site_command = $service->forge->executeSiteCommand( + $service->setting->server, + $service->site->id, + ['command' => $content] + ); + + $this->information('---> site command status: %s', $site_command->status); + $site_command = $service->waitForSiteCommand( - $service->forge->executeSiteCommand( - $service->setting->server, - $service->site->id, - ['command' => $content] - ) + $site_command ); if ($site_command->status === 'failed') { From cbfb14df27a8531cbf685669e0a9828d28b178f2 Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 10:48:32 +0100 Subject: [PATCH 06/21] Handle command status. --- app/Services/Forge/ForgeService.php | 3 ++- app/Services/Forge/ForgeSiteCommandWaiter.php | 3 +-- .../Forge/Pipeline/ImportDatabaseFromSql.php | 14 ++++++-------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/Services/Forge/ForgeService.php b/app/Services/Forge/ForgeService.php index 59b0fa5..56799c3 100644 --- a/app/Services/Forge/ForgeService.php +++ b/app/Services/Forge/ForgeService.php @@ -153,6 +153,7 @@ public function siteDirectory(): string public function waitForSiteCommand(SiteCommand $site_command): SiteCommand { - return (new ForgeSiteCommandWaiter($this->forge))->waitFor($site_command); + $waiter = app()->makeWith(ForgeSiteCommandWaiter::class, [$this->forge]); + return $waiter->waitFor($site_command); } } diff --git a/app/Services/Forge/ForgeSiteCommandWaiter.php b/app/Services/Forge/ForgeSiteCommandWaiter.php index cc17ef8..603baea 100644 --- a/app/Services/Forge/ForgeSiteCommandWaiter.php +++ b/app/Services/Forge/ForgeSiteCommandWaiter.php @@ -60,9 +60,8 @@ public function waitFor(SiteCommand $site_command): SiteCommand protected function commandIsRunning(SiteCommand $site_command): bool { - echo sprintf('site command status: %s', $site_command->status); return !isset($site_command->status) - || $site_command->status === 'running'; + || in_array($site_command->status, ['running', 'waiting']); } } diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php index 55f2d3c..27edff1 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php @@ -40,16 +40,14 @@ public function attemptImport(ForgeService $service, Closure $next, string $file $content = $this->buildImportCommandContent($service, $file); - $site_command = $service->forge->executeSiteCommand( - $service->setting->server, - $service->site->id, - ['command' => $content] - ); - - $this->information('---> site command status: %s', $site_command->status); + $this->information('---> Database import command finished successfully.'); $site_command = $service->waitForSiteCommand( - $site_command + $service->forge->executeSiteCommand( + $service->setting->server, + $service->site->id, + ['command' => $content] + ) ); if ($site_command->status === 'failed') { From 1fa9c3e56d43b559f2721ba1ade704d29d6f439d Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 10:57:49 +0100 Subject: [PATCH 07/21] Fixed Forge service injection. --- app/Services/Forge/ForgeService.php | 2 +- app/Services/Forge/Pipeline/ImportDatabaseFromSql.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/Services/Forge/ForgeService.php b/app/Services/Forge/ForgeService.php index 56799c3..51b55fa 100644 --- a/app/Services/Forge/ForgeService.php +++ b/app/Services/Forge/ForgeService.php @@ -153,7 +153,7 @@ public function siteDirectory(): string public function waitForSiteCommand(SiteCommand $site_command): SiteCommand { - $waiter = app()->makeWith(ForgeSiteCommandWaiter::class, [$this->forge]); + $waiter = app()->makeWith(ForgeSiteCommandWaiter::class, ['forge' => $this->forge]); return $waiter->waitFor($site_command); } } diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php index 27edff1..8a54190 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php @@ -40,8 +40,6 @@ public function attemptImport(ForgeService $service, Closure $next, string $file $content = $this->buildImportCommandContent($service, $file); - $this->information('---> Database import command finished successfully.'); - $site_command = $service->waitForSiteCommand( $service->forge->executeSiteCommand( $service->setting->server, From b2b6749324b82ea01cc3da42b9e3989d4697f3e6 Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 11:12:18 +0100 Subject: [PATCH 08/21] Handling array return type. --- app/Services/Forge/ForgeSiteCommandWaiter.php | 2 +- .../Services/Forge/ForgeSiteCommandWaiterTest.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Services/Forge/ForgeSiteCommandWaiter.php b/app/Services/Forge/ForgeSiteCommandWaiter.php index 603baea..0cbacc1 100644 --- a/app/Services/Forge/ForgeSiteCommandWaiter.php +++ b/app/Services/Forge/ForgeSiteCommandWaiter.php @@ -52,7 +52,7 @@ public function waitFor(SiteCommand $site_command): SiteCommand $site_command->serverId, $site_command->siteId, $site_command->id - ); + )[0]; } return $site_command; diff --git a/tests/Unit/Services/Forge/ForgeSiteCommandWaiterTest.php b/tests/Unit/Services/Forge/ForgeSiteCommandWaiterTest.php index cdc826b..c19ffdc 100644 --- a/tests/Unit/Services/Forge/ForgeSiteCommandWaiterTest.php +++ b/tests/Unit/Services/Forge/ForgeSiteCommandWaiterTest.php @@ -18,7 +18,7 @@ $forge->shouldReceive('getSiteCommand') ->times($waiter->maxAttempts) - ->andReturn($site_command); + ->andReturn([$site_command]); $site_command = $waiter->waitFor($site_command); @@ -29,7 +29,7 @@ ]); }); -test('it waits until the command is no longer running', function() { +test('it waits until the command is no longer running', function () { $forge = Mockery::mock(Forge::class); $site_command = Mockery::mock(SiteCommand::class); @@ -45,9 +45,9 @@ $forge->shouldReceive('getSiteCommand') ->times(3) ->andReturn( - $site_command, - $site_command, - $finished_command + [$site_command], + [$site_command], + [$finished_command] ); $site_command = $waiter->waitFor($site_command); @@ -60,4 +60,4 @@ Sleep::for($waiter->retrySeconds)->seconds(), ]); -}); \ No newline at end of file +}); From 6928b6bfc678b6e218c11d99a15e87ff3d3ce84e Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 12:20:36 +0100 Subject: [PATCH 09/21] Added seed command. --- app/Commands/ProvisionCommand.php | 2 + .../Pipeline/ImportDatabaseFromSeeder.php | 73 ++++++++ .../Forge/Pipeline/ImportDatabaseFromSql.php | 6 +- tests/Pest.php | 22 ++- .../Pipeline/ImportDatabaseFromSeederTest.php | 159 ++++++++++++++++++ .../Pipeline/ImportDatabaseFromSqlTest.php | 18 -- 6 files changed, 257 insertions(+), 23 deletions(-) create mode 100644 app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php create mode 100644 tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php diff --git a/app/Commands/ProvisionCommand.php b/app/Commands/ProvisionCommand.php index ce43449..485ab3a 100644 --- a/app/Commands/ProvisionCommand.php +++ b/app/Commands/ProvisionCommand.php @@ -23,6 +23,7 @@ use App\Services\Forge\Pipeline\FindServer; use App\Services\Forge\Pipeline\FindSite; use App\Services\Forge\Pipeline\ImportDatabaseFromSql; +use App\Services\Forge\Pipeline\ImportDatabaseFromSeeder; use App\Services\Forge\Pipeline\InstallGitRepository; use App\Services\Forge\Pipeline\NginxTemplateSearchReplace; use App\Services\Forge\Pipeline\ObtainLetsEncryptCertification; @@ -59,6 +60,7 @@ public function handle(ForgeService $service): void UpdateDeployScript::class, ImportDatabaseFromSql::class, DeploySite::class, + ImportDatabaseFromSeeder::class, RunOptionalCommands::class, EnsureJobScheduled::class, PutCommentOnPullRequest::class, diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php new file mode 100644 index 0000000..f97de58 --- /dev/null +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Services\Forge\Pipeline; + +use App\Services\Forge\ForgeService; +use App\Traits\Outputifier; +use Closure; + +class ImportDatabaseFromSeeder +{ + use Outputifier; + + public function __invoke(ForgeService $service, Closure $next) + { + if (!$service->siteNewlyMade && !$service->setting->dbImportOnDeployment) { + return $next($service); + } + + if (!$service->setting->dbImportSeed) { + return $next($service); + } + + return $this->attemptSeed($service, $next); + } + + public function attemptSeed(ForgeService $service, Closure $next) + { + $this->information(sprintf('Seeding database.')); + + $content = $this->buildImportCommandContent($service); + + $site_command = $service->waitForSiteCommand( + $service->forge->executeSiteCommand( + $service->setting->server, + $service->site->id, + ['command' => $content] + ) + ); + + if ($site_command->status === 'failed') { + $this->fail(sprintf('---> Database seed failed with message: %s', $site_command->output)); + return $next; + + } elseif ($site_command->status !== 'finished') { + $this->fail('---> Database seed did not finish in time.'); + return $next; + } + + $this->information('---> Database seeded successfully.'); + + return $next($service); + } + + public function buildImportCommandContent(ForgeService $service): string + { + return sprintf( + '%s artisan %s', + $service->site->phpVersion ?? 'php', + $service->siteNewlyMade ? 'db:seed' : 'migrate:fresh --seed' + ); + } +} diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php index 8a54190..a1bd357 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php @@ -49,15 +49,15 @@ public function attemptImport(ForgeService $service, Closure $next, string $file ); if ($site_command->status === 'failed') { - $this->fail(sprintf('---> Database import command failed with message: %s', $site_command->output)); + $this->fail(sprintf('---> Database import failed with message: %s', $site_command->output)); return $next; } elseif ($site_command->status !== 'finished') { - $this->fail('---> Database import command did not finish in time.'); + $this->fail('---> Database import did not finish in time.'); return $next; } - $this->information('---> Database import command finished successfully.'); + $this->information('---> Database import finished successfully.'); return $next($service); } diff --git a/tests/Pest.php b/tests/Pest.php index 68e2388..3506f0b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,10 @@ timeoutSeconds = 0; + foreach ($settings as $name => $value) { + $setting->{$name} = $value; + } + + $forge = Mockery::mock(Forge::class); + $forge->shouldReceive('setTimeout') + ->with($setting->timeoutSeconds); + + $service = Mockery::mock(ForgeService::class, [$setting, $forge])->makePartial(); + $service->site = new Site($site_attributes); + + return $service; } diff --git a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php new file mode 100644 index 0000000..5dbe48c --- /dev/null +++ b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php @@ -0,0 +1,159 @@ + false, + ]); + + $pipe = Mockery::mock(ImportDatabaseFromSeeder::class) + ->makePartial(); + $pipe->shouldReceive('attemptSeed') + ->never(); + + $next = fn () => true; + expect($pipe($service, $next))->toBe(true); +}); + +test('it attempts import when siteNewlyMade is false, and dbImportOnDeployment is true', function () { + + $service = configureMockService([ + 'dbImportOnDeployment' => true, + 'dbImportSeed' => true, + ]); + + $next = fn () => true; + $pipe = Mockery::mock(ImportDatabaseFromSeeder::class) + ->makePartial(); + $pipe->shouldReceive('attemptSeed') + ->once() + ->andReturn($next()); + + expect($pipe($service, $next))->toBe(true); +}); + +test('it attempts import when siteNewlyMade is true', function () { + + $service = configureMockService([ + 'dbImportOnDeployment' => false, + 'dbImportSeed' => true, + ]); + $service->siteNewlyMade = true; + + $next = fn () => true; + $pipe = Mockery::mock(ImportDatabaseFromSeeder::class) + ->makePartial(); + $pipe->shouldReceive('attemptSeed') + ->once() + ->andReturn($next()); + + expect($pipe($service, $next))->toBe(true); +}); + +test('it generates import command without phpVersion', function () { + + $service = configureMockService(); + $service->siteNewlyMade = true; + + $pipe = new ImportDatabaseFromSeeder(); + + expect($pipe->buildImportCommandContent($service)) + ->toBe('php artisan db:seed'); +}); + +test('it generates import command with phpVersion', function () { + + $service = configureMockService([], [ + 'phpVersion' => 'php81', + ]); + $service->siteNewlyMade = true; + + $pipe = new ImportDatabaseFromSeeder(); + + expect($pipe->buildImportCommandContent($service)) + ->toBe('php81 artisan db:seed'); +}); + +test('it executes import command with finished response', function () { + + $service = configureMockService([ + 'server' => 1, + ], [ + 'id' => 2, + ]); + $service->siteNewlyMade = true; + + $site_command = Mockery::mock(SiteCommand::class); + $site_command->status = 'finished'; + + $service->forge->shouldReceive('executeSiteCommand') + ->with(1, 2, ['command' => 'php artisan db:seed']) + ->once() + ->andReturn($site_command); + + $next = fn () => true; + + $pipe = new ImportDatabaseFromSeeder(); + $result = $pipe->attemptSeed( + $service, + $next + ); + + expect($result)->toBe(true); +}); + +test('it executes import command with failure status', function () { + + $service = configureMockService([ + 'server' => 1, + ]); + + $site_command = Mockery::mock(SiteCommand::class); + $site_command->status = 'failed'; + $site_command->output = 'oops'; + + $service->forge->shouldReceive('executeSiteCommand') + ->once() + ->andReturn($site_command); + + $next = fn () => true; + + $pipe = new ImportDatabaseFromSeeder(); + $result = $pipe->attemptSeed( + $service, + $next + ); + + expect($result)->toBe($next); +}); + +test('it executes import command with missing status', function () { + + $service = configureMockService([ + 'server' => 1, + ]); + + $site_command = Mockery::mock(SiteCommand::class); + + $service->forge->shouldReceive('executeSiteCommand') + ->once() + ->andReturn($site_command); + + $service->shouldReceive('waitForSiteCommand') + ->with($site_command) + ->andReturn($site_command); + + $next = fn () => true; + + $pipe = new ImportDatabaseFromSeeder(); + $result = $pipe->attemptSeed( + $service, + $next + ); + + expect($result)->toBe($next); +}); diff --git a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php index 6fee21b..258791e 100644 --- a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php +++ b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php @@ -7,24 +7,6 @@ use Laravel\Forge\Resources\Site; use Laravel\Forge\Resources\SiteCommand; -function configureMockService(array $settings = [], array $site_attributes = []): ForgeService -{ - $setting = Mockery::mock(ForgeSetting::class); - $setting->timeoutSeconds = 0; - foreach ($settings as $name => $value) { - $setting->{$name} = $value; - } - - $forge = Mockery::mock(Forge::class); - $forge->shouldReceive('setTimeout') - ->with($setting->timeoutSeconds); - - $service = Mockery::mock(ForgeService::class, [$setting, $forge])->makePartial(); - $service->site = new Site($site_attributes); - - return $service; -} - test('it skips import when dbImportOnDeployment is false', function () { $service = configureMockService([ From 8dbbb09c4eb1bfc020dc63d9393c8d8977c5a70a Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 12:32:10 +0100 Subject: [PATCH 10/21] Supports custom seeder class --- app/Services/Forge/ForgeSetting.php | 4 +- .../Pipeline/ImportDatabaseFromSeeder.php | 27 ++++++++---- .../Pipeline/ImportDatabaseFromSeederTest.php | 42 +++++++++++++++++-- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/app/Services/Forge/ForgeSetting.php b/app/Services/Forge/ForgeSetting.php index 20621c7..58e1dd6 100644 --- a/app/Services/Forge/ForgeSetting.php +++ b/app/Services/Forge/ForgeSetting.php @@ -126,7 +126,7 @@ class ForgeSetting /** * Flag to importing database via seeding. */ - public bool $dbImportSeed; + public string|bool $dbImportSeed; /** * Flag to importing database via SQL file. @@ -230,7 +230,7 @@ protected function validate(array $configurations): \Illuminate\Validation\Valid 'domain' => ['required'], 'git_provider' => ['required'], 'repository' => ['required'], - 'branch' => ['required', new BranchNameRegex], + 'branch' => ['required', new BranchNameRegex()], 'project_type' => ['string'], 'php_version' => ['nullable', 'string'], 'subdomain_pattern' => ['nullable', 'string'], diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php index f97de58..e85b66e 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php @@ -23,15 +23,15 @@ class ImportDatabaseFromSeeder public function __invoke(ForgeService $service, Closure $next) { - if (!$service->siteNewlyMade && !$service->setting->dbImportOnDeployment) { + if (!($seeder = $service->setting->dbImportSeed)) { return $next($service); } - if (!$service->setting->dbImportSeed) { + if (!$service->siteNewlyMade && !$service->setting->dbImportOnDeployment) { return $next($service); } - return $this->attemptSeed($service, $next); + return $this->attemptSeed($service, $next, $seeder); } public function attemptSeed(ForgeService $service, Closure $next) @@ -64,10 +64,23 @@ public function attemptSeed(ForgeService $service, Closure $next) public function buildImportCommandContent(ForgeService $service): string { - return sprintf( - '%s artisan %s', + + $seeder = ''; + if (is_string($service->setting->dbImportSeed)) { + $seeder = sprintf( + '--%s=%s', + $service->siteNewlyMade + ? 'class' + : 'seeder', + $service->setting->dbImportSeed + ); + } + + return trim(sprintf( + '%s artisan %s %s', $service->site->phpVersion ?? 'php', - $service->siteNewlyMade ? 'db:seed' : 'migrate:fresh --seed' - ); + $service->siteNewlyMade ? 'db:seed' : 'migrate:fresh --seed', + $seeder + )); } } diff --git a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php index 5dbe48c..b1bb8b9 100644 --- a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php +++ b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php @@ -7,6 +7,7 @@ test('it skips import when dbImportOnDeployment is false', function () { $service = configureMockService([ + 'dbImportSeed' => true, 'dbImportOnDeployment' => false, ]); @@ -22,8 +23,8 @@ test('it attempts import when siteNewlyMade is false, and dbImportOnDeployment is true', function () { $service = configureMockService([ - 'dbImportOnDeployment' => true, 'dbImportSeed' => true, + 'dbImportOnDeployment' => true, ]); $next = fn () => true; @@ -39,8 +40,8 @@ test('it attempts import when siteNewlyMade is true', function () { $service = configureMockService([ - 'dbImportOnDeployment' => false, 'dbImportSeed' => true, + 'dbImportOnDeployment' => false, ]); $service->siteNewlyMade = true; @@ -56,7 +57,9 @@ test('it generates import command without phpVersion', function () { - $service = configureMockService(); + $service = configureMockService([ + 'dbImportSeed' => true, + ]); $service->siteNewlyMade = true; $pipe = new ImportDatabaseFromSeeder(); @@ -67,7 +70,9 @@ test('it generates import command with phpVersion', function () { - $service = configureMockService([], [ + $service = configureMockService([ + 'dbImportSeed' => true, + ], [ 'phpVersion' => 'php81', ]); $service->siteNewlyMade = true; @@ -78,9 +83,36 @@ ->toBe('php81 artisan db:seed'); }); +test('it generates import command with custom seeder on provision', function () { + + $service = configureMockService([ + 'dbImportSeed' => 'FooSeeder', + ]); + $service->siteNewlyMade = true; + + $pipe = new ImportDatabaseFromSeeder(); + + expect($pipe->buildImportCommandContent($service)) + ->toBe('php artisan db:seed --class=FooSeeder'); +}); + +test('it generates import command with custom seeder on deployment', function () { + + $service = configureMockService([ + 'dbImportSeed' => 'FooSeeder', + ]); + $service->siteNewlyMade = false; + + $pipe = new ImportDatabaseFromSeeder(); + + expect($pipe->buildImportCommandContent($service)) + ->toBe('php artisan migrate:fresh --seed --seeder=FooSeeder'); +}); + test('it executes import command with finished response', function () { $service = configureMockService([ + 'dbImportSeed' => true, 'server' => 1, ], [ 'id' => 2, @@ -109,6 +141,7 @@ test('it executes import command with failure status', function () { $service = configureMockService([ + 'dbImportSeed' => true, 'server' => 1, ]); @@ -134,6 +167,7 @@ test('it executes import command with missing status', function () { $service = configureMockService([ + 'dbImportSeed' => true, 'server' => 1, ]); From ff700de5833a07bdda5fa92a2d32d606e78e5d5e Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 19:36:17 +0100 Subject: [PATCH 11/21] Removed information calls. --- app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php | 2 -- app/Services/Forge/Pipeline/ImportDatabaseFromSql.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php index e85b66e..e7ba841 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php @@ -57,8 +57,6 @@ public function attemptSeed(ForgeService $service, Closure $next) return $next; } - $this->information('---> Database seeded successfully.'); - return $next($service); } diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php index a1bd357..47e5e9a 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php @@ -57,8 +57,6 @@ public function attemptImport(ForgeService $service, Closure $next, string $file return $next; } - $this->information('---> Database import finished successfully.'); - return $next($service); } From 70a41bf639365256cebe473188c6e06e57b5dc7f Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 20:13:27 +0100 Subject: [PATCH 12/21] Allow strings for seeder. --- app/Services/Forge/ForgeSetting.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/Forge/ForgeSetting.php b/app/Services/Forge/ForgeSetting.php index 58e1dd6..242916b 100644 --- a/app/Services/Forge/ForgeSetting.php +++ b/app/Services/Forge/ForgeSetting.php @@ -242,7 +242,7 @@ protected function validate(array $configurations): \Illuminate\Validation\Valid 'db_creation_required' => ['boolean'], 'db_name' => ['nullable', 'string'], 'db_import_sql' => ['nullable', 'string'], - 'db_import_seed' => ['boolean'], + 'db_import_seed' => ['nullable'], 'db_import_on_deployment' => ['boolean'], 'auto_source_required' => ['boolean'], 'ssl_required' => ['boolean'], From 9ab5062c9aa36ff2b8f77e9504fc706b89a6ca7c Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 20:36:04 +0100 Subject: [PATCH 13/21] Add executable workaround. --- .../Forge/Pipeline/ImportDatabaseFromSeeder.php | 13 +++++++++++-- .../Forge/Pipeline/ImportDatabaseFromSeederTest.php | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php index e7ba841..d31992d 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php @@ -62,7 +62,6 @@ public function attemptSeed(ForgeService $service, Closure $next) public function buildImportCommandContent(ForgeService $service): string { - $seeder = ''; if (is_string($service->setting->dbImportSeed)) { $seeder = sprintf( @@ -76,9 +75,19 @@ public function buildImportCommandContent(ForgeService $service): string return trim(sprintf( '%s artisan %s %s', - $service->site->phpVersion ?? 'php', + $this->phpExecutable($service->site->phpVersion ?? 'php'), $service->siteNewlyMade ? 'db:seed' : 'migrate:fresh --seed', $seeder )); } + + /** + * Forge's phpVersion strings don't exactly map to the executable. + * For example php83 corresponds to the php8.3 executable. + * This workaround assumes no minor versions above 9! + */ + protected function phpExecutable(string $phpVersion): string + { + return preg_replace_callback('/\d$/', fn ($matches) => '.' . $matches[0], $phpVersion); + } } diff --git a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php index b1bb8b9..a75a332 100644 --- a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php +++ b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php @@ -80,7 +80,7 @@ $pipe = new ImportDatabaseFromSeeder(); expect($pipe->buildImportCommandContent($service)) - ->toBe('php81 artisan db:seed'); + ->toBe('php8.1 artisan db:seed'); }); test('it generates import command with custom seeder on provision', function () { From f4d45a80cff08aa19a6592fbe179e29c68d195a1 Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Wed, 15 May 2024 20:58:35 +0100 Subject: [PATCH 14/21] Added validation rule. --- app/Rules/DBImportSeed.php | 16 ++++++++++++++++ app/Services/Forge/ForgeSetting.php | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 app/Rules/DBImportSeed.php diff --git a/app/Rules/DBImportSeed.php b/app/Rules/DBImportSeed.php new file mode 100644 index 0000000..bf285b4 --- /dev/null +++ b/app/Rules/DBImportSeed.php @@ -0,0 +1,16 @@ + ['boolean'], 'db_name' => ['nullable', 'string'], 'db_import_sql' => ['nullable', 'string'], - 'db_import_seed' => ['nullable'], + 'db_import_seed' => [new DBImportSeed()], 'db_import_on_deployment' => ['boolean'], 'auto_source_required' => ['boolean'], 'ssl_required' => ['boolean'], From 18352995cc7240d576f649c5eb087cff31f40807 Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Fri, 17 May 2024 16:50:22 +0100 Subject: [PATCH 15/21] Updated tests --- app/Services/Forge/Pipeline/ImportDatabaseFromSql.php | 4 ++-- .../Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php | 1 - .../Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php | 5 +---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php index 47e5e9a..8e536e5 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php @@ -23,11 +23,11 @@ class ImportDatabaseFromSql public function __invoke(ForgeService $service, Closure $next) { - if (!$service->siteNewlyMade && !$service->setting->dbImportOnDeployment) { + if (!($file = $service->setting->dbImportSql)) { return $next($service); } - if (!($file = $service->setting->dbImportSql)) { + if (!$service->siteNewlyMade && !$service->setting->dbImportOnDeployment) { return $next($service); } diff --git a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php index a75a332..f597190 100644 --- a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php +++ b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php @@ -1,6 +1,5 @@ true, 'dbImportOnDeployment' => false, ]); From b0d114485e12777fa8b23a95e2f846f3751cf83b Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Fri, 17 May 2024 17:31:28 +0100 Subject: [PATCH 16/21] Updated tests --- .../Forge/Pipeline/ImportDatabaseFromSql.php | 37 ++++++-- tests/Pest.php | 4 +- .../Pipeline/ImportDatabaseFromSqlTest.php | 95 +++++++++++++------ 3 files changed, 96 insertions(+), 40 deletions(-) diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php index 8e536e5..18ecbf4 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php +++ b/app/Services/Forge/Pipeline/ImportDatabaseFromSql.php @@ -68,18 +68,39 @@ public function buildImportCommandContent(ForgeService $service, string $file): default => "cat {$file}" }; - return collect([ + return implode(' ', [ $extract, '|', - 'mysql', - '-u', + $this->buildDatabaseConnection($service) + ]); + } + + protected function buildDatabaseConnection(ForgeService $service): string + { + if (str_contains($service->server->databaseType, 'postgres')) { + return sprintf( + 'pgsql postgres://%s:%s%s/%s', + $service->database['DB_USERNAME'], + $service->database['DB_PASSWORD'], + isset($service->database['DB_HOST']) + ? sprintf( + '@%s:%s', + $service->database['DB_HOST'], + $service->database['DB_PORT'] ?? '5432', + ) + : '', + $service->getFormattedDatabaseName(), + ); + } + + return sprintf( + '%s -u %s -p%s %s %s %s', + str_contains($service->server->databaseType, 'mariadb') ? 'mariadb' : 'mysql', $service->database['DB_USERNAME'], - "-p{$service->database['DB_PASSWORD']}", - isset($service->database['DB_PORT']) ? '-P ' . $service->database['DB_PORT'] : '', + $service->database['DB_PASSWORD'], isset($service->database['DB_HOST']) ? '-h ' . $service->database['DB_HOST'] : '', + isset($service->database['DB_PORT']) ? '-P ' . $service->database['DB_PORT'] : '', $service->getFormattedDatabaseName(), - ]) - ->filter() - ->implode(' '); + ); } } diff --git a/tests/Pest.php b/tests/Pest.php index 3506f0b..e44793f 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -3,6 +3,7 @@ use App\Services\Forge\ForgeService; use App\Services\Forge\ForgeSetting; use Laravel\Forge\Forge; +use Laravel\Forge\Resources\Server; use Laravel\Forge\Resources\Site; /* @@ -44,7 +45,7 @@ | */ -function configureMockService(array $settings = [], array $site_attributes = []): ForgeService +function configureMockService(array $settings = [], array $site_attributes = [], array $server_attributes = []): ForgeService { $setting = Mockery::mock(ForgeSetting::class); $setting->timeoutSeconds = 0; @@ -58,6 +59,7 @@ function configureMockService(array $settings = [], array $site_attributes = []) $service = Mockery::mock(ForgeService::class, [$setting, $forge])->makePartial(); $service->site = new Site($site_attributes); + $service->server = new Server($server_attributes); return $service; } diff --git a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php index 20837ce..e5e848e 100644 --- a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php +++ b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php @@ -74,9 +74,14 @@ test('it generates import command for file with .gz extension', function () { - $service = configureMockService([ - 'dbName' => 'my_db', - ]); + $service = configureMockService( + settings: [ + 'dbName' => 'my_db', + ], + server_attributes: [ + 'databaseType' => 'mysql' + ] + ); $service->setDatabase([ 'DB_USERNAME' => 'foo', 'DB_PASSWORD' => 'bar', @@ -87,14 +92,19 @@ $pipe = new ImportDatabaseFromSql(); expect($pipe->buildImportCommandContent($service, '/path/to/db.sql.gz')) - ->toBe('gunzip < /path/to/db.sql.gz | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db'); + ->toBe('gunzip < /path/to/db.sql.gz | mysql -u foo -pbar -h 1.2.3.4 -P 1234 my_db'); }); test('it generates import command for file with .zip extension', function () { - $service = configureMockService([ - 'dbName' => 'my_db', - ]); + $service = configureMockService( + settings: [ + 'dbName' => 'my_db', + ], + server_attributes: [ + 'databaseType' => 'mysql' + ] + ); $service->setDatabase([ 'DB_USERNAME' => 'foo', 'DB_PASSWORD' => 'bar', @@ -105,14 +115,19 @@ $pipe = new ImportDatabaseFromSql(); expect($pipe->buildImportCommandContent($service, '/path/to/db.sql.zip')) - ->toBe('unzip -p /path/to/db.sql.zip | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db'); + ->toBe('unzip -p /path/to/db.sql.zip | mysql -u foo -pbar -h 1.2.3.4 -P 1234 my_db'); }); test('it generates import command for file with .sql extension', function () { - $service = configureMockService([ - 'dbName' => 'my_db', - ]); + $service = configureMockService( + settings: [ + 'dbName' => 'my_db', + ], + server_attributes: [ + 'databaseType' => 'mysql' + ] + ); $service->setDatabase([ 'DB_USERNAME' => 'foo', 'DB_PASSWORD' => 'bar', @@ -123,17 +138,23 @@ $pipe = new ImportDatabaseFromSql(); expect($pipe->buildImportCommandContent($service, '/path/to/db.sql')) - ->toBe('cat /path/to/db.sql | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db'); + ->toBe('cat /path/to/db.sql | mysql -u foo -pbar -h 1.2.3.4 -P 1234 my_db'); }); test('it executes import command with finished response', function () { - $service = configureMockService([ - 'dbName' => 'my_db', - 'server' => 1, - ], [ - 'id' => 2, - ]); + $service = configureMockService( + settings: [ + 'dbName' => 'my_db', + 'server' => 1, + ], + site_attributes: [ + 'id' => 2, + ], + server_attributes: [ + 'databaseType' => 'mysql' + ] + ); $service->setDatabase([ 'DB_USERNAME' => 'foo', 'DB_PASSWORD' => 'bar', @@ -145,7 +166,7 @@ $site_command->status = 'finished'; $service->forge->shouldReceive('executeSiteCommand') - ->with(1, 2, ['command' => 'cat x.sql | mysql -u foo -pbar -P 1234 -h 1.2.3.4 my_db']) + ->with(1, 2, ['command' => 'cat x.sql | mysql -u foo -pbar -h 1.2.3.4 -P 1234 my_db']) ->once() ->andReturn($site_command); @@ -163,12 +184,18 @@ test('it executes import command with failure status', function () { - $service = configureMockService([ - 'dbName' => 'my_db', - 'server' => 1, - ], [ - 'id' => 2, - ]); + $service = configureMockService( + settings: [ + 'dbName' => 'my_db', + 'server' => 1, + ], + site_attributes: [ + 'id' => 2, + ], + server_attributes: [ + 'databaseType' => 'mysql' + ] + ); $site_command = Mockery::mock(SiteCommand::class); $site_command->status = 'failed'; @@ -192,12 +219,18 @@ test('it executes import command with missing status', function () { - $service = configureMockService([ - 'dbName' => 'my_db', - 'server' => 1, - ], [ - 'id' => 2, - ]); + $service = configureMockService( + settings: [ + 'dbName' => 'my_db', + 'server' => 1 + ], + site_attributes: [ + 'id' => 2, + ], + server_attributes: [ + 'databaseType' => 'mysql' + ] + ); $site_command = Mockery::mock(SiteCommand::class); From d1474c1afa7a7f639226908534aff08e4993066c Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Fri, 17 May 2024 17:38:49 +0100 Subject: [PATCH 17/21] Added tests --- .../Pipeline/ImportDatabaseFromSqlTest.php | 68 +++++-------------- 1 file changed, 17 insertions(+), 51 deletions(-) diff --git a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php index e5e848e..0bdc627 100644 --- a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php +++ b/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSqlTest.php @@ -72,37 +72,14 @@ expect($pipe($service, $next))->toBe(true); }); -test('it generates import command for file with .gz extension', function () { +test('it generates import command', function (string $databaseType, string $file, string $expected) { $service = configureMockService( settings: [ 'dbName' => 'my_db', ], server_attributes: [ - 'databaseType' => 'mysql' - ] - ); - $service->setDatabase([ - 'DB_USERNAME' => 'foo', - 'DB_PASSWORD' => 'bar', - 'DB_HOST' => '1.2.3.4', - 'DB_PORT' => 1234, - ]); - - $pipe = new ImportDatabaseFromSql(); - - expect($pipe->buildImportCommandContent($service, '/path/to/db.sql.gz')) - ->toBe('gunzip < /path/to/db.sql.gz | mysql -u foo -pbar -h 1.2.3.4 -P 1234 my_db'); -}); - -test('it generates import command for file with .zip extension', function () { - - $service = configureMockService( - settings: [ - 'dbName' => 'my_db', - ], - server_attributes: [ - 'databaseType' => 'mysql' + 'databaseType' => $databaseType ] ); $service->setDatabase([ @@ -114,32 +91,21 @@ $pipe = new ImportDatabaseFromSql(); - expect($pipe->buildImportCommandContent($service, '/path/to/db.sql.zip')) - ->toBe('unzip -p /path/to/db.sql.zip | mysql -u foo -pbar -h 1.2.3.4 -P 1234 my_db'); -}); - -test('it generates import command for file with .sql extension', function () { - - $service = configureMockService( - settings: [ - 'dbName' => 'my_db', - ], - server_attributes: [ - 'databaseType' => 'mysql' - ] - ); - $service->setDatabase([ - 'DB_USERNAME' => 'foo', - 'DB_PASSWORD' => 'bar', - 'DB_HOST' => '1.2.3.4', - 'DB_PORT' => 1234, - ]); - - $pipe = new ImportDatabaseFromSql(); - - expect($pipe->buildImportCommandContent($service, '/path/to/db.sql')) - ->toBe('cat /path/to/db.sql | mysql -u foo -pbar -h 1.2.3.4 -P 1234 my_db'); -}); + expect($pipe->buildImportCommandContent($service, '/path/to/' . $file)) + ->toBe($expected); +})->with([ + 'mysql with .gz extension' => ['mysql', 'db.sql.gz', 'gunzip < /path/to/db.sql.gz | mysql -u foo -pbar -h 1.2.3.4 -P 1234 my_db'], + 'mysql with .zip extension' => ['mysql', 'db.sql.zip', 'unzip -p /path/to/db.sql.zip | mysql -u foo -pbar -h 1.2.3.4 -P 1234 my_db'], + 'mysql with .sql extension' => ['mysql', 'db.sql', 'cat /path/to/db.sql | mysql -u foo -pbar -h 1.2.3.4 -P 1234 my_db'], + + 'mariadb with .gz extension' => ['mariadb', 'db.sql.gz', 'gunzip < /path/to/db.sql.gz | mariadb -u foo -pbar -h 1.2.3.4 -P 1234 my_db'], + 'mariadb with .zip extension' => ['mariadb', 'db.sql.zip', 'unzip -p /path/to/db.sql.zip | mariadb -u foo -pbar -h 1.2.3.4 -P 1234 my_db'], + 'mariadb with .sql extension' => ['mariadb', 'db.sql', 'cat /path/to/db.sql | mariadb -u foo -pbar -h 1.2.3.4 -P 1234 my_db'], + + 'postgres with .gz extension' => ['postgres', 'db.sql.gz', 'gunzip < /path/to/db.sql.gz | pgsql postgres://foo:bar@1.2.3.4:1234/my_db'], + 'postgres with .zip extension' => ['postgres', 'db.sql.zip', 'unzip -p /path/to/db.sql.zip | pgsql postgres://foo:bar@1.2.3.4:1234/my_db'], + 'postgres with .sql extension' => ['postgres', 'db.sql', 'cat /path/to/db.sql | pgsql postgres://foo:bar@1.2.3.4:1234/my_db'], +]); test('it executes import command with finished response', function () { From 706c9948bb757bc3583b03d8c9f9026558452c2c Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Fri, 17 May 2024 17:57:44 +0100 Subject: [PATCH 18/21] Reverted package name. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bfbcd26..e139f73 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "gbradley/laravel-harbor", + "name": "mehrancodes/laravel-harbor", "description": "A CLI tool to Quickly create On-Demand preview environment for your apps.", "keywords": ["php", "laravel-harbor", "laravel-zero", "console", "cli", "continuous-integration", "ci", "laravel-forge", "provision", "staging", "preview", "pull-requests"], "homepage": "https://laravel-harbor.com", From 61671e717e8f0b19981054c9e37d1e0bb790b7ef Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Fri, 17 May 2024 18:18:53 +0100 Subject: [PATCH 19/21] Rename pipeline. --- app/Commands/ProvisionCommand.php | 4 +- app/Rules/{DBImportSeed.php => DBSeed.php} | 2 +- app/Services/Forge/ForgeSetting.php | 4 +- ...atabaseFromSeeder.php => SeedDatabase.php} | 10 +-- config/forge.php | 4 +- ...romSeederTest.php => SeedDatabaseTest.php} | 66 +++++++------------ 6 files changed, 36 insertions(+), 54 deletions(-) rename app/Rules/{DBImportSeed.php => DBSeed.php} (87%) rename app/Services/Forge/Pipeline/{ImportDatabaseFromSeeder.php => SeedDatabase.php} (89%) rename tests/Unit/Services/Forge/Pipeline/{ImportDatabaseFromSeederTest.php => SeedDatabaseTest.php} (70%) diff --git a/app/Commands/ProvisionCommand.php b/app/Commands/ProvisionCommand.php index 485ab3a..4605fe9 100644 --- a/app/Commands/ProvisionCommand.php +++ b/app/Commands/ProvisionCommand.php @@ -23,13 +23,13 @@ use App\Services\Forge\Pipeline\FindServer; use App\Services\Forge\Pipeline\FindSite; use App\Services\Forge\Pipeline\ImportDatabaseFromSql; -use App\Services\Forge\Pipeline\ImportDatabaseFromSeeder; use App\Services\Forge\Pipeline\InstallGitRepository; use App\Services\Forge\Pipeline\NginxTemplateSearchReplace; use App\Services\Forge\Pipeline\ObtainLetsEncryptCertification; use App\Services\Forge\Pipeline\OrCreateNewSite; use App\Services\Forge\Pipeline\PutCommentOnPullRequest; use App\Services\Forge\Pipeline\RunOptionalCommands; +use App\Services\Forge\Pipeline\SeedDatabase; use App\Services\Forge\Pipeline\UpdateDeployScript; use App\Services\Forge\Pipeline\UpdateEnvironmentVariables; use App\Traits\Outputifier; @@ -60,7 +60,7 @@ public function handle(ForgeService $service): void UpdateDeployScript::class, ImportDatabaseFromSql::class, DeploySite::class, - ImportDatabaseFromSeeder::class, + SeedDatabase::class, RunOptionalCommands::class, EnsureJobScheduled::class, PutCommentOnPullRequest::class, diff --git a/app/Rules/DBImportSeed.php b/app/Rules/DBSeed.php similarity index 87% rename from app/Rules/DBImportSeed.php rename to app/Rules/DBSeed.php index bf285b4..14d54c4 100644 --- a/app/Rules/DBImportSeed.php +++ b/app/Rules/DBSeed.php @@ -5,7 +5,7 @@ use Closure; use Illuminate\Contracts\Validation\ValidationRule; -class DBImportSeed implements ValidationRule +class DBSeed implements ValidationRule { public function validate(string $attribute, mixed $value, Closure $fail): void { diff --git a/app/Services/Forge/ForgeSetting.php b/app/Services/Forge/ForgeSetting.php index 7761984..00fa240 100644 --- a/app/Services/Forge/ForgeSetting.php +++ b/app/Services/Forge/ForgeSetting.php @@ -14,7 +14,7 @@ namespace App\Services\Forge; use App\Rules\BranchNameRegex; -use App\Rules\DBImportSeed; +use App\Rules\DBSeed; use App\Traits\Outputifier; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; @@ -243,7 +243,7 @@ protected function validate(array $configurations): \Illuminate\Validation\Valid 'db_creation_required' => ['boolean'], 'db_name' => ['nullable', 'string'], 'db_import_sql' => ['nullable', 'string'], - 'db_import_seed' => [new DBImportSeed()], + 'db_import_seed' => [new DBSeed()], 'db_import_on_deployment' => ['boolean'], 'auto_source_required' => ['boolean'], 'ssl_required' => ['boolean'], diff --git a/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php b/app/Services/Forge/Pipeline/SeedDatabase.php similarity index 89% rename from app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php rename to app/Services/Forge/Pipeline/SeedDatabase.php index d31992d..da87646 100644 --- a/app/Services/Forge/Pipeline/ImportDatabaseFromSeeder.php +++ b/app/Services/Forge/Pipeline/SeedDatabase.php @@ -17,17 +17,17 @@ use App\Traits\Outputifier; use Closure; -class ImportDatabaseFromSeeder +class SeedDatabase { use Outputifier; public function __invoke(ForgeService $service, Closure $next) { - if (!($seeder = $service->setting->dbImportSeed)) { + if (!($seeder = $service->setting->dbSeed)) { return $next($service); } - if (!$service->siteNewlyMade && !$service->setting->dbImportOnDeployment) { + if (!$service->siteNewlyMade) { return $next($service); } @@ -63,13 +63,13 @@ public function attemptSeed(ForgeService $service, Closure $next) public function buildImportCommandContent(ForgeService $service): string { $seeder = ''; - if (is_string($service->setting->dbImportSeed)) { + if (is_string($service->setting->dbSeed)) { $seeder = sprintf( '--%s=%s', $service->siteNewlyMade ? 'class' : 'seeder', - $service->setting->dbImportSeed + $service->setting->dbSeed ); } diff --git a/config/forge.php b/config/forge.php index 140a35c..d22bc56 100644 --- a/config/forge.php +++ b/config/forge.php @@ -61,8 +61,8 @@ // Import the database via a SQL file (default: null). 'db_import_sql' => env('FORGE_DB_IMPORT_SQL', null), - // Import the database via seeding (default: false). - 'db_import_seed' => env('FORGE_DB_IMPORT_SEED', false), + // Seed the database (default: false). + 'db_seed' => env('FORGE_DB_SEED', false), // Flag to perform database import on deployment (default: false). 'db_import_on_deployment' => env('FORGE_DB_IMPORT_ON_DEPLOYMENT', false), diff --git a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php b/tests/Unit/Services/Forge/Pipeline/SeedDatabaseTest.php similarity index 70% rename from tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php rename to tests/Unit/Services/Forge/Pipeline/SeedDatabaseTest.php index f597190..b1b95be 100644 --- a/tests/Unit/Services/Forge/Pipeline/ImportDatabaseFromSeederTest.php +++ b/tests/Unit/Services/Forge/Pipeline/SeedDatabaseTest.php @@ -1,33 +1,17 @@ true, - 'dbImportOnDeployment' => false, - ]); - - $pipe = Mockery::mock(ImportDatabaseFromSeeder::class) - ->makePartial(); - $pipe->shouldReceive('attemptSeed') - ->never(); - - $next = fn () => true; - expect($pipe($service, $next))->toBe(true); -}); - -test('it attempts import when siteNewlyMade is false, and dbImportOnDeployment is true', function () { +test('it attempts import when siteNewlyMade is true', function () { $service = configureMockService([ - 'dbImportSeed' => true, - 'dbImportOnDeployment' => true, + 'dbSeed' => true ]); + $service->siteNewlyMade = true; $next = fn () => true; - $pipe = Mockery::mock(ImportDatabaseFromSeeder::class) + $pipe = Mockery::mock(SeedDatabase::class) ->makePartial(); $pipe->shouldReceive('attemptSeed') ->once() @@ -36,20 +20,18 @@ expect($pipe($service, $next))->toBe(true); }); -test('it attempts import when siteNewlyMade is true', function () { +test('it skips import when siteNewlyMade is false', function () { $service = configureMockService([ - 'dbImportSeed' => true, - 'dbImportOnDeployment' => false, + 'dbSeed' => true ]); - $service->siteNewlyMade = true; + $service->siteNewlyMade = false; $next = fn () => true; - $pipe = Mockery::mock(ImportDatabaseFromSeeder::class) + $pipe = Mockery::mock(SeedDatabase::class) ->makePartial(); $pipe->shouldReceive('attemptSeed') - ->once() - ->andReturn($next()); + ->never(); expect($pipe($service, $next))->toBe(true); }); @@ -57,11 +39,11 @@ test('it generates import command without phpVersion', function () { $service = configureMockService([ - 'dbImportSeed' => true, + 'dbSeed' => true, ]); $service->siteNewlyMade = true; - $pipe = new ImportDatabaseFromSeeder(); + $pipe = new SeedDatabase(); expect($pipe->buildImportCommandContent($service)) ->toBe('php artisan db:seed'); @@ -70,13 +52,13 @@ test('it generates import command with phpVersion', function () { $service = configureMockService([ - 'dbImportSeed' => true, + 'dbSeed' => true, ], [ 'phpVersion' => 'php81', ]); $service->siteNewlyMade = true; - $pipe = new ImportDatabaseFromSeeder(); + $pipe = new SeedDatabase(); expect($pipe->buildImportCommandContent($service)) ->toBe('php8.1 artisan db:seed'); @@ -85,11 +67,11 @@ test('it generates import command with custom seeder on provision', function () { $service = configureMockService([ - 'dbImportSeed' => 'FooSeeder', + 'dbSeed' => 'FooSeeder', ]); $service->siteNewlyMade = true; - $pipe = new ImportDatabaseFromSeeder(); + $pipe = new SeedDatabase(); expect($pipe->buildImportCommandContent($service)) ->toBe('php artisan db:seed --class=FooSeeder'); @@ -98,11 +80,11 @@ test('it generates import command with custom seeder on deployment', function () { $service = configureMockService([ - 'dbImportSeed' => 'FooSeeder', + 'dbSeed' => 'FooSeeder', ]); $service->siteNewlyMade = false; - $pipe = new ImportDatabaseFromSeeder(); + $pipe = new SeedDatabase(); expect($pipe->buildImportCommandContent($service)) ->toBe('php artisan migrate:fresh --seed --seeder=FooSeeder'); @@ -111,7 +93,7 @@ test('it executes import command with finished response', function () { $service = configureMockService([ - 'dbImportSeed' => true, + 'dbSeed' => true, 'server' => 1, ], [ 'id' => 2, @@ -128,7 +110,7 @@ $next = fn () => true; - $pipe = new ImportDatabaseFromSeeder(); + $pipe = new SeedDatabase(); $result = $pipe->attemptSeed( $service, $next @@ -140,7 +122,7 @@ test('it executes import command with failure status', function () { $service = configureMockService([ - 'dbImportSeed' => true, + 'dbSeed' => true, 'server' => 1, ]); @@ -154,7 +136,7 @@ $next = fn () => true; - $pipe = new ImportDatabaseFromSeeder(); + $pipe = new SeedDatabase(); $result = $pipe->attemptSeed( $service, $next @@ -166,7 +148,7 @@ test('it executes import command with missing status', function () { $service = configureMockService([ - 'dbImportSeed' => true, + 'dbSeed' => true, 'server' => 1, ]); @@ -182,7 +164,7 @@ $next = fn () => true; - $pipe = new ImportDatabaseFromSeeder(); + $pipe = new SeedDatabase(); $result = $pipe->attemptSeed( $service, $next From 4c4c1fdcd158cd6ef8eab6949f97b01a12e8f82f Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Fri, 17 May 2024 18:23:16 +0100 Subject: [PATCH 20/21] Rename property. --- app/Services/Forge/ForgeSetting.php | 6 +++--- config/forge.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Services/Forge/ForgeSetting.php b/app/Services/Forge/ForgeSetting.php index 00fa240..2d3631d 100644 --- a/app/Services/Forge/ForgeSetting.php +++ b/app/Services/Forge/ForgeSetting.php @@ -125,12 +125,12 @@ class ForgeSetting public ?string $dbName; /** - * Flag to importing database via seeding. + * Flag / seeder to seed database. */ - public string|bool $dbImportSeed; + public bool|string $dbSeed; /** - * Flag to importing database via SQL file. + * Path of file to import into database. */ public ?string $dbImportSql; diff --git a/config/forge.php b/config/forge.php index d22bc56..7e6b933 100644 --- a/config/forge.php +++ b/config/forge.php @@ -58,12 +58,12 @@ // Override default database and database username, if needed. Defaults to the site name. 'db_name' => env('FORGE_DB_NAME', null), - // Import the database via a SQL file (default: null). - 'db_import_sql' => env('FORGE_DB_IMPORT_SQL', null), - // Seed the database (default: false). 'db_seed' => env('FORGE_DB_SEED', false), + // Import the database via a SQL file (default: null). + 'db_import_sql' => env('FORGE_DB_IMPORT_SQL', null), + // Flag to perform database import on deployment (default: false). 'db_import_on_deployment' => env('FORGE_DB_IMPORT_ON_DEPLOYMENT', false), From 1ebb0c761544c161c1546e32c2199160afca811b Mon Sep 17 00:00:00 2001 From: Graham Bradley Date: Fri, 17 May 2024 18:29:13 +0100 Subject: [PATCH 21/21] Tidying up tests --- app/Services/Forge/Pipeline/SeedDatabase.php | 8 +--- .../Forge/Pipeline/SeedDatabaseTest.php | 43 ++++++++----------- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/app/Services/Forge/Pipeline/SeedDatabase.php b/app/Services/Forge/Pipeline/SeedDatabase.php index da87646..2a532ce 100644 --- a/app/Services/Forge/Pipeline/SeedDatabase.php +++ b/app/Services/Forge/Pipeline/SeedDatabase.php @@ -65,18 +65,14 @@ public function buildImportCommandContent(ForgeService $service): string $seeder = ''; if (is_string($service->setting->dbSeed)) { $seeder = sprintf( - '--%s=%s', - $service->siteNewlyMade - ? 'class' - : 'seeder', + '--class=%s', $service->setting->dbSeed ); } return trim(sprintf( - '%s artisan %s %s', + '%s artisan db:seed %s', $this->phpExecutable($service->site->phpVersion ?? 'php'), - $service->siteNewlyMade ? 'db:seed' : 'migrate:fresh --seed', $seeder )); } diff --git a/tests/Unit/Services/Forge/Pipeline/SeedDatabaseTest.php b/tests/Unit/Services/Forge/Pipeline/SeedDatabaseTest.php index b1b95be..1a35571 100644 --- a/tests/Unit/Services/Forge/Pipeline/SeedDatabaseTest.php +++ b/tests/Unit/Services/Forge/Pipeline/SeedDatabaseTest.php @@ -51,11 +51,14 @@ test('it generates import command with phpVersion', function () { - $service = configureMockService([ - 'dbSeed' => true, - ], [ - 'phpVersion' => 'php81', - ]); + $service = configureMockService( + settings: [ + 'dbSeed' => true, + ], + site_attributes: [ + 'phpVersion' => 'php81', + ] + ); $service->siteNewlyMade = true; $pipe = new SeedDatabase(); @@ -64,7 +67,7 @@ ->toBe('php8.1 artisan db:seed'); }); -test('it generates import command with custom seeder on provision', function () { +test('it generates import command with custom seeder', function () { $service = configureMockService([ 'dbSeed' => 'FooSeeder', @@ -77,27 +80,17 @@ ->toBe('php artisan db:seed --class=FooSeeder'); }); -test('it generates import command with custom seeder on deployment', function () { - - $service = configureMockService([ - 'dbSeed' => 'FooSeeder', - ]); - $service->siteNewlyMade = false; - - $pipe = new SeedDatabase(); - - expect($pipe->buildImportCommandContent($service)) - ->toBe('php artisan migrate:fresh --seed --seeder=FooSeeder'); -}); - test('it executes import command with finished response', function () { - $service = configureMockService([ - 'dbSeed' => true, - 'server' => 1, - ], [ - 'id' => 2, - ]); + $service = configureMockService( + settings: [ + 'dbSeed' => true, + 'server' => 1, + ], + site_attributes: [ + 'id' => 2, + ] + ); $service->siteNewlyMade = true; $site_command = Mockery::mock(SiteCommand::class);