remoteRunner = new class implements RemoteCommandRunner { /** @var array */ public array $scripts = []; public function run(Server $server, string $script): string { $this->scripts[] = $script; return str_contains($script, 'docker buildx imagetools inspect') || str_contains($script, 'push_output=$(docker push') ? "image_digest=sha256:registrydigest\n" : "image_digest=billing-api:aaaaaaaaaaaa@sha256:localdigest\n"; } }; app()->instance(RemoteCommandRunner::class, $this->remoteRunner); }); it('builds a target-server artifact over ssh with a temporary deploy key and stores the resolved digest', function () { $organisation = Organisation::factory()->create(); $server = buildServerFor($organisation); $application = Application::factory()->for($organisation)->create([ 'name' => 'Billing API', 'repository_url' => 'git@example.com:org/repo.git', ]); app(GenerateDeployKey::class)->execute($application, [ 'public' => 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestPublicKey keystone', 'private' => "-----BEGIN OPENSSH PRIVATE KEY-----\ntest\n-----END OPENSSH PRIVATE KEY-----", 'fingerprint' => 'SHA256:test', ]); $environment = app(CreateLaravelEnvironment::class)->execute($application->refresh(), 'production'); $environment->services()->first()->update(['server_id' => $server->id]); $artifact = app(PlanBuildArtifact::class)->execute($environment, str_repeat('a', 40)); $built = app(BuildApplicationArtifact::class)->execute($artifact); expect($built->status)->toBe(BuildArtifactStatus::AVAILABLE) ->and($built->image_digest)->toBe('sha256:localdigest') ->and($this->remoteRunner->scripts[0])->toContain('GIT_SSH_COMMAND') ->and($this->remoteRunner->scripts[0])->toContain('git clone --depth 1 --branch') ->and($this->remoteRunner->scripts[0])->toContain('docker build --file Dockerfile.keystone') ->and($this->remoteRunner->scripts[0])->toContain('/home/keystone/operations/build-') ->and($this->remoteRunner->scripts[0])->toContain('trap cleanup EXIT'); }); it('resolves external registry artifacts without building locally', function () { $organisation = Organisation::factory()->create(); $server = buildServerFor($organisation); $organisation->registries()->create([ 'name' => 'GHCR', 'type' => RegistryType::GHCR, 'url' => 'ghcr.io/example', ]); $application = Application::factory()->for($organisation)->create([ 'name' => 'Billing API', 'repository_url' => 'git@example.com:org/repo.git', ]); $environment = app(CreateLaravelEnvironment::class)->execute($application->refresh(), 'production'); $environment->services()->first()->update(['server_id' => $server->id]); $environment->services()->first()->update(['desired_replicas' => 2]); $artifact = app(PlanBuildArtifact::class)->execute($environment, str_repeat('b', 40)); $built = app(BuildApplicationArtifact::class)->execute($artifact); expect($built->registry_ref)->toBe('ghcr.io/example/billing-api:bbbbbbbbbbbb') ->and($built->image_digest)->toBe('sha256:registrydigest') ->and($this->remoteRunner->scripts[0])->toContain('docker buildx imagetools inspect') ->and($this->remoteRunner->scripts[0])->toContain('ghcr.io/example/billing-api:bbbbbbbbbbbb') ->and($this->remoteRunner->scripts[0])->not->toContain('docker build --file') ->and($this->remoteRunner->scripts[0])->not->toContain('git clone'); }); it('builds and pushes managed registry artifacts without embedding registry credentials', function () { $organisation = Organisation::factory()->create(); $server = buildServerFor($organisation, true); $organisation->registries()->create([ 'name' => 'Managed', 'type' => RegistryType::MANAGED, 'url' => 'registry.example.com', 'credentials' => [ 'build_username' => 'keystone-build', 'build_password' => 'super-secret-password', 'runtime_username' => 'keystone-runtime', 'runtime_password' => 'runtime-secret', ], 'control_server_id' => $server->id, 'health_status' => 'healthy', 'readiness_checks' => ['control_https' => 'passed', 'build_push' => 'passed'], 'ready_at' => now(), ]); $application = Application::factory()->for($organisation)->create([ 'name' => 'Billing API', 'repository_url' => 'git@example.com:org/repo.git', ]); app(GenerateDeployKey::class)->execute($application, [ 'public' => 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestPublicKey keystone', 'private' => "-----BEGIN OPENSSH PRIVATE KEY-----\ntest\n-----END OPENSSH PRIVATE KEY-----", 'fingerprint' => 'SHA256:test', ]); $environment = app(CreateLaravelEnvironment::class)->execute($application->refresh(), 'production'); $environment->services()->first()->update([ 'server_id' => $server->id, 'desired_replicas' => 2, ]); $artifact = app(PlanBuildArtifact::class)->execute($environment, str_repeat('c', 40)); $built = app(BuildApplicationArtifact::class)->execute($artifact); expect($built->registry_ref)->toBe("registry.example.com/keystone/{$application->uuid}/{$environment->uuid}:cccccccccccc") ->and($built->metadata['build_strategy'])->toBe(BuildStrategy::DEDICATED_BUILDER->value) ->and($built->image_digest)->toBe('sha256:registrydigest') ->and($this->remoteRunner->scripts[0])->toContain('flock 9') ->and($this->remoteRunner->scripts[0])->toContain('docker login') ->and($this->remoteRunner->scripts[0])->toContain(base64_encode('super-secret-password')) ->and($this->remoteRunner->scripts[0])->toContain('docker build --file Dockerfile.keystone') ->and($this->remoteRunner->scripts[0])->toContain('push_output=$(docker push') ->and($this->remoteRunner->scripts[0])->toContain('digest: \\(sha256:') ->and($this->remoteRunner->scripts[0])->not->toContain('docker manifest inspect') ->and($this->remoteRunner->scripts[0])->not->toContain('"digest"') ->and($this->remoteRunner->scripts[0])->not->toContain('super-secret-password') ->and($this->remoteRunner->scripts[0])->toContain('DOCKER_CONFIG=\'/root/.docker\''); }); function buildServerFor(Organisation $organisation, bool $buildEnabled = false): Server { $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([ 'is_control_node' => $buildEnabled, 'build_enabled' => $buildEnabled, ]); }