*/ private const SKIP_SERVER_SETTINGS = [ 'id', 'server_id', 'created_at', 'updated_at', 'is_reachable', 'is_usable', 'force_disabled', 'sentinel_token', 'sentinel_custom_url', 'is_sentinel_enabled', 'is_sentinel_debug_enabled', ]; /** * Application attributes that are relational FKs remapped on import. * * @var list */ private const SKIP_APPLICATION_ATTRS = [ 'id', 'created_at', 'updated_at', 'deleted_at', 'environment_id', 'destination_id', 'destination_type', 'source_id', 'source_type', 'private_key_id', 'additional_servers_count', 'additional_networks_count', 'server_status', ]; /** * @return array */ public function export(Server $server, bool $includeSensitive = true): array { if ($server->id === 0) { throw new RuntimeException('The Coolify host (localhost) cannot be transferred between instances.'); } $server->loadMissing(['settings', 'privateKey', 'standaloneDockers', 'swarmDockers', 'cloudProviderToken', 'sslCertificates']); $privateKey = $server->privateKey; if (! $privateKey instanceof PrivateKey) { throw new RuntimeException('Server has no private key to export.'); } if (! $includeSensitive) { throw new RuntimeException('Server transfer export requires sensitive data access (read:sensitive).'); } // applications() also includes multi-destination attachments on this server; // unique by id so a multi-dest app is only considered once for validation/export. $applications = $server->applications()->unique('id')->values(); $databases = collect($server->databases())->unique(fn ($db) => $db::class.':'.$db->id)->values(); $services = $server->services()->get()->unique('id')->values(); $this->assertNoAdditionalDestinations($server, $applications); $projectIds = $this->collectProjectIds($applications, $databases, $services); $projects = Project::query()->whereIn('id', $projectIds)->orderBy('name')->get(); $destinationUuidByKey = $this->destinationUuidMap($server); $dependencies = $this->collectDependencies($server, $applications, $databases, $services); $sourceInstanceUrl = rtrim((string) (instanceSettings()->fqdn ?: config('app.url')), '/'); $warnings = $this->buildWarnings($applications, $dependencies, $sourceInstanceUrl); $payload = ServerTransferBundle::wrap([ 'source_instance' => [ 'url' => $sourceInstanceUrl, 'name' => config('app.name'), ], 'warnings' => $warnings, // Back-compat: single server key 'private_key' => $this->exportPrivateKey($privateKey), // All keys needed by server, apps, and git sources 'private_keys' => $dependencies['private_keys']->map(fn (PrivateKey $key) => $this->exportPrivateKey($key))->values()->all(), 'github_apps' => $dependencies['github_apps']->map(fn (GithubApp $app) => $this->exportGithubApp($app))->values()->all(), 'gitlab_apps' => $dependencies['gitlab_apps']->map(fn (GitlabApp $app) => $this->exportGitlabApp($app))->values()->all(), 's3_storages' => $dependencies['s3_storages']->map(fn (S3Storage $storage) => $this->exportS3Storage($storage))->values()->all(), 'cloud_provider_tokens' => $dependencies['cloud_provider_tokens']->map(fn (CloudProviderToken $token) => $this->exportCloudProviderToken($token))->values()->all(), 'ssl_certificates' => $this->exportSslCertificates($server), 'volume_backups' => $this->exportVolumeBackups($applications, $databases, $services), 'server' => $this->exportServer($server), 'destinations' => $this->exportDestinations($server), 'shared_environment_variables' => [ 'server' => $this->exportSharedEnvVars( SharedEnvironmentVariable::query() ->where('type', 'server') ->where('server_id', $server->id) ->get() ), ], 'projects' => $projects->map(function (Project $project) use ($server, $destinationUuidByKey, $applications, $databases, $services) { return $this->exportProject($project, $server, $destinationUuidByKey, $applications, $databases, $services); })->values()->all(), ]); return $payload; } /** * @param Collection $applications * @param Collection $databases * @param Collection $services * @return array{ * private_keys: Collection, * github_apps: Collection, * gitlab_apps: Collection, * s3_storages: Collection, * cloud_provider_tokens: Collection * } */ private function collectDependencies(Server $server, Collection $applications, Collection $databases, Collection $services): array { $privateKeys = collect([$server->privateKey])->filter(); $githubApps = collect(); $gitlabApps = collect(); $s3Ids = collect(); foreach ($applications as $application) { if ($application->private_key_id) { $key = PrivateKey::find($application->private_key_id); if ($key) { $privateKeys->push($key); } } if ($application->source_type === GithubApp::class && $application->source_id !== null) { $gh = GithubApp::find($application->source_id); // System-wide GitHub Apps are instance-owned; do not copy them. // Application export still records source.uuid so import can re-link // to an existing system-wide app on the target instance. if ($gh && ! $gh->is_system_wide) { $githubApps->push($gh); if ($gh->private_key_id) { $pk = PrivateKey::find($gh->private_key_id); if ($pk) { $privateKeys->push($pk); } } } } if ($application->source_type === GitlabApp::class && $application->source_id !== null) { $gl = GitlabApp::find($application->source_id); // System-wide GitLab Apps are instance-owned; re-link by UUID on import. if ($gl && ! $gl->is_system_wide) { $gitlabApps->push($gl); if ($gl->private_key_id) { $pk = PrivateKey::find($gl->private_key_id); if ($pk) { $privateKeys->push($pk); } } } } } $collectVolumeS3 = function ($resource) use (&$s3Ids): void { if (method_exists($resource, 'persistentStorages')) { foreach ($resource->persistentStorages as $volume) { foreach ($volume->scheduledBackups as $vb) { if ($vb->s3_storage_id) { $s3Ids->push($vb->s3_storage_id); } } } } if (method_exists($resource, 'fileStorages')) { foreach ($resource->fileStorages as $file) { foreach ($file->scheduledBackups as $vb) { if ($vb->s3_storage_id) { $s3Ids->push($vb->s3_storage_id); } } } } }; foreach ($databases as $database) { if (method_exists($database, 'scheduledBackups')) { foreach ($database->scheduledBackups as $backup) { if ($backup->s3_storage_id) { $s3Ids->push($backup->s3_storage_id); } } } $database->loadMissing(['persistentStorages.scheduledBackups', 'fileStorages.scheduledBackups']); $collectVolumeS3($database); } foreach ($applications as $application) { $application->loadMissing(['persistentStorages.scheduledBackups', 'fileStorages.scheduledBackups', 'previews.persistentStorages.scheduledBackups']); $collectVolumeS3($application); foreach ($application->previews as $preview) { $collectVolumeS3($preview); } } foreach ($services as $service) { $service->loadMissing([ 'applications.environment_variables', 'applications.persistentStorages.scheduledBackups', 'applications.fileStorages.scheduledBackups', 'databases.persistentStorages.scheduledBackups', 'databases.fileStorages.scheduledBackups', 'databases.scheduledBackups', ]); foreach ($service->applications as $serviceApp) { $collectVolumeS3($serviceApp); } foreach ($service->databases as $serviceDb) { foreach ($serviceDb->scheduledBackups as $backup) { if ($backup->s3_storage_id) { $s3Ids->push($backup->s3_storage_id); } } $collectVolumeS3($serviceDb); } } $cloudTokens = collect(); if ($server->cloud_provider_token_id) { $token = CloudProviderToken::find($server->cloud_provider_token_id); if ($token) { $cloudTokens->push($token); } } return [ 'private_keys' => $privateKeys->unique('id')->values(), 'github_apps' => $githubApps->unique('id')->values(), 'gitlab_apps' => $gitlabApps->unique('id')->values(), 's3_storages' => S3Storage::query()->whereIn('id', $s3Ids->unique()->filter()->all())->get(), 'cloud_provider_tokens' => $cloudTokens->unique('id')->values(), ]; } /** * @param Collection $applications * @param array $dependencies * @return list */ private function buildWarnings(Collection $applications, array $dependencies, string $sourceInstanceUrl): array { $warnings = []; if ($dependencies['github_apps']->isNotEmpty()) { $warnings[] = 'Team-scoped GitHub Apps were exported with credentials (system-wide apps are skipped). After import, update each GitHub App webhook URL (and setup/callback URL if used) to this Coolify instance so push/PR automations keep working. ' ."Webhook path: {$sourceInstanceUrl}/webhooks/source/github/events " .'(replace host with the target instance FQDN after import). ' .'Also reinstall/refresh the GitHub App installation if installation_id is invalid on the new host.'; } $usesSystemWideGithub = $applications->contains(function (Application $app) { if ($app->source_type !== GithubApp::class || $app->source_id === null) { return false; } $gh = GithubApp::find($app->source_id); return $gh?->is_system_wide === true; }); if ($usesSystemWideGithub) { $warnings[] = 'Some applications use a system-wide GitHub App, which is not exported. On import, Coolify will re-link to a matching system-wide GitHub App on the target instance (by UUID) if one exists.'; } $usesSystemWideGitlab = $applications->contains(function (Application $app) { if ($app->source_type !== GitlabApp::class || $app->source_id === null) { return false; } $gl = GitlabApp::find($app->source_id); return $gl?->is_system_wide === true; }); if ($usesSystemWideGitlab) { $warnings[] = 'Some applications use a system-wide GitLab App, which is not exported. On import, Coolify will re-link to a matching system-wide GitLab App on the target instance (by UUID) if one exists.'; } if ($dependencies['gitlab_apps']->isNotEmpty()) { $warnings[] = 'Team-scoped GitLab Apps/OAuth sources were exported (system-wide apps are skipped). After import, update GitLab webhook URLs to the target Coolify instance ' ."({$sourceInstanceUrl}/webhooks/source/gitlab/events — use the new FQDN) and refresh OAuth tokens if needed."; } if ($dependencies['s3_storages']->isNotEmpty()) { $warnings[] = 'S3 storage credentials were exported. Confirm endpoint access from the target instance and that bucket policies still apply.'; } return $warnings; } /** * @return array */ private function exportPrivateKey(PrivateKey $key): array { try { $material = $key->private_key; } catch (\Throwable $e) { throw new RuntimeException( "Cannot decrypt private key \"{$key->name}\" (uuid={$key->uuid}). " .'This instance APP_KEY does not match the key that encrypted the data ' .'(common after regenerating .dev-instances/*.env). ' .'Restore the original APP_KEY or re-save the private key material. ' .'Original error: '.$e->getMessage(), previous: $e ); } return [ 'uuid' => $key->uuid, 'name' => $key->name, 'description' => $key->description, 'private_key' => $material, 'is_git_related' => (bool) $key->is_git_related, 'fingerprint' => $key->fingerprint, ]; } /** * @return array */ private function exportServer(Server $server): array { $settings = $server->settings; $settingsPayload = []; if ($settings) { foreach ($settings->getAttributes() as $key => $value) { if (in_array($key, self::SKIP_SERVER_SETTINGS, true)) { continue; } // Read through cast so encrypted fields are plaintext for re-encryption on import. $settingsPayload[$key] = $settings->{$key}; } } return [ 'uuid' => $server->uuid, 'name' => $server->name, 'description' => $server->description, 'ip' => (string) $server->ip, 'port' => (int) $server->port, 'user' => (string) $server->user, 'proxy' => $server->proxy?->toArray() ?? [], 'is_build_server' => (bool) $server->is_build_server, 'cloud_provider_token_uuid' => $server->cloudProviderToken?->uuid, 'settings' => $settingsPayload, ]; } /** * @return list> */ private function exportDestinations(Server $server): array { $items = []; foreach ($server->standaloneDockers as $destination) { $items[] = [ 'uuid' => $destination->uuid, 'name' => $destination->name, 'network' => $destination->network, 'type' => 'standalone', ]; } foreach ($server->swarmDockers as $destination) { $items[] = [ 'uuid' => $destination->uuid, 'name' => $destination->name, 'network' => $destination->network, 'type' => 'swarm', ]; } return $items; } /** * @param Collection $applications * @param Collection $databases * @param Collection $services * @param array $destinationUuidByKey * @return array */ private function exportProject( Project $project, Server $server, array $destinationUuidByKey, Collection $applications, Collection $databases, Collection $services, ): array { $environments = $project->environments()->orderBy('name')->get(); return [ 'uuid' => $project->uuid, 'name' => $project->name, 'description' => $project->description, 'shared_environment_variables' => $this->exportSharedEnvVars( SharedEnvironmentVariable::query() ->where('type', 'project') ->where('project_id', $project->id) ->get() ), 'environments' => $environments->map(function (Environment $environment) use ($destinationUuidByKey, $applications, $databases, $services) { $envApps = $applications->where('environment_id', $environment->id)->values(); $envDbs = $databases->where('environment_id', $environment->id)->values(); $envServices = $services->where('environment_id', $environment->id)->values(); return [ 'uuid' => $environment->uuid, 'name' => $environment->name, 'description' => $environment->description, 'shared_environment_variables' => $this->exportSharedEnvVars( SharedEnvironmentVariable::query() ->where('type', 'environment') ->where('environment_id', $environment->id) ->get() ), 'applications' => $envApps->map(fn (Application $app) => $this->exportApplication($app, $destinationUuidByKey))->all(), 'databases' => $envDbs->map(fn (Model $db) => $this->exportDatabase($db, $destinationUuidByKey))->all(), 'services' => $envServices->map(fn (Service $service) => $this->exportService($service, $destinationUuidByKey))->all(), ]; })->values()->all(), ]; } /** * @param array $destinationUuidByKey * @return array */ private function exportApplication(Application $application, array $destinationUuidByKey): array { $application->loadMissing([ 'settings', 'environment_variables', 'environment_variables_preview', 'persistentStorages', 'fileStorages', 'scheduled_tasks', 'tags', 'previews.persistentStorages', 'source', 'private_key', ]); $columns = Schema::getColumnListing($application->getTable()); $attrs = []; foreach ($application->getFillable() as $field) { if (in_array($field, self::SKIP_APPLICATION_ATTRS, true)) { continue; } if (! in_array($field, $columns, true)) { continue; } $attrs[$field] = $application->{$field}; } // Ensure hidden encrypted fields are included as plaintext. foreach ([ 'http_basic_auth_password', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile', 'docker_compose', 'docker_compose_raw', 'custom_labels', ] as $hidden) { if (in_array($hidden, $columns, true)) { $attrs[$hidden] = $application->{$hidden}; } } $settings = []; if ($application->settings) { foreach ($application->settings->getFillable() as $field) { if (in_array($field, ['application_id', 'id'], true)) { continue; } $settings[$field] = $application->settings->{$field}; } } $destinationKey = $application->destination_type.':'.$application->destination_id; $source = null; if ($application->source_type === GithubApp::class && $application->source) { $source = ['type' => 'github_app', 'uuid' => $application->source->uuid]; } elseif ($application->source_type === GitlabApp::class && $application->source) { $source = ['type' => 'gitlab_app', 'uuid' => $application->source->uuid]; } return [ 'type' => 'application', 'uuid' => $application->uuid, 'destination_uuid' => $destinationUuidByKey[$destinationKey] ?? null, 'attributes' => $attrs, 'settings' => $settings, 'environment_variables' => $this->exportEnvVars($application->environment_variables()->get()), 'environment_variables_preview' => $this->exportEnvVars($application->environment_variables_preview()->get()), 'persistent_storages' => $this->exportPersistentStorages($application->persistentStorages()->get()), 'file_storages' => $this->exportFileStorages($application->fileStorages()->get()), 'scheduled_tasks' => $this->exportScheduledTasks($application->scheduled_tasks()->get()), 'tags' => $this->exportTags($application->tags), 'previews' => $this->exportPreviews($application->previews), 'source' => $source, 'private_key_uuid' => $application->private_key?->uuid, 'had_git_source' => filled($application->source_id), 'had_private_key' => filled($application->private_key_id), ]; } /** * @param array $destinationUuidByKey * @return array */ private function exportDatabase(Model $database, array $destinationUuidByKey): array { $with = ['environment_variables', 'persistentStorages', 'fileStorages']; if (method_exists($database, 'tags')) { $with[] = 'tags'; } if (method_exists($database, 'scheduledBackups')) { $with[] = 'scheduledBackups.s3'; } $database->loadMissing($with); $columns = Schema::getColumnListing($database->getTable()); $attrs = []; foreach ($database->getFillable() as $field) { if (in_array($field, ['id', 'created_at', 'updated_at', 'deleted_at', 'environment_id', 'destination_id', 'destination_type'], true)) { continue; } if (! in_array($field, $columns, true)) { continue; } $attrs[$field] = $database->{$field}; } // Secrets are often $hidden and sometimes omitted from $fillable (e.g. redis_password). foreach ($columns as $column) { if (array_key_exists($column, $attrs)) { continue; } if (str_contains($column, 'password') || str_ends_with($column, '_password')) { $attrs[$column] = $database->{$column}; } } $destinationKey = $database->destination_type.':'.$database->destination_id; return [ 'type' => class_basename($database), 'model' => $database::class, 'uuid' => $database->uuid, 'destination_uuid' => $destinationUuidByKey[$destinationKey] ?? null, 'attributes' => $attrs, 'environment_variables' => method_exists($database, 'environment_variables') ? $this->exportEnvVars($database->environment_variables()->get()) : [], 'persistent_storages' => method_exists($database, 'persistentStorages') ? $this->exportPersistentStorages($database->persistentStorages()->get()) : [], 'file_storages' => method_exists($database, 'fileStorages') ? $this->exportFileStorages($database->fileStorages()->get()) : [], 'tags' => method_exists($database, 'tags') ? $this->exportTags($database->tags) : [], 'scheduled_backups' => method_exists($database, 'scheduledBackups') ? $this->exportScheduledBackups($database->scheduledBackups) : [], ]; } /** * @param array $destinationUuidByKey * @return array */ private function exportService(Service $service, array $destinationUuidByKey): array { $service->loadMissing([ 'environment_variables', 'applications.environment_variables', 'applications.persistentStorages', 'applications.fileStorages', 'databases.persistentStorages', 'databases.fileStorages', 'databases.scheduledBackups.s3', 'scheduled_tasks', 'tags', ]); $columns = Schema::getColumnListing($service->getTable()); $attrs = []; foreach ($service->getFillable() as $field) { if (in_array($field, ['id', 'created_at', 'updated_at', 'deleted_at', 'environment_id', 'destination_id', 'destination_type', 'server_id'], true)) { continue; } if (! in_array($field, $columns, true)) { continue; } $attrs[$field] = $service->{$field}; } // Hidden compose fields foreach (['docker_compose', 'docker_compose_raw'] as $hidden) { if (in_array($hidden, $columns, true)) { $attrs[$hidden] = $service->{$hidden}; } } $destinationKey = $service->destination_type.':'.$service->destination_id; return [ 'type' => 'service', 'uuid' => $service->uuid, 'destination_uuid' => $destinationUuidByKey[$destinationKey] ?? null, 'attributes' => $attrs, 'environment_variables' => $this->exportEnvVars($service->environment_variables()->get()), 'scheduled_tasks' => $this->exportScheduledTasks($service->scheduled_tasks()->get()), 'tags' => $this->exportTags($service->tags), 'applications' => $service->applications ->map(fn ($app) => $this->exportServiceApplication($app)) ->values() ->all(), 'databases' => $service->databases ->map(fn ($db) => $this->exportServiceDatabase($db)) ->values() ->all(), ]; } /** * @return array */ private function exportServiceApplication(Model $app): array { $columns = Schema::getColumnListing($app->getTable()); $attrs = []; foreach ($app->getFillable() as $field) { if (in_array($field, ['id', 'created_at', 'updated_at', 'deleted_at', 'service_id'], true)) { continue; } if (! in_array($field, $columns, true)) { continue; } $attrs[$field] = $app->{$field}; } return [ 'uuid' => $app->uuid, 'attributes' => $attrs, // Flat fields kept for older bundles / readability 'name' => $app->name, 'fqdn' => $app->fqdn, 'image' => $app->image ?? null, 'ports' => $app->ports ?? null, 'exposes' => $app->exposes ?? null, 'exclude_from_status' => (bool) ($app->exclude_from_status ?? false), 'required_fqdn' => (bool) ($app->required_fqdn ?? false), 'human_name' => $app->human_name ?? null, 'description' => $app->description ?? null, 'is_log_drain_enabled' => (bool) ($app->is_log_drain_enabled ?? false), 'is_include_timestamps' => (bool) ($app->is_include_timestamps ?? false), 'is_gzip_enabled' => (bool) ($app->is_gzip_enabled ?? true), 'is_stripprefix_enabled' => (bool) ($app->is_stripprefix_enabled ?? true), 'environment_variables' => method_exists($app, 'environment_variables') ? $this->exportEnvVars($app->environment_variables()->get()) : [], 'persistent_storages' => method_exists($app, 'persistentStorages') ? $this->exportPersistentStorages($app->persistentStorages) : [], 'file_storages' => method_exists($app, 'fileStorages') ? $this->exportFileStorages($app->fileStorages) : [], ]; } /** * @return array */ private function exportServiceDatabase(Model $db): array { $columns = Schema::getColumnListing($db->getTable()); $attrs = []; foreach ($db->getFillable() as $field) { if (in_array($field, ['id', 'created_at', 'updated_at', 'deleted_at', 'service_id'], true)) { continue; } if (! in_array($field, $columns, true)) { continue; } $attrs[$field] = $db->{$field}; } return [ 'uuid' => $db->uuid, 'attributes' => $attrs, 'name' => $db->name, 'human_name' => $db->human_name ?? null, 'description' => $db->description ?? null, 'image' => $db->image ?? null, 'ports' => $db->ports ?? null, 'exposes' => $db->exposes ?? null, 'exclude_from_status' => (bool) ($db->exclude_from_status ?? false), 'is_log_drain_enabled' => (bool) ($db->is_log_drain_enabled ?? false), 'is_include_timestamps' => (bool) ($db->is_include_timestamps ?? false), 'is_gzip_enabled' => (bool) ($db->is_gzip_enabled ?? true), 'is_stripprefix_enabled' => (bool) ($db->is_stripprefix_enabled ?? true), 'public_port' => $db->public_port ?? null, 'public_port_timeout' => $db->public_port_timeout ?? null, 'is_public' => (bool) ($db->is_public ?? false), 'custom_type' => $db->custom_type ?? null, 'persistent_storages' => method_exists($db, 'persistentStorages') ? $this->exportPersistentStorages($db->persistentStorages) : [], 'file_storages' => method_exists($db, 'fileStorages') ? $this->exportFileStorages($db->fileStorages) : [], 'scheduled_backups' => method_exists($db, 'scheduledBackups') ? $this->exportScheduledBackups($db->scheduledBackups) : [], ]; } /** * Multi-server / multi-destination applications cannot be transferred safely: * the bundle is per-server and additional destinations may live on other hosts. * * @param Collection $applications */ private function assertNoAdditionalDestinations(Server $server, Collection $applications): void { $involvesThisServer = DB::table('additional_destinations') ->where('server_id', $server->id) ->exists(); $appIds = $applications->pluck('id')->filter()->all(); $involvesTheseApps = $appIds !== [] && DB::table('additional_destinations') ->whereIn('application_id', $appIds) ->exists(); if ($involvesThisServer || $involvesTheseApps) { throw new RuntimeException( 'This server cannot be transferred because one or more applications use additional destinations (multi-server / multi-destination deploy). ' .'Remove all additional destinations from those applications, then retry the export.' ); } } /** * @param Collection|iterable $variables * @return list> */ private function exportSharedEnvVars(iterable $variables): array { $out = []; foreach ($variables as $variable) { // Skip auto COOLIFY_SERVER_* — recreated on server create. if (in_array($variable->key, ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME'], true)) { continue; } $out[] = [ 'key' => $variable->key, 'value' => $variable->value, 'is_multiline' => (bool) $variable->is_multiline, 'is_literal' => (bool) $variable->is_literal, 'is_shown_once' => (bool) $variable->is_shown_once, 'comment' => $variable->comment, ]; } return $out; } /** * @param Collection|iterable $variables * @return list> */ private function exportEnvVars(iterable $variables): array { $out = []; foreach ($variables as $variable) { $out[] = [ 'uuid' => $variable->uuid, 'key' => $variable->key, 'value' => $variable->value, 'is_literal' => (bool) $variable->is_literal, 'is_multiline' => (bool) $variable->is_multiline, 'is_preview' => (bool) $variable->is_preview, 'is_runtime' => (bool) $variable->is_runtime, 'is_buildtime' => (bool) $variable->is_buildtime, 'is_shown_once' => (bool) $variable->is_shown_once, 'is_shared' => (bool) $variable->is_shared, 'is_required' => (bool) ($variable->is_required ?? false), 'comment' => $variable->comment, 'order' => $variable->order, ]; } return $out; } /** * @param Collection|iterable $volumes * @return list> */ private function exportPersistentStorages(iterable $volumes): array { $out = []; foreach ($volumes as $volume) { $out[] = [ 'uuid' => $volume->uuid, 'name' => $volume->name, 'mount_path' => $volume->mount_path, 'host_path' => $volume->host_path, 'is_preview_suffix_enabled' => (bool) ($volume->is_preview_suffix_enabled ?? false), ]; } return $out; } /** * @param Collection|iterable $storages * @return list> */ private function exportFileStorages(iterable $storages): array { $out = []; foreach ($storages as $storage) { $out[] = [ 'uuid' => $storage->uuid, 'fs_path' => $storage->fs_path, 'mount_path' => $storage->mount_path, 'content' => $storage->content, 'is_directory' => (bool) $storage->is_directory, 'is_host_file' => (bool) $storage->is_host_file, 'chown' => $storage->chown, 'chmod' => $storage->chmod, 'is_based_on_git' => (bool) ($storage->is_based_on_git ?? false), 'is_preview_suffix_enabled' => (bool) ($storage->is_preview_suffix_enabled ?? false), ]; } return $out; } /** * @param Collection|iterable $tasks * @return list> */ private function exportScheduledTasks(iterable $tasks): array { $out = []; foreach ($tasks as $task) { $out[] = [ 'uuid' => $task->uuid, 'name' => $task->name, 'command' => $task->command, 'frequency' => $task->frequency, 'container' => $task->container, 'timeout' => $task->timeout, 'enabled' => (bool) ($task->enabled ?? true), ]; } return $out; } /** * @param Collection|iterable $tags * @return list */ private function exportTags(iterable $tags): array { $out = []; foreach ($tags as $tag) { $out[] = [ 'uuid' => $tag->uuid, 'name' => $tag->name, ]; } return $out; } /** * @param Collection|iterable $backups * @return list> */ private function exportScheduledBackups(iterable $backups): array { $out = []; foreach ($backups as $backup) { $out[] = [ 'uuid' => $backup->uuid, 'description' => $backup->description, 'enabled' => (bool) $backup->enabled, 'save_s3' => (bool) $backup->save_s3, 'frequency' => $backup->frequency, 'databases_to_backup' => $backup->databases_to_backup, 'dump_all' => (bool) ($backup->dump_all ?? false), 'database_backup_retention_amount_locally' => $backup->database_backup_retention_amount_locally, 'database_backup_retention_days_locally' => $backup->database_backup_retention_days_locally, 'database_backup_retention_max_storage_locally' => $backup->database_backup_retention_max_storage_locally, 'database_backup_retention_amount_s3' => $backup->database_backup_retention_amount_s3, 'database_backup_retention_days_s3' => $backup->database_backup_retention_days_s3, 'database_backup_retention_max_storage_s3' => $backup->database_backup_retention_max_storage_s3, 'timeout' => $backup->timeout, 'disable_local_backup' => (bool) ($backup->disable_local_backup ?? false), 's3_storage_uuid' => $backup->s3?->uuid, 'had_s3_storage' => filled($backup->s3_storage_id), ]; } return $out; } /** * @return array */ private function exportGithubApp(GithubApp $app): array { return [ 'uuid' => $app->uuid, 'name' => $app->name, 'organization' => $app->organization, 'api_url' => $app->api_url, 'html_url' => $app->html_url, 'custom_user' => $app->custom_user, 'custom_port' => $app->custom_port, 'app_id' => $app->app_id, 'installation_id' => $app->installation_id, 'client_id' => $app->client_id, 'client_secret' => $app->client_secret, 'webhook_secret' => $app->webhook_secret, 'is_system_wide' => false, 'is_public' => (bool) $app->is_public, 'contents' => $app->contents, 'metadata' => $app->metadata, 'pull_requests' => $app->pull_requests, 'administration' => $app->administration, 'private_key_uuid' => $app->privateKey?->uuid, 'webhook_url_hint' => '/webhooks/source/github/events', ]; } /** * @return array */ private function exportGitlabApp(GitlabApp $app): array { return [ 'uuid' => $app->uuid, 'name' => $app->name, 'organization' => $app->organization, 'api_url' => $app->api_url, 'html_url' => $app->html_url, 'custom_port' => $app->custom_port, 'custom_user' => $app->custom_user, 'is_system_wide' => false, 'is_public' => (bool) $app->is_public, 'app_id' => $app->app_id, 'app_secret' => $app->app_secret, 'oauth_id' => $app->oauth_id, 'client_id' => $app->client_id, 'client_secret' => $app->client_secret, 'access_token' => $app->access_token, 'refresh_token' => $app->refresh_token, 'expires_at' => $app->expires_at, 'redirect_uri' => $app->redirect_uri, 'group_name' => $app->group_name, 'public_key' => $app->public_key, 'webhook_token' => $app->webhook_token, 'deploy_key_id' => $app->deploy_key_id, 'private_key_uuid' => $app->privateKey?->uuid, 'webhook_url_hint' => '/webhooks/source/gitlab/events', ]; } /** * @return array */ private function exportS3Storage(S3Storage $storage): array { return [ 'uuid' => $storage->uuid, 'name' => $storage->name, 'description' => $storage->description, 'region' => $storage->region, 'key' => $storage->key, 'secret' => $storage->secret, 'bucket' => $storage->bucket, 'endpoint' => $storage->endpoint, 'is_usable' => (bool) $storage->is_usable, ]; } /** * @return array */ private function exportCloudProviderToken(CloudProviderToken $token): array { return [ 'uuid' => $token->uuid, 'provider' => $token->provider, 'token' => $token->token, 'name' => $token->name, 'description' => $token->description, ]; } /** * @return list> */ private function exportSslCertificates(Server $server): array { $out = []; foreach ($server->sslCertificates as $cert) { $resourceUuid = null; $resourceKind = null; if ($cert->resource_type && $cert->resource_id) { $resource = $cert->resource_type::find($cert->resource_id); if ($resource && isset($resource->uuid)) { $resourceUuid = $resource->uuid; $resourceKind = match (true) { $resource instanceof Application => 'application', $resource instanceof Service => 'service', default => class_basename($resource), }; } } $out[] = [ 'ssl_certificate' => $cert->ssl_certificate, 'ssl_private_key' => $cert->ssl_private_key, 'configuration_dir' => $cert->configuration_dir, 'mount_path' => $cert->mount_path, 'common_name' => $cert->common_name, 'subject_alternative_names' => $cert->subject_alternative_names, 'valid_until' => optional($cert->valid_until)?->toIso8601String(), 'is_ca_certificate' => (bool) $cert->is_ca_certificate, 'resource_kind' => $resourceKind, 'resource_uuid' => $resourceUuid, ]; } return $out; } /** * @param Collection|iterable $previews * @return list> */ private function exportPreviews(iterable $previews): array { $out = []; foreach ($previews as $preview) { $out[] = [ 'uuid' => $preview->uuid, 'pull_request_id' => $preview->pull_request_id, 'pull_request_html_url' => $preview->pull_request_html_url, 'pull_request_issue_comment_id' => $preview->pull_request_issue_comment_id, 'fqdn' => $preview->fqdn, 'status' => $preview->status, 'git_type' => $preview->git_type, 'docker_compose_domains' => $preview->docker_compose_domains, 'docker_registry_image_tag' => $preview->docker_registry_image_tag, 'persistent_storages' => method_exists($preview, 'persistentStorages') ? $this->exportPersistentStorages($preview->persistentStorages) : [], ]; } return $out; } /** * @param Collection $applications * @param Collection $databases * @param Collection $services * @return list> */ private function exportVolumeBackups(Collection $applications, Collection $databases, Collection $services): array { $out = []; $seen = []; $collectFrom = function ($resource) use (&$out, &$seen) { $storages = collect(); if (method_exists($resource, 'persistentStorages')) { $storages = $storages->merge($resource->persistentStorages); } if (method_exists($resource, 'fileStorages')) { $storages = $storages->merge($resource->fileStorages); } foreach ($storages as $storage) { if (! method_exists($storage, 'scheduledBackups')) { continue; } foreach ($storage->scheduledBackups as $backup) { if (isset($seen[$backup->id])) { continue; } $seen[$backup->id] = true; $kind = $storage instanceof LocalFileVolume ? 'file_volume' : 'persistent_volume'; $out[] = [ 'uuid' => $backup->uuid, 'backupable_kind' => $kind, 'backupable_uuid' => $storage->uuid, 'frequency' => $backup->frequency, 'enabled' => (bool) $backup->enabled, 'save_s3' => (bool) $backup->save_s3, 'disable_local_backup' => (bool) $backup->disable_local_backup, 'stop_during_backup' => (bool) ($backup->stop_during_backup ?? false), 'retention_amount_locally' => $backup->retention_amount_locally, 'retention_days_locally' => $backup->retention_days_locally, 'retention_max_storage_locally' => $backup->retention_max_storage_locally, 'retention_amount_s3' => $backup->retention_amount_s3, 'retention_days_s3' => $backup->retention_days_s3, 'retention_max_storage_s3' => $backup->retention_max_storage_s3, 'timeout' => $backup->timeout, 's3_storage_uuid' => $backup->s3?->uuid, 'had_s3_storage' => filled($backup->s3_storage_id), ]; } } }; foreach ($applications as $application) { $application->loadMissing([ 'persistentStorages.scheduledBackups.s3', 'fileStorages.scheduledBackups.s3', 'previews.persistentStorages.scheduledBackups.s3', ]); $collectFrom($application); foreach ($application->previews as $preview) { $collectFrom($preview); } } foreach ($databases as $database) { if (method_exists($database, 'loadMissing')) { $database->loadMissing([ 'persistentStorages.scheduledBackups.s3', 'fileStorages.scheduledBackups.s3', ]); } $collectFrom($database); } foreach ($services as $service) { $service->loadMissing([ 'applications.persistentStorages.scheduledBackups.s3', 'applications.fileStorages.scheduledBackups.s3', 'databases.persistentStorages.scheduledBackups.s3', 'databases.fileStorages.scheduledBackups.s3', ]); foreach ($service->applications as $serviceApp) { $collectFrom($serviceApp); } foreach ($service->databases as $serviceDb) { $collectFrom($serviceDb); } } return $out; } /** * @param Collection $applications * @param Collection $databases * @param Collection $services * @return list */ private function collectProjectIds(Collection $applications, Collection $databases, Collection $services): array { $environmentIds = $applications->pluck('environment_id') ->merge($databases->pluck('environment_id')) ->merge($services->pluck('environment_id')) ->filter() ->unique() ->values(); if ($environmentIds->isEmpty()) { return []; } return Environment::query() ->whereIn('id', $environmentIds) ->pluck('project_id') ->unique() ->values() ->all(); } /** * @return array */ private function destinationUuidMap(Server $server): array { $map = []; foreach ($server->standaloneDockers as $destination) { $map[StandaloneDocker::class.':'.$destination->id] = $destination->uuid; } foreach ($server->swarmDockers as $destination) { $map[SwarmDocker::class.':'.$destination->id] = $destination->uuid; } return $map; } }