Implement Keystone environment deployments
This commit is contained in:
151
tests/Feature/ServiceDeploymentOperationTest.php
Normal file
151
tests/Feature/ServiceDeploymentOperationTest.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\OperationKind;
|
||||
use App\Enums\OperationStatus;
|
||||
use App\Enums\ServiceCategory;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Jobs\Services\DeployService;
|
||||
use App\Jobs\Services\RunStep;
|
||||
use App\Models\Network;
|
||||
use App\Models\OperationStep;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Provider;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Services\Operations\RemoteCommandRunner;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
beforeEach(function () {
|
||||
app()->instance(RemoteCommandRunner::class, new class implements RemoteCommandRunner
|
||||
{
|
||||
public function run(Server $server, string $script): string
|
||||
{
|
||||
return "image_digest=postgres:18@sha256:postgresdigest\n";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('creates service deploy operations that upload generated compose files first', function () {
|
||||
Bus::fake();
|
||||
|
||||
$service = Service::factory()->for(serviceDeploymentServer())->create([
|
||||
'name' => 'postgres',
|
||||
'category' => ServiceCategory::DATABASE,
|
||||
'type' => ServiceType::POSTGRES,
|
||||
'version' => '18',
|
||||
'version_track' => '18',
|
||||
'driver_name' => 'postgres.18',
|
||||
'credentials' => [
|
||||
'user' => 'keystone',
|
||||
'password' => 'secret',
|
||||
'db' => 'keystone',
|
||||
],
|
||||
]);
|
||||
|
||||
(new DeployService($service))->handle();
|
||||
|
||||
$operation = $service->operations()->first();
|
||||
$firstStep = $operation->steps()->orderBy('order')->first();
|
||||
$postgresStep = $operation->steps()->where('name', 'Start Postgres service')->first();
|
||||
|
||||
expect($operation->kind)->toBe(OperationKind::SERVICE_DEPLOY)
|
||||
->and($firstStep->name)->toBe('Upload Compose file')
|
||||
->and($firstStep->script)->toContain('compose.yml')
|
||||
->and($firstStep->script)->toContain('/.env')
|
||||
->and($firstStep->script)->toContain('base64 -d')
|
||||
->and($service->refresh()->available_image_digest)->toBe('sha256:postgresdigest')
|
||||
->and($postgresStep->script)->toBe("docker compose -f /home/keystone/services/{$service->id}/compose.yml up -d")
|
||||
->and($operation->steps()->where('name', 'Check Postgres health')->first()->script)->toContain('docker compose')
|
||||
->and($operation->steps()->pluck('script')->implode("\n"))->not->toContain('docker run');
|
||||
|
||||
Bus::assertDispatched(RunStep::class);
|
||||
});
|
||||
|
||||
it('resolves encrypted operation step secrets only for execution', function () {
|
||||
$step = new OperationStep([
|
||||
'script' => 'docker login --password [!password!]',
|
||||
'secrets' => ['password' => 'secret'],
|
||||
]);
|
||||
|
||||
expect($step->script)->toContain('[!password!]')
|
||||
->and($step->scriptForExecution())->toBe('docker login --password secret');
|
||||
});
|
||||
|
||||
it('extracts runtime state markers from operation step logs', function () {
|
||||
$step = new OperationStep([
|
||||
'logs' => "starting\ncontainer_id=abc123\nhealth_status=healthy\n",
|
||||
]);
|
||||
|
||||
expect($step->capturedRuntimeState())->toBe([
|
||||
'container_id' => 'abc123',
|
||||
'health_status' => 'healthy',
|
||||
]);
|
||||
});
|
||||
|
||||
it('cascades operation cancellation when a step fails', function () {
|
||||
$server = serviceDeploymentServer();
|
||||
$service = Service::factory()->for($server)->create();
|
||||
$parent = $service->operations()->create([
|
||||
'kind' => OperationKind::ENVIRONMENT_DEPLOY,
|
||||
'status' => OperationStatus::IN_PROGRESS,
|
||||
]);
|
||||
$serviceDeploy = $service->operations()->create([
|
||||
'parent_id' => $parent->id,
|
||||
'kind' => OperationKind::SERVICE_DEPLOY,
|
||||
'status' => OperationStatus::IN_PROGRESS,
|
||||
]);
|
||||
$replicaDeploy = $service->operations()->create([
|
||||
'parent_id' => $serviceDeploy->id,
|
||||
'kind' => OperationKind::REPLICA_DEPLOY,
|
||||
'status' => OperationStatus::PENDING,
|
||||
]);
|
||||
$gatewayCutover = $service->operations()->create([
|
||||
'parent_id' => $parent->id,
|
||||
'kind' => OperationKind::GATEWAY_CUTOVER,
|
||||
'status' => OperationStatus::PENDING,
|
||||
]);
|
||||
$step = $serviceDeploy->steps()->create([
|
||||
'name' => 'Failing step',
|
||||
'order' => 1,
|
||||
'status' => OperationStatus::IN_PROGRESS,
|
||||
'script' => 'false',
|
||||
]);
|
||||
$replicaDeploy->steps()->create([
|
||||
'name' => 'Replica step',
|
||||
'order' => 1,
|
||||
'status' => OperationStatus::PENDING,
|
||||
'script' => 'true',
|
||||
]);
|
||||
$gatewayCutover->steps()->create([
|
||||
'name' => 'Gateway step',
|
||||
'order' => 1,
|
||||
'status' => OperationStatus::PENDING,
|
||||
'script' => 'true',
|
||||
]);
|
||||
|
||||
(new RunStep($step))->failed(new RuntimeException('boom'));
|
||||
|
||||
expect($serviceDeploy->refresh()->status)->toBe(OperationStatus::FAILED)
|
||||
->and($parent->refresh()->status)->toBe(OperationStatus::FAILED)
|
||||
->and($replicaDeploy->refresh()->status)->toBe(OperationStatus::CANCELLED)
|
||||
->and($gatewayCutover->refresh()->status)->toBe(OperationStatus::CANCELLED)
|
||||
->and($gatewayCutover->steps()->first()->status)->toBe(OperationStatus::CANCELLED);
|
||||
});
|
||||
|
||||
function serviceDeploymentServer(): Server
|
||||
{
|
||||
$organisation = Organisation::factory()->create();
|
||||
$provider = Provider::factory()->forOrganisation($organisation)->create();
|
||||
$network = Network::create([
|
||||
'organisation_id' => $organisation->id,
|
||||
'provider_id' => $provider->id,
|
||||
'name' => 'test-network',
|
||||
'ip_range' => '10.0.0.0/24',
|
||||
]);
|
||||
|
||||
return Server::factory()
|
||||
->forOrganisation($organisation->id)
|
||||
->forProvider($provider->id)
|
||||
->forNetwork($network->id)
|
||||
->create();
|
||||
}
|
||||
Reference in New Issue
Block a user