Add managed registry provisioning, pruning, and readiness tracking

This commit is contained in:
2026-06-08 20:44:16 +01:00
parent 5b977c1f41
commit 3a851db08f
52 changed files with 2706 additions and 116 deletions

View File

@@ -57,7 +57,7 @@ it('creates a parent environment operation with child service deploy operations'
->forProvider($provider->id)
->forNetwork($network->id)
->create();
$application = Application::factory()->for($organisation)->create();
$application = Application::factory()->for($organisation)->create(['name' => 'Keystone Test App']);
generateDeployKey($application);
$environment = Environment::factory()->for($application)->create();
$service = Service::factory()->for($environment)->for($server)->create([
@@ -113,7 +113,7 @@ it('creates replica route configure and gateway cutover child operations', funct
->forProvider($provider->id)
->forNetwork($network->id)
->create();
$application = Application::factory()->for($organisation)->create();
$application = Application::factory()->for($organisation)->create(['name' => 'Keystone Test App']);
generateDeployKey($application);
$environment = Environment::factory()->for($application)->create();
$web = Service::factory()->for($environment)->for($server)->create([
@@ -191,6 +191,7 @@ it('creates replica route configure and gateway cutover child operations', funct
->first();
$serviceDeploy = $parent->children()->where('kind', OperationKind::SERVICE_DEPLOY)->first();
$compose = renderedComposeFrom($serviceDeploy->steps()->where('name', 'Render Compose files')->first()->script, $web->id);
expect($serviceDeploy->children()->where('kind', OperationKind::REPLICA_DEPLOY)->count())->toBe(2)
->and($parent->children()->where('kind', OperationKind::SLICE_CONFIGURE)->count())->toBe(1)
@@ -203,6 +204,8 @@ it('creates replica route configure and gateway cutover child operations', funct
->toContain('docker pull')
->and($serviceDeploy->children()->where('kind', OperationKind::REPLICA_DEPLOY)->first()->steps()->where('name', 'Pull image for replica 1')->first()->script)
->toContain('@sha256:deploymentdigest')
->and($compose)
->toContain('image: "registry.example.com/keystone-test-app:aaaaaaaaaaaa@sha256:deploymentdigest"')
->and($serviceDeploy->children()->where('kind', OperationKind::REPLICA_DEPLOY)->first()->steps()->where('name', 'Health check replica 1')->first()->script)
->toContain('health_status=')
->and($parent->children()->where('kind', OperationKind::SLICE_CONFIGURE)->first()->steps()->first()->script)
@@ -364,6 +367,80 @@ it('places desired replicas across configured server placements', function () {
->toBe($servers->pluck('id')->all());
});
it('renders compose and root registry auth on each managed registry replica server', function () {
$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',
]);
$servers = Server::factory()
->count(2)
->forOrganisation($organisation->id)
->forProvider($provider->id)
->forNetwork($network->id)
->create();
$servers[0]->update([
'is_control_node' => true,
'build_enabled' => true,
]);
$organisation->registries()->create([
'name' => 'Managed',
'type' => 'managed',
'url' => 'registry.example.com',
'credentials' => [
'build_username' => 'keystone-build',
'build_password' => 'build-secret',
'runtime_username' => 'keystone-runtime',
'runtime_password' => 'runtime-secret',
],
'control_server_id' => $servers[0]->id,
'health_status' => 'healthy',
'readiness_checks' => ['control_https' => 'passed', 'build_push' => 'passed'],
'ready_at' => now(),
]);
$application = Application::factory()->for($organisation)->create(['name' => 'Keystone Test App']);
generateDeployKey($application);
$environment = Environment::factory()->for($application)->create();
$service = Service::factory()->for($environment)->create([
'organisation_id' => $organisation->id,
'name' => 'web',
'category' => ServiceCategory::APPLICATION,
'type' => ServiceType::LARAVEL,
'version' => 'php-8.4',
'version_track' => 'php-8.4',
'driver_name' => 'laravel.php-8.4',
'deploy_policy' => DeployPolicy::WITH_ENVIRONMENT,
'desired_replicas' => 2,
'config' => [
'server_ids' => $servers->pluck('id')->all(),
],
]);
(new DeployEnvironment($environment))->handle();
$serviceDeploy = $service->operations()
->where('kind', OperationKind::SERVICE_DEPLOY)
->firstOrFail();
$replicaOperations = $serviceDeploy->children()
->where('kind', OperationKind::REPLICA_DEPLOY)
->with('steps')
->get();
expect($replicaOperations)->toHaveCount(2)
->and($replicaOperations[0]->steps->pluck('name')->all())->toContain('Render replica 1 Compose files')
->and($replicaOperations[1]->steps->pluck('name')->all())->toContain('Render replica 2 Compose files');
$authStep = $replicaOperations[0]->steps->firstWhere('name', 'Configure registry auth for replica 1');
expect($authStep->script)->toContain("DOCKER_CONFIG='/root/.docker'")
->and($authStep->script)->toContain('[!registry_password_base64!]')
->and($authStep->script)->not->toContain('runtime-secret')
->and($authStep->secrets['registry_password_base64'])->toBe(base64_encode('runtime-secret'));
});
it('skips environment service operations when the target revision is already available', function () {
$organisation = Organisation::factory()->create();
$application = Application::factory()->for($organisation)->create();
@@ -404,6 +481,17 @@ function generateDeployKey(Application $application): void
]);
}
function renderedComposeFrom(string $script, int $serviceId): string
{
preg_match(
'/printf %s \'(?<encoded>[^\']+)\' \| base64 -d > \/home\/keystone\/services\/'.$serviceId.'\/compose\.yml/',
$script,
$matches,
);
return base64_decode($matches['encoded'] ?? '', true) ?: '';
}
it('blocks multi-server deploys that do not have a registry', function () {
$organisation = Organisation::factory()->create();
$application = Application::factory()->for($organisation)->create();