networks()->create([ 'name' => 'keystone', 'external_id' => 'net-12345', 'provider_id' => $provider->id, 'ip_range' => '10.0.0.0/24', ]); [$control, $workers] = $this->seedFleet($organisation, $provider, $network); $this->seedRegistries($organisation, $control); $sourceProvider = $this->seedSourceProvider($organisation); $application = $this->seedApplication($organisation, $sourceProvider); $production = $this->seedEnvironment($application, $control, $workers, 'production', 'main'); $staging = $this->seedEnvironment($application, $control, $workers, 'staging', 'develop'); $this->seedVariety($production, $staging); $this->seedOperationsHistory($control, $production, $staging); } /** * @return array{0: Server, 1: Collection} */ private function seedFleet(Organisation $organisation, Provider $provider, Network $network): array { $factory = fn (): \Database\Factories\ServerFactory => Server::factory() ->forNetwork($network->id) ->forOrganisation($organisation->id) ->forProvider($provider->id); $control = $factory()->create([ 'name' => 'keystone-control-1', 'status' => ServerStatus::ACTIVE, 'provider_status' => 'running', 'private_ip' => '10.0.0.10', 'is_control_node' => true, 'build_enabled' => true, ]); $workers = collect(range(1, 3))->map(fn (int $index): Server => $factory()->create([ 'name' => "keystone-worker-{$index}", 'status' => ServerStatus::ACTIVE, 'provider_status' => 'running', 'private_ip' => '10.0.0.'.(20 + $index), ])); return [$control, $workers]; } private function seedRegistries(Organisation $organisation, Server $control): void { $this->managedRegistry = $organisation->registries()->create([ 'name' => 'Keystone Managed', 'type' => RegistryType::MANAGED, 'url' => 'registry.keystone.internal:5000', 'storage_path' => '/home/keystone/registry/data', 'control_server_id' => $control->id, 'readiness_checks' => [ 'storage_writable' => true, 'http_reachable' => true, 'auth_configured' => true, ], ]); $this->managedRegistry->markHealthy('Registry online and serving manifests'); $organisation->registries()->create([ 'name' => 'GitHub Container Registry', 'type' => RegistryType::GHCR, 'url' => 'ghcr.io', 'credentials' => [ 'username' => 'keystone-bot', 'token' => Str::password(40), ], 'health_status' => 'healthy', 'health_message' => 'Authenticated successfully', 'health_checked_at' => now(), ]); } private function seedSourceProvider(Organisation $organisation): \App\Models\SourceProvider { return $organisation->sourceProviders()->create([ 'name' => 'Gitea', 'type' => SourceProviderType::GITEA, 'url' => 'https://git.keystone.internal', 'config' => [ 'api_url' => 'https://git.keystone.internal/api/v1', 'organisation' => 'stratbucket', ], ]); } private function seedApplication(Organisation $organisation, \App\Models\SourceProvider $sourceProvider): Application { return $organisation->applications()->create([ 'name' => 'ClipBin', 'source_provider_id' => $sourceProvider->id, 'repository_url' => 'git@git.keystone.internal:stratbucket/clipbin.git', 'repository_type' => RepositoryType::GIT, 'default_branch' => 'main', 'deploy_key_installed_at' => now()->subWeeks(2), ]); } private function seedEnvironment( Application $application, Server $control, Collection $workers, string $name, string $branch, ): Environment { $environment = app(CreateLaravelEnvironment::class)->execute($application, $name, $branch); $web = $environment->services()->where('type', ServiceType::LARAVEL)->firstOrFail(); $web->forceFill(['server_id' => $workers->first()->id])->save(); $this->createReplica($web, $workers->first(), 80); $postgres = $this->createDependencyService( $environment, $workers->get(1), 'postgres', ServiceCategory::DATABASE, ServiceType::POSTGRES, '18', 5432, DeployPolicy::DEPENDENCY_ONLY, ); $valkey = $this->createDependencyService( $environment, $workers->get(2), 'valkey', ServiceCategory::CACHE, ServiceType::VALKEY, '8', 6379, DeployPolicy::DEPENDENCY_ONLY, ); $caddy = $this->createDependencyService( $environment, $control, 'gateway', ServiceCategory::GATEWAY, ServiceType::CADDY, '2', 80, DeployPolicy::MANUAL_OR_ON_ROUTE_CHANGE, ); app(AttachManagedService::class)->execute($environment, $postgres, EnvironmentAttachmentRole::DATABASE, isPrimary: true); app(AttachManagedService::class)->execute($environment, $valkey, EnvironmentAttachmentRole::CACHE); app(AttachManagedService::class)->execute($environment, $caddy, EnvironmentAttachmentRole::GATEWAY); foreach ([$web, $postgres, $valkey, $caddy] as $service) { foreach ($service->replicas()->get() as $replica) { app(RegisterServiceEndpoint::class)->execute($replica); } } $this->seedUserVariables($environment); $this->advanceToRunning($environment); return $environment->refresh(); } private function createDependencyService( Environment $environment, Server $server, string $name, ServiceCategory $category, ServiceType $type, string $version, int $port, DeployPolicy $deployPolicy, ): Service { $service = $environment->services()->create([ 'organisation_id' => $environment->application->organisation_id, 'server_id' => $server->id, 'name' => $name, 'category' => $category, 'type' => $type, 'version' => $version, 'version_track' => $version, 'driver_name' => "{$type->value}.{$version}", 'status' => ServiceStatus::NOT_INSTALLED, 'deploy_policy' => $deployPolicy, 'process_roles' => [], 'desired_replicas' => 1, 'config' => [], ]); if (method_exists($service->driver(), 'defaultCredentials')) { $service->credentials = $service->driver()->defaultCredentials(); $service->save(); } $this->createReplica($service, $server, $port); return $service; } private function createReplica(Service $service, Server $server, int $port): void { $service->replicas()->create([ 'server_id' => $server->id, 'container_name' => "keystone-service-{$service->id}-1", 'internal_host' => "keystone-service-{$service->id}", 'internal_port' => $port, 'status' => 'pending', 'health_status' => 'unknown', 'config' => [], ]); } private function seedUserVariables(Environment $environment): void { $values = [ 'APP_NAME' => 'ClipBin', 'APP_ENV' => $environment->name, 'LOG_CHANNEL' => 'stderr', ]; foreach ($values as $key => $value) { $environment->variables()->updateOrCreate(['key' => $key], [ 'value' => $value, 'source' => EnvironmentVariableSource::USER, 'overridable' => true, ]); } } private function advanceToRunning(Environment $environment): void { $environment->forceFill(['status' => 'active'])->save(); foreach ($environment->services()->with('replicas', 'slices')->get() as $service) { $digest = $this->digest(); $service->forceFill([ 'status' => ServiceStatus::RUNNING, 'current_image_digest' => $digest, 'available_image_digest' => $digest, ])->save(); $service->replicas->each->forceFill([ 'status' => 'running', 'health_status' => 'healthy', 'image_digest' => $digest, ])->each->save(); $service->slices->each->forceFill(['status' => 'active'])->each->save(); } $this->completeSliceProvisionOperations($environment); } private function completeSliceProvisionOperations(Environment $environment): void { foreach ($environment->services()->with('slices.operations.steps')->get() as $service) { foreach ($service->slices as $slice) { foreach ($slice->operations as $operation) { $operation->forceFill([ 'status' => OperationStatus::COMPLETED, 'started_at' => now()->subMinutes(8), 'finished_at' => now()->subMinutes(7), ])->save(); $operation->steps->each->forceFill([ 'status' => OperationStatus::COMPLETED, 'logs' => 'Slice provisioned successfully.', 'started_at' => now()->subMinutes(8), 'finished_at' => now()->subMinutes(7), ])->each->save(); } } } } private function seedVariety(Environment $production, Environment $staging): void { $stagingValkey = $staging->services()->where('type', ServiceType::VALKEY)->firstOrFail(); $stagingValkey->replicas()->update(['health_status' => 'unhealthy']); $production->buildArtifacts()->create([ 'commit_sha' => $this->sha(), 'image_tag' => 'clipbin:production-'.Str::random(7), 'image_digest' => $this->digest(), 'registry_ref' => $this->managedRegistry->url.'/keystone/clipbin', 'built_by_service_id' => $production->services()->where('type', ServiceType::LARAVEL)->value('id'), 'status' => BuildArtifactStatus::AVAILABLE, 'metadata' => ['branch' => 'main', 'build_seconds' => 142], ]); $staging->buildArtifacts()->create([ 'commit_sha' => $this->sha(), 'image_tag' => 'clipbin:staging-'.Str::random(7), 'registry_ref' => $this->managedRegistry->url.'/keystone/clipbin', 'built_by_service_id' => $staging->services()->where('type', ServiceType::LARAVEL)->value('id'), 'status' => BuildArtifactStatus::BUILDING, 'metadata' => ['branch' => 'develop'], ]); } private function seedOperationsHistory(Server $control, Environment $production, Environment $staging): void { $registryOp = $control->operations()->create([ 'kind' => OperationKind::REGISTRY_PROVISION, 'status' => OperationStatus::COMPLETED, 'started_at' => now()->subDays(5), 'finished_at' => now()->subDays(5)->addMinutes(4), ]); $registryOp->steps()->create([ 'name' => 'Provision managed registry', 'order' => 1, 'status' => OperationStatus::COMPLETED, 'script' => 'docker run -d --name keystone-registry registry:2', 'logs' => 'Registry container started and reachable.', 'started_at' => now()->subDays(5), 'finished_at' => now()->subDays(5)->addMinutes(4), ]); $serverOp = $control->operations()->create([ 'kind' => OperationKind::SERVER_PROVISION, 'status' => OperationStatus::COMPLETED, 'started_at' => now()->subDays(6), 'finished_at' => now()->subDays(6)->addMinutes(9), ]); $serverOp->steps()->create([ 'name' => 'Install container runtime', 'order' => 1, 'status' => OperationStatus::COMPLETED, 'script' => 'apt-get install -y docker-ce', 'logs' => 'Docker installed, daemon active.', 'started_at' => now()->subDays(6), 'finished_at' => now()->subDays(6)->addMinutes(9), ]); $deployOp = $production->operations()->create([ 'kind' => OperationKind::ENVIRONMENT_DEPLOY, 'status' => OperationStatus::COMPLETED, 'started_at' => now()->subHours(3), 'finished_at' => now()->subHours(3)->addMinutes(6), ]); $deployOp->steps()->create([ 'name' => 'Build application image', 'order' => 1, 'status' => OperationStatus::COMPLETED, 'script' => 'keystone build --env production', 'logs' => 'Image built and pushed to managed registry.', 'started_at' => now()->subHours(3), 'finished_at' => now()->subHours(3)->addMinutes(4), ]); $web = $production->services()->where('type', ServiceType::LARAVEL)->firstOrFail(); $serviceOp = $web->operations()->create([ 'parent_id' => $deployOp->id, 'kind' => OperationKind::SERVICE_DEPLOY, 'status' => OperationStatus::COMPLETED, 'started_at' => now()->subHours(3)->addMinutes(4), 'finished_at' => now()->subHours(3)->addMinutes(6), ]); $serviceOp->steps()->create([ 'name' => 'Roll out web replica', 'order' => 1, 'status' => OperationStatus::COMPLETED, 'script' => 'keystone service:deploy web', 'logs' => 'Replica healthy, traffic switched.', 'started_at' => now()->subHours(3)->addMinutes(4), 'finished_at' => now()->subHours(3)->addMinutes(6), ]); $inProgress = $staging->operations()->create([ 'kind' => OperationKind::ENVIRONMENT_DEPLOY, 'status' => OperationStatus::IN_PROGRESS, 'started_at' => now()->subMinutes(2), ]); $inProgress->steps()->create([ 'name' => 'Build application image', 'order' => 1, 'status' => OperationStatus::IN_PROGRESS, 'script' => 'keystone build --env staging', 'logs' => 'Compiling assets...', 'started_at' => now()->subMinutes(2), ]); $stagingValkey = $staging->services()->where('type', ServiceType::VALKEY)->firstOrFail(); $failedOp = $stagingValkey->operations()->create([ 'kind' => OperationKind::SERVICE_DEPLOY, 'status' => OperationStatus::FAILED, 'started_at' => now()->subHours(1), 'finished_at' => now()->subHours(1)->addMinutes(2), ]); $failedOp->steps()->create([ 'name' => 'Start valkey replica', 'order' => 1, 'status' => OperationStatus::FAILED, 'script' => 'keystone service:deploy valkey', 'logs' => 'Pulling image valkey/valkey:8...', 'error_logs' => 'Health check failed: connection refused on 6379 after 30s.', 'started_at' => now()->subHours(1), 'finished_at' => now()->subHours(1)->addMinutes(2), ]); } private function digest(): string { return 'sha256:'.hash('sha256', Str::uuid()->toString()); } private function sha(): string { return substr(hash('sha1', Str::uuid()->toString()), 0, 40); } }