mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 04:23:34 +00:00
1605 lines
69 KiB
PHP
1605 lines
69 KiB
PHP
<?php
|
|
|
|
namespace App\Services\ServerTransfer;
|
|
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationPreview;
|
|
use App\Models\CloudProviderToken;
|
|
use App\Models\Environment;
|
|
use App\Models\EnvironmentVariable;
|
|
use App\Models\GithubApp;
|
|
use App\Models\GitlabApp;
|
|
use App\Models\LocalFileVolume;
|
|
use App\Models\LocalPersistentVolume;
|
|
use App\Models\PrivateKey;
|
|
use App\Models\Project;
|
|
use App\Models\S3Storage;
|
|
use App\Models\ScheduledDatabaseBackup;
|
|
use App\Models\ScheduledTask;
|
|
use App\Models\ScheduledVolumeBackup;
|
|
use App\Models\Server;
|
|
use App\Models\Service;
|
|
use App\Models\ServiceApplication;
|
|
use App\Models\ServiceDatabase;
|
|
use App\Models\SharedEnvironmentVariable;
|
|
use App\Models\SslCertificate;
|
|
use App\Models\StandaloneClickhouse;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\StandaloneDragonfly;
|
|
use App\Models\StandaloneKeydb;
|
|
use App\Models\StandaloneMariadb;
|
|
use App\Models\StandaloneMongodb;
|
|
use App\Models\StandaloneMysql;
|
|
use App\Models\StandalonePostgresql;
|
|
use App\Models\StandaloneRedis;
|
|
use App\Models\SwarmDocker;
|
|
use App\Models\Tag;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Validation\ValidationException;
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
class ServerTransferImporter
|
|
{
|
|
/**
|
|
* @var array<string, class-string>
|
|
*/
|
|
private const DATABASE_MODELS = [
|
|
'StandalonePostgresql' => StandalonePostgresql::class,
|
|
'StandaloneMysql' => StandaloneMysql::class,
|
|
'StandaloneMariadb' => StandaloneMariadb::class,
|
|
'StandaloneMongodb' => StandaloneMongodb::class,
|
|
'StandaloneRedis' => StandaloneRedis::class,
|
|
'StandaloneKeydb' => StandaloneKeydb::class,
|
|
'StandaloneDragonfly' => StandaloneDragonfly::class,
|
|
'StandaloneClickhouse' => StandaloneClickhouse::class,
|
|
];
|
|
|
|
/** @var array<string, PrivateKey> */
|
|
private array $privateKeyMap = [];
|
|
|
|
/** @var array<string, GithubApp> */
|
|
private array $githubAppMap = [];
|
|
|
|
/** @var array<string, GitlabApp> */
|
|
private array $gitlabAppMap = [];
|
|
|
|
/** @var array<string, S3Storage> */
|
|
private array $s3StorageMap = [];
|
|
|
|
/** @var array<string, CloudProviderToken> */
|
|
private array $cloudTokenMap = [];
|
|
|
|
/** @var array<string, LocalPersistentVolume|LocalFileVolume> */
|
|
private array $volumeMap = [];
|
|
|
|
/** @var array<string, Application> */
|
|
private array $applicationMap = [];
|
|
|
|
/** @var array<string, Service> */
|
|
private array $serviceMap = [];
|
|
|
|
/** @var array<string, Model> */
|
|
private array $databaseMap = [];
|
|
|
|
/**
|
|
* @param array<string, mixed> $bundle
|
|
* @return array{
|
|
* dry_run: bool,
|
|
* warnings: list<string>,
|
|
* server_uuid: string|null,
|
|
* private_key_uuid: string|null,
|
|
* created: array<string, int>,
|
|
* preserved_uuids: bool,
|
|
* export_id: string|null,
|
|
* claimed: bool,
|
|
* claim: array<string, mixed>|null
|
|
* }
|
|
*/
|
|
public function import(
|
|
array $bundle,
|
|
int $teamId,
|
|
bool $dryRun = false,
|
|
bool $preserveUuids = true,
|
|
bool $adoptMode = true,
|
|
bool $claim = true,
|
|
bool $writeRemote = false,
|
|
bool $rebindSentinel = true,
|
|
): array {
|
|
ServerTransferBundle::assertValid($bundle);
|
|
|
|
$validation = ServerTransferBundle::validate($bundle);
|
|
$warnings = $validation['warnings'];
|
|
if (is_array(data_get($bundle, 'warnings'))) {
|
|
$warnings = array_values(array_unique(array_merge($warnings, $bundle['warnings'])));
|
|
}
|
|
|
|
$serverUuid = (string) data_get($bundle, 'server.uuid');
|
|
$serverIp = (string) data_get($bundle, 'server.ip');
|
|
|
|
$uuidConflict = Server::withTrashed()->where('uuid', $serverUuid)->exists() && $preserveUuids;
|
|
$existingIp = Server::where('ip', $serverIp)->first();
|
|
$ipConflict = $existingIp !== null;
|
|
|
|
if (! $dryRun && $uuidConflict) {
|
|
throw ValidationException::withMessages([
|
|
'server.uuid' => ["A server with UUID {$serverUuid} already exists on this instance. Delete/transfer it first, or import with preserve_uuids=false."],
|
|
]);
|
|
}
|
|
|
|
if (! $dryRun && $ipConflict) {
|
|
throw ValidationException::withMessages([
|
|
'server.ip' => ["A server with IP/domain {$serverIp} already exists (uuid={$existingIp->uuid}). Complete handoff on the source instance first."],
|
|
]);
|
|
}
|
|
|
|
if ($uuidConflict) {
|
|
$warnings[] = "A server with UUID {$serverUuid} already exists on this instance.";
|
|
}
|
|
if ($ipConflict) {
|
|
$warnings[] = "A server with IP/domain {$serverIp} already exists (uuid={$existingIp->uuid}).";
|
|
}
|
|
|
|
$created = [
|
|
'projects' => 0,
|
|
'environments' => 0,
|
|
'applications' => 0,
|
|
'databases' => 0,
|
|
'services' => 0,
|
|
'destinations' => 0,
|
|
'private_keys' => 0,
|
|
'github_apps' => 0,
|
|
'gitlab_apps' => 0,
|
|
's3_storages' => 0,
|
|
'cloud_provider_tokens' => 0,
|
|
'ssl_certificates' => 0,
|
|
'volume_backups' => 0,
|
|
'previews' => 0,
|
|
];
|
|
|
|
if ($dryRun) {
|
|
foreach (data_get($bundle, 'projects', []) as $project) {
|
|
$created['projects']++;
|
|
foreach (data_get($project, 'environments', []) as $environment) {
|
|
$created['environments']++;
|
|
$created['applications'] += count(data_get($environment, 'applications', []));
|
|
$created['databases'] += count(data_get($environment, 'databases', []));
|
|
$created['services'] += count(data_get($environment, 'services', []));
|
|
foreach (data_get($environment, 'applications', []) as $app) {
|
|
$created['previews'] += count(data_get($app, 'previews', []));
|
|
}
|
|
}
|
|
}
|
|
$created['destinations'] = count(data_get($bundle, 'destinations', []));
|
|
$created['private_keys'] = max(
|
|
count(data_get($bundle, 'private_keys', [])),
|
|
data_get($bundle, 'private_key') ? 1 : 0
|
|
);
|
|
$created['github_apps'] = count(data_get($bundle, 'github_apps', []));
|
|
$created['gitlab_apps'] = count(data_get($bundle, 'gitlab_apps', []));
|
|
$created['s3_storages'] = count(data_get($bundle, 's3_storages', []));
|
|
$created['cloud_provider_tokens'] = count(data_get($bundle, 'cloud_provider_tokens', []));
|
|
$created['ssl_certificates'] = count(data_get($bundle, 'ssl_certificates', []));
|
|
$created['volume_backups'] = count(data_get($bundle, 'volume_backups', []));
|
|
|
|
// Append transfer hints for the target instance FQDN.
|
|
$targetUrl = rtrim((string) (instanceSettings()->fqdn ?: config('app.url')), '/');
|
|
if (count(data_get($bundle, 'github_apps', [])) > 0) {
|
|
$warnings[] = "After import, set GitHub App webhook URL to {$targetUrl}/webhooks/source/github/events (and update setup/callback URLs in GitHub if needed) so automations work on this instance.";
|
|
}
|
|
if (count(data_get($bundle, 'gitlab_apps', [])) > 0) {
|
|
$warnings[] = "After import, set GitLab webhook URL to {$targetUrl}/webhooks/source/gitlab/events so automations work on this instance.";
|
|
}
|
|
|
|
return [
|
|
'dry_run' => true,
|
|
'warnings' => $warnings,
|
|
'server_uuid' => $preserveUuids ? $serverUuid : null,
|
|
'private_key_uuid' => data_get($bundle, 'private_key.uuid'),
|
|
'created' => $created,
|
|
'preserved_uuids' => $preserveUuids,
|
|
'export_id' => data_get($bundle, 'export_id'),
|
|
'claimed' => false,
|
|
'claim' => null,
|
|
];
|
|
}
|
|
|
|
$result = DB::transaction(function () use ($bundle, $teamId, $preserveUuids, $adoptMode, $warnings, &$created) {
|
|
$this->privateKeyMap = [];
|
|
$this->githubAppMap = [];
|
|
$this->gitlabAppMap = [];
|
|
$this->s3StorageMap = [];
|
|
$this->cloudTokenMap = [];
|
|
$this->volumeMap = [];
|
|
$this->applicationMap = [];
|
|
$this->serviceMap = [];
|
|
$this->databaseMap = [];
|
|
|
|
// Import shared dependencies first
|
|
$keyPayloads = data_get($bundle, 'private_keys', []);
|
|
if ($keyPayloads === [] && data_get($bundle, 'private_key')) {
|
|
$keyPayloads = [data_get($bundle, 'private_key')];
|
|
}
|
|
foreach ($keyPayloads as $keyPayload) {
|
|
$key = $this->importPrivateKey($keyPayload, $teamId, $preserveUuids);
|
|
$this->privateKeyMap[(string) data_get($keyPayload, 'uuid', $key->uuid)] = $key;
|
|
$this->privateKeyMap[$key->uuid] = $key;
|
|
$created['private_keys']++;
|
|
}
|
|
|
|
foreach (data_get($bundle, 'github_apps', []) as $ghPayload) {
|
|
// Defensive: never import system-wide GitHub Apps from a bundle.
|
|
if ((bool) data_get($ghPayload, 'is_system_wide', false)) {
|
|
continue;
|
|
}
|
|
$gh = $this->importGithubApp($ghPayload, $teamId, $preserveUuids);
|
|
$this->githubAppMap[(string) data_get($ghPayload, 'uuid', $gh->uuid)] = $gh;
|
|
$this->githubAppMap[$gh->uuid] = $gh;
|
|
$created['github_apps']++;
|
|
}
|
|
|
|
foreach (data_get($bundle, 'gitlab_apps', []) as $glPayload) {
|
|
// Defensive: never import system-wide GitLab Apps from a bundle.
|
|
if ((bool) data_get($glPayload, 'is_system_wide', false)) {
|
|
continue;
|
|
}
|
|
$gl = $this->importGitlabApp($glPayload, $teamId, $preserveUuids);
|
|
$this->gitlabAppMap[(string) data_get($glPayload, 'uuid', $gl->uuid)] = $gl;
|
|
$this->gitlabAppMap[$gl->uuid] = $gl;
|
|
$created['gitlab_apps']++;
|
|
}
|
|
|
|
foreach (data_get($bundle, 's3_storages', []) as $s3Payload) {
|
|
$s3 = $this->importS3Storage($s3Payload, $teamId, $preserveUuids);
|
|
$this->s3StorageMap[(string) data_get($s3Payload, 'uuid', $s3->uuid)] = $s3;
|
|
$this->s3StorageMap[$s3->uuid] = $s3;
|
|
$created['s3_storages']++;
|
|
}
|
|
|
|
foreach (data_get($bundle, 'cloud_provider_tokens', []) as $tokenPayload) {
|
|
$token = $this->importCloudProviderToken($tokenPayload, $teamId, $preserveUuids);
|
|
$this->cloudTokenMap[(string) data_get($tokenPayload, 'uuid', $token->uuid)] = $token;
|
|
$this->cloudTokenMap[$token->uuid] = $token;
|
|
$created['cloud_provider_tokens']++;
|
|
}
|
|
|
|
$serverKeyUuid = (string) data_get($bundle, 'private_key.uuid', '');
|
|
$privateKey = $this->privateKeyMap[$serverKeyUuid]
|
|
?? $this->importPrivateKey(data_get($bundle, 'private_key', []), $teamId, $preserveUuids);
|
|
|
|
$server = $this->importServer(data_get($bundle, 'server', []), $privateKey, $teamId, $preserveUuids, $adoptMode);
|
|
$destinationMap = $this->importDestinations(data_get($bundle, 'destinations', []), $server, $preserveUuids);
|
|
$created['destinations'] = count($destinationMap);
|
|
|
|
$this->importServerSharedEnvVars(data_get($bundle, 'shared_environment_variables.server', []), $server, $teamId);
|
|
|
|
foreach (data_get($bundle, 'projects', []) as $projectPayload) {
|
|
$project = $this->importProject($projectPayload, $teamId, $preserveUuids, $created);
|
|
foreach (data_get($projectPayload, 'environments', []) as $environmentPayload) {
|
|
$environment = $this->importEnvironment($environmentPayload, $project, $preserveUuids, $created);
|
|
|
|
foreach (data_get($environmentPayload, 'applications', []) as $appPayload) {
|
|
$app = $this->importApplication($appPayload, $environment, $destinationMap, $preserveUuids, $adoptMode, $teamId);
|
|
$this->applicationMap[(string) data_get($appPayload, 'uuid', $app->uuid)] = $app;
|
|
$created['applications']++;
|
|
$created['previews'] += count(data_get($appPayload, 'previews', []));
|
|
}
|
|
|
|
foreach (data_get($environmentPayload, 'databases', []) as $dbPayload) {
|
|
$db = $this->importDatabase($dbPayload, $environment, $destinationMap, $preserveUuids, $adoptMode, $teamId);
|
|
if ($db) {
|
|
$this->databaseMap[(string) data_get($dbPayload, 'uuid', $db->uuid)] = $db;
|
|
}
|
|
$created['databases']++;
|
|
}
|
|
|
|
foreach (data_get($environmentPayload, 'services', []) as $servicePayload) {
|
|
$service = $this->importService($servicePayload, $environment, $server, $destinationMap, $preserveUuids, $adoptMode, $teamId);
|
|
$this->serviceMap[(string) data_get($servicePayload, 'uuid', $service->uuid)] = $service;
|
|
$created['services']++;
|
|
}
|
|
}
|
|
}
|
|
|
|
$created['ssl_certificates'] = $this->importSslCertificates(data_get($bundle, 'ssl_certificates', []), $server);
|
|
$created['volume_backups'] = $this->importVolumeBackups(data_get($bundle, 'volume_backups', []), $teamId);
|
|
|
|
$targetUrl = rtrim((string) (instanceSettings()->fqdn ?: config('app.url')), '/');
|
|
if ($created['github_apps'] > 0) {
|
|
$warnings[] = "GitHub Apps imported. Update webhook URL to {$targetUrl}/webhooks/source/github/events (and setup/callback URLs in GitHub) so automations work on this instance.";
|
|
}
|
|
if ($created['gitlab_apps'] > 0) {
|
|
$warnings[] = "GitLab sources imported. Update webhook URL to {$targetUrl}/webhooks/source/gitlab/events so automations work on this instance.";
|
|
}
|
|
|
|
$metadata = $server->server_metadata ?? [];
|
|
$metadata['transfer'] = [
|
|
'status' => 'imported',
|
|
'export_id' => data_get($bundle, 'export_id'),
|
|
'source_instance_url' => data_get($bundle, 'source_instance.url'),
|
|
'imported_at' => now()->toIso8601String(),
|
|
'adopt_mode' => $adoptMode,
|
|
];
|
|
$server->server_metadata = $metadata;
|
|
$server->save();
|
|
|
|
return [
|
|
'dry_run' => false,
|
|
'warnings' => array_values(array_unique($warnings)),
|
|
'server_uuid' => $server->uuid,
|
|
'private_key_uuid' => $privateKey->uuid,
|
|
'created' => $created,
|
|
'preserved_uuids' => $preserveUuids,
|
|
'export_id' => data_get($bundle, 'export_id'),
|
|
'claimed' => false,
|
|
'claim' => null,
|
|
];
|
|
});
|
|
|
|
// Claim after the import transaction commits so host/SSH work cannot roll back DB rows.
|
|
if ($claim && filled(data_get($result, 'server_uuid'))) {
|
|
$server = Server::where('uuid', $result['server_uuid'])->where('team_id', $teamId)->first();
|
|
if ($server) {
|
|
try {
|
|
$claimResult = app(ServerTransferClaimer::class)->claim(
|
|
$server,
|
|
writeRemote: $writeRemote,
|
|
rebindSentinel: $rebindSentinel,
|
|
);
|
|
$result['claimed'] = true;
|
|
$result['claim'] = $claimResult;
|
|
if (! data_get($claimResult, 'claim_written') && $writeRemote) {
|
|
$result['warnings'][] = 'Server imported and claimed in Coolify, but the remote ownership file was not written (SSH unavailable).';
|
|
}
|
|
} catch (Throwable $e) {
|
|
$result['claimed'] = false;
|
|
$result['claim'] = null;
|
|
$result['warnings'][] = 'Server imported, but automatic claim failed: '.$e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function importPrivateKey(array $payload, int $teamId, bool $preserveUuids): PrivateKey
|
|
{
|
|
$material = (string) data_get($payload, 'private_key');
|
|
if ($material === '') {
|
|
throw new RuntimeException('Private key material is required.');
|
|
}
|
|
|
|
$fingerprint = PrivateKey::generateFingerprint($material)
|
|
?? data_get($payload, 'fingerprint');
|
|
|
|
if ($fingerprint) {
|
|
$existing = PrivateKey::query()->where('fingerprint', $fingerprint)->first();
|
|
if ($existing) {
|
|
if ((int) $existing->team_id !== $teamId) {
|
|
throw ValidationException::withMessages([
|
|
'private_key' => ['This SSH private key already exists on another team on this instance.'],
|
|
]);
|
|
}
|
|
|
|
return $existing;
|
|
}
|
|
}
|
|
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
if (PrivateKey::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$key = new PrivateKey([
|
|
'name' => data_get($payload, 'name') ?: 'Imported transfer key',
|
|
'description' => data_get($payload, 'description'),
|
|
'private_key' => $material,
|
|
'team_id' => $teamId,
|
|
'is_git_related' => (bool) data_get($payload, 'is_git_related', false),
|
|
]);
|
|
$key->uuid = $uuid;
|
|
|
|
try {
|
|
$key->save();
|
|
} catch (Throwable $e) {
|
|
// Retry once without forcing uuid if uniqueness/validation conflicts.
|
|
$key->uuid = new_public_id();
|
|
$key->save();
|
|
}
|
|
|
|
return $key->fresh();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function importServer(array $payload, PrivateKey $privateKey, int $teamId, bool $preserveUuids, bool $adoptMode): Server
|
|
{
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
if (Server::withTrashed()->where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$cloudTokenId = null;
|
|
$cloudTokenUuid = data_get($payload, 'cloud_provider_token_uuid');
|
|
if ($cloudTokenUuid && isset($this->cloudTokenMap[$cloudTokenUuid])) {
|
|
$cloudTokenId = $this->cloudTokenMap[$cloudTokenUuid]->id;
|
|
}
|
|
|
|
$server = new Server;
|
|
$server->forceFill([
|
|
'name' => data_get($payload, 'name') ?: generate_random_name(),
|
|
'description' => data_get($payload, 'description'),
|
|
'ip' => data_get($payload, 'ip'),
|
|
'port' => (int) data_get($payload, 'port', 22),
|
|
'user' => data_get($payload, 'user') ?: 'root',
|
|
'private_key_id' => $privateKey->id,
|
|
'team_id' => $teamId,
|
|
'cloud_provider_token_id' => $cloudTokenId,
|
|
]);
|
|
$server->uuid = $uuid;
|
|
$server->save();
|
|
|
|
if ($server->settings && data_get($payload, 'is_build_server')) {
|
|
$server->settings->is_build_server = true;
|
|
$server->settings->save();
|
|
}
|
|
|
|
$proxy = data_get($payload, 'proxy', []);
|
|
if (is_array($proxy) && $proxy !== []) {
|
|
foreach ($proxy as $key => $value) {
|
|
$server->proxy->set($key, $value);
|
|
}
|
|
$server->save();
|
|
}
|
|
|
|
$settingsPayload = data_get($payload, 'settings', []);
|
|
if (is_array($settingsPayload) && $server->settings) {
|
|
$safe = collect($settingsPayload)
|
|
->except([
|
|
'id', 'server_id', 'created_at', 'updated_at',
|
|
'is_reachable', 'is_usable', 'force_disabled',
|
|
'sentinel_token', 'sentinel_custom_url',
|
|
])
|
|
->all();
|
|
$server->settings->fill($safe);
|
|
// Imported servers start not validated; claim will rebind sentinel.
|
|
$server->settings->is_reachable = false;
|
|
$server->settings->is_usable = false;
|
|
$server->settings->force_disabled = false;
|
|
$server->settings->is_sentinel_enabled = false;
|
|
$server->settings->save();
|
|
}
|
|
|
|
if ($adoptMode) {
|
|
// Do not trigger validation/install automatically; operator claims next.
|
|
$server->settings->is_reachable = false;
|
|
$server->settings->is_usable = false;
|
|
$server->settings->save();
|
|
}
|
|
|
|
return $server->fresh(['settings', 'standaloneDockers', 'swarmDockers']);
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $destinations
|
|
* @return array<string, StandaloneDocker|SwarmDocker> keyed by original uuid
|
|
*/
|
|
private function importDestinations(array $destinations, Server $server, bool $preserveUuids): array
|
|
{
|
|
$map = [];
|
|
$existingStandalone = $server->standaloneDockers->values();
|
|
$existingSwarm = $server->swarmDockers->values();
|
|
$standaloneIndex = 0;
|
|
$swarmIndex = 0;
|
|
|
|
if ($destinations === []) {
|
|
$first = $existingStandalone->first() ?? $existingSwarm->first();
|
|
if ($first) {
|
|
$map[$first->uuid] = $first;
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
foreach ($destinations as $destination) {
|
|
$type = data_get($destination, 'type', 'standalone');
|
|
$originalUuid = (string) data_get($destination, 'uuid');
|
|
$uuid = $preserveUuids && $originalUuid !== '' ? $originalUuid : new_public_id();
|
|
|
|
if ($type === 'swarm') {
|
|
$model = $existingSwarm->get($swarmIndex);
|
|
$swarmIndex++;
|
|
if ($model) {
|
|
$model->forceFill([
|
|
'uuid' => SwarmDocker::where('uuid', $uuid)->where('id', '!=', $model->id)->exists() ? $model->uuid : $uuid,
|
|
'name' => data_get($destination, 'name') ?: $model->name,
|
|
'network' => data_get($destination, 'network') ?: $model->network,
|
|
])->saveQuietly();
|
|
} else {
|
|
$model = new SwarmDocker;
|
|
$model->forceFill([
|
|
'uuid' => SwarmDocker::where('uuid', $uuid)->exists() ? new_public_id() : $uuid,
|
|
'name' => data_get($destination, 'name') ?: 'coolify-overlay',
|
|
'network' => data_get($destination, 'network') ?: 'coolify-overlay',
|
|
'server_id' => $server->id,
|
|
])->saveQuietly();
|
|
}
|
|
} else {
|
|
$model = $existingStandalone->get($standaloneIndex);
|
|
$standaloneIndex++;
|
|
if ($model) {
|
|
$targetUuid = StandaloneDocker::where('uuid', $uuid)->where('id', '!=', $model->id)->exists()
|
|
? $model->uuid
|
|
: $uuid;
|
|
$model->forceFill([
|
|
'uuid' => $targetUuid,
|
|
'name' => data_get($destination, 'name') ?: $model->name,
|
|
'network' => data_get($destination, 'network') ?: $model->network,
|
|
])->saveQuietly();
|
|
} else {
|
|
$model = new StandaloneDocker;
|
|
$model->forceFill([
|
|
'uuid' => StandaloneDocker::where('uuid', $uuid)->exists() ? new_public_id() : $uuid,
|
|
'name' => data_get($destination, 'name') ?: 'coolify',
|
|
'network' => data_get($destination, 'network') ?: 'coolify',
|
|
'server_id' => $server->id,
|
|
])->saveQuietly();
|
|
}
|
|
}
|
|
|
|
$map[$originalUuid !== '' ? $originalUuid : $model->uuid] = $model->fresh();
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $variables
|
|
*/
|
|
private function importServerSharedEnvVars(array $variables, Server $server, int $teamId): void
|
|
{
|
|
foreach ($variables as $variable) {
|
|
SharedEnvironmentVariable::create([
|
|
'key' => data_get($variable, 'key'),
|
|
'value' => data_get($variable, 'value'),
|
|
'is_multiline' => (bool) data_get($variable, 'is_multiline', false),
|
|
'is_literal' => (bool) data_get($variable, 'is_literal', false),
|
|
'is_shown_once' => (bool) data_get($variable, 'is_shown_once', false),
|
|
'comment' => data_get($variable, 'comment'),
|
|
'type' => 'server',
|
|
'server_id' => $server->id,
|
|
'team_id' => $teamId,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @param array<string, int> $created
|
|
*/
|
|
private function importProject(array $payload, int $teamId, bool $preserveUuids, array &$created): Project
|
|
{
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
$existing = Project::where('uuid', $uuid)->where('team_id', $teamId)->first();
|
|
if ($existing) {
|
|
$this->importProjectSharedEnvVars(data_get($payload, 'shared_environment_variables', []), $existing, $teamId);
|
|
|
|
return $existing;
|
|
}
|
|
|
|
if (Project::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$project = Project::create([
|
|
'uuid' => $uuid,
|
|
'name' => data_get($payload, 'name') ?: generate_random_name(),
|
|
'description' => data_get($payload, 'description'),
|
|
'team_id' => $teamId,
|
|
]);
|
|
$created['projects']++;
|
|
|
|
$this->importProjectSharedEnvVars(data_get($payload, 'shared_environment_variables', []), $project, $teamId);
|
|
|
|
return $project->fresh(['environments']);
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $variables
|
|
*/
|
|
private function importProjectSharedEnvVars(array $variables, Project $project, int $teamId): void
|
|
{
|
|
foreach ($variables as $variable) {
|
|
$exists = SharedEnvironmentVariable::query()
|
|
->where('type', 'project')
|
|
->where('project_id', $project->id)
|
|
->where('key', data_get($variable, 'key'))
|
|
->exists();
|
|
if ($exists) {
|
|
continue;
|
|
}
|
|
SharedEnvironmentVariable::create([
|
|
'key' => data_get($variable, 'key'),
|
|
'value' => data_get($variable, 'value'),
|
|
'is_multiline' => (bool) data_get($variable, 'is_multiline', false),
|
|
'is_literal' => (bool) data_get($variable, 'is_literal', false),
|
|
'is_shown_once' => (bool) data_get($variable, 'is_shown_once', false),
|
|
'comment' => data_get($variable, 'comment'),
|
|
'type' => 'project',
|
|
'project_id' => $project->id,
|
|
'team_id' => $teamId,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @param array<string, int> $created
|
|
*/
|
|
private function importEnvironment(array $payload, Project $project, bool $preserveUuids, array &$created): Environment
|
|
{
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
$byUuid = Environment::where('uuid', $uuid)->where('project_id', $project->id)->first();
|
|
if ($byUuid) {
|
|
$this->importEnvironmentSharedEnvVars(data_get($payload, 'shared_environment_variables', []), $byUuid, $project->team_id);
|
|
|
|
return $byUuid;
|
|
}
|
|
|
|
$name = (string) data_get($payload, 'name', 'production');
|
|
$byName = $project->environments()->where('name', $name)->first();
|
|
if ($byName) {
|
|
if ($preserveUuids && $uuid !== '' && ! Environment::where('uuid', $uuid)->exists()) {
|
|
$byName->uuid = $uuid;
|
|
$byName->save();
|
|
}
|
|
if (filled(data_get($payload, 'description'))) {
|
|
$byName->description = data_get($payload, 'description');
|
|
$byName->save();
|
|
}
|
|
$this->importEnvironmentSharedEnvVars(data_get($payload, 'shared_environment_variables', []), $byName, $project->team_id);
|
|
|
|
return $byName->fresh();
|
|
}
|
|
|
|
if (Environment::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$environment = Environment::create([
|
|
'uuid' => $uuid,
|
|
'name' => $name,
|
|
'description' => data_get($payload, 'description'),
|
|
'project_id' => $project->id,
|
|
]);
|
|
$created['environments']++;
|
|
|
|
$this->importEnvironmentSharedEnvVars(data_get($payload, 'shared_environment_variables', []), $environment, $project->team_id);
|
|
|
|
return $environment;
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $variables
|
|
*/
|
|
private function importEnvironmentSharedEnvVars(array $variables, Environment $environment, int $teamId): void
|
|
{
|
|
foreach ($variables as $variable) {
|
|
$exists = SharedEnvironmentVariable::query()
|
|
->where('type', 'environment')
|
|
->where('environment_id', $environment->id)
|
|
->where('key', data_get($variable, 'key'))
|
|
->exists();
|
|
if ($exists) {
|
|
continue;
|
|
}
|
|
SharedEnvironmentVariable::create([
|
|
'key' => data_get($variable, 'key'),
|
|
'value' => data_get($variable, 'value'),
|
|
'is_multiline' => (bool) data_get($variable, 'is_multiline', false),
|
|
'is_literal' => (bool) data_get($variable, 'is_literal', false),
|
|
'is_shown_once' => (bool) data_get($variable, 'is_shown_once', false),
|
|
'comment' => data_get($variable, 'comment'),
|
|
'type' => 'environment',
|
|
'environment_id' => $environment->id,
|
|
'team_id' => $teamId,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @param array<string, StandaloneDocker|SwarmDocker> $destinationMap
|
|
*/
|
|
private function importApplication(
|
|
array $payload,
|
|
Environment $environment,
|
|
array $destinationMap,
|
|
bool $preserveUuids,
|
|
bool $adoptMode,
|
|
int $teamId,
|
|
): Application {
|
|
$destination = $this->resolveDestination(data_get($payload, 'destination_uuid'), $destinationMap);
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
if (Application::withTrashed()->where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$attributes = (array) data_get($payload, 'attributes', []);
|
|
unset($attributes['id'], $attributes['environment_id'], $attributes['destination_id'], $attributes['destination_type'], $attributes['source_id'], $attributes['source_type'], $attributes['private_key_id']);
|
|
$attributes = $this->onlyExistingColumns(Application::class, $attributes);
|
|
|
|
$attributes['uuid'] = $uuid;
|
|
$attributes['environment_id'] = $environment->id;
|
|
$attributes['destination_id'] = $destination->id;
|
|
$attributes['destination_type'] = $destination->getMorphClass();
|
|
|
|
// Re-link git source
|
|
$source = data_get($payload, 'source');
|
|
if (is_array($source) && filled(data_get($source, 'uuid'))) {
|
|
$sourceUuid = (string) data_get($source, 'uuid');
|
|
$sourceType = (string) data_get($source, 'type');
|
|
if ($sourceType === 'github_app') {
|
|
$gh = $this->githubAppMap[$sourceUuid]
|
|
?? GithubApp::query()->where('uuid', $sourceUuid)->where('is_system_wide', true)->first()
|
|
?? GithubApp::query()->where('uuid', $sourceUuid)->where('team_id', $teamId)->first();
|
|
if ($gh) {
|
|
$attributes['source_type'] = GithubApp::class;
|
|
$attributes['source_id'] = $gh->id;
|
|
}
|
|
} elseif ($sourceType === 'gitlab_app') {
|
|
$gl = $this->gitlabAppMap[$sourceUuid]
|
|
?? GitlabApp::query()->where('uuid', $sourceUuid)->where('is_system_wide', true)->first()
|
|
?? GitlabApp::query()->where('uuid', $sourceUuid)->where('team_id', $teamId)->first();
|
|
if ($gl) {
|
|
$attributes['source_type'] = GitlabApp::class;
|
|
$attributes['source_id'] = $gl->id;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Re-link deploy key
|
|
$appKeyUuid = data_get($payload, 'private_key_uuid');
|
|
if ($appKeyUuid && isset($this->privateKeyMap[$appKeyUuid])) {
|
|
$attributes['private_key_id'] = $this->privateKeyMap[$appKeyUuid]->id;
|
|
}
|
|
|
|
if ($adoptMode) {
|
|
// Keep runtime status if provided so inventory can match, but default exited if empty.
|
|
$attributes['status'] = data_get($attributes, 'status') ?: 'exited';
|
|
} else {
|
|
$attributes['status'] = 'exited';
|
|
}
|
|
|
|
$application = Application::create($attributes);
|
|
|
|
$settings = (array) data_get($payload, 'settings', []);
|
|
if ($settings !== [] && $application->settings) {
|
|
$application->settings->fill(collect($settings)->except(['id', 'application_id'])->all());
|
|
$application->settings->save();
|
|
}
|
|
|
|
$this->importEnvVars(data_get($payload, 'environment_variables', []), $application, false);
|
|
$this->importEnvVars(data_get($payload, 'environment_variables_preview', []), $application, true);
|
|
$this->importPersistentStorages(data_get($payload, 'persistent_storages', []), $application);
|
|
$this->importFileStorages(data_get($payload, 'file_storages', []), $application);
|
|
$this->importScheduledTasks(data_get($payload, 'scheduled_tasks', []), $application, $teamId);
|
|
$this->importTags(data_get($payload, 'tags', []), $application, $teamId);
|
|
$this->importPreviews(data_get($payload, 'previews', []), $application, $preserveUuids, $adoptMode);
|
|
|
|
return $application;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @param array<string, StandaloneDocker|SwarmDocker> $destinationMap
|
|
*/
|
|
private function importDatabase(
|
|
array $payload,
|
|
Environment $environment,
|
|
array $destinationMap,
|
|
bool $preserveUuids,
|
|
bool $adoptMode,
|
|
int $teamId = 0,
|
|
): ?Model {
|
|
$type = (string) data_get($payload, 'type', 'StandalonePostgresql');
|
|
$modelClass = self::DATABASE_MODELS[$type]
|
|
?? (is_string(data_get($payload, 'model')) && class_exists((string) data_get($payload, 'model'))
|
|
? (string) data_get($payload, 'model')
|
|
: null);
|
|
|
|
if (! $modelClass || ! class_exists($modelClass)) {
|
|
throw new RuntimeException("Unsupported database type: {$type}");
|
|
}
|
|
|
|
$destination = $this->resolveDestination(data_get($payload, 'destination_uuid'), $destinationMap);
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
if ($modelClass::withTrashed()->where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$attributes = (array) data_get($payload, 'attributes', []);
|
|
unset($attributes['id'], $attributes['environment_id'], $attributes['destination_id'], $attributes['destination_type']);
|
|
$attributes = $this->onlyExistingColumns($modelClass, $attributes);
|
|
$attributes['uuid'] = $uuid;
|
|
$attributes['environment_id'] = $environment->id;
|
|
$attributes['destination_id'] = $destination->id;
|
|
$attributes['destination_type'] = $destination->getMorphClass();
|
|
$attributes['status'] = $adoptMode
|
|
? (data_get($attributes, 'status') ?: 'exited')
|
|
: 'exited';
|
|
|
|
// Avoid auto-created default volumes; we restore exported ones.
|
|
$database = $modelClass::withoutEvents(function () use ($modelClass, $attributes, $uuid) {
|
|
$database = new $modelClass;
|
|
$database->forceFill(collect($attributes)->except(['uuid'])->all());
|
|
$database->uuid = $uuid;
|
|
$database->save();
|
|
|
|
return $database;
|
|
});
|
|
|
|
$this->importEnvVars(data_get($payload, 'environment_variables', []), $database, false);
|
|
$this->importPersistentStorages(data_get($payload, 'persistent_storages', []), $database);
|
|
$this->importFileStorages(data_get($payload, 'file_storages', []), $database);
|
|
if (method_exists($database, 'tags')) {
|
|
$this->importTags(data_get($payload, 'tags', []), $database, $teamId);
|
|
}
|
|
$this->importScheduledBackups(data_get($payload, 'scheduled_backups', []), $database, $teamId);
|
|
|
|
return $database;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @param array<string, StandaloneDocker|SwarmDocker> $destinationMap
|
|
*/
|
|
private function importService(
|
|
array $payload,
|
|
Environment $environment,
|
|
Server $server,
|
|
array $destinationMap,
|
|
bool $preserveUuids,
|
|
bool $adoptMode,
|
|
int $teamId = 0,
|
|
): Service {
|
|
$destination = $this->resolveDestination(data_get($payload, 'destination_uuid'), $destinationMap);
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
if (Service::withTrashed()->where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$attributes = (array) data_get($payload, 'attributes', []);
|
|
unset($attributes['id'], $attributes['environment_id'], $attributes['destination_id'], $attributes['destination_type'], $attributes['server_id']);
|
|
$attributes = $this->onlyExistingColumns(Service::class, $attributes);
|
|
$attributes['uuid'] = $uuid;
|
|
$attributes['environment_id'] = $environment->id;
|
|
$attributes['server_id'] = $server->id;
|
|
$attributes['destination_id'] = $destination->id;
|
|
$attributes['destination_type'] = $destination->getMorphClass();
|
|
|
|
$service = Service::create($attributes);
|
|
|
|
$this->importEnvVars(data_get($payload, 'environment_variables', []), $service, false);
|
|
$this->importScheduledTasks(data_get($payload, 'scheduled_tasks', []), $service, $teamId);
|
|
$this->importTags(data_get($payload, 'tags', []), $service, $teamId);
|
|
|
|
foreach (data_get($payload, 'applications', []) as $appPayload) {
|
|
$this->importServiceApplication($appPayload, $service, $preserveUuids, $adoptMode);
|
|
}
|
|
|
|
foreach (data_get($payload, 'databases', []) as $dbPayload) {
|
|
$this->importServiceDatabase($dbPayload, $service, $preserveUuids, $adoptMode, $teamId);
|
|
}
|
|
|
|
return $service;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $appPayload
|
|
*/
|
|
private function importServiceApplication(array $appPayload, Service $service, bool $preserveUuids, bool $adoptMode): ServiceApplication
|
|
{
|
|
$appUuid = $preserveUuids && filled(data_get($appPayload, 'uuid'))
|
|
? (string) data_get($appPayload, 'uuid')
|
|
: new_public_id();
|
|
if (ServiceApplication::where('uuid', $appUuid)->exists()) {
|
|
$appUuid = new_public_id();
|
|
}
|
|
|
|
$attributes = (array) data_get($appPayload, 'attributes', []);
|
|
if ($attributes === []) {
|
|
// Back-compat with older thin service-application payloads.
|
|
$attributes = collect($appPayload)->except([
|
|
'uuid', 'environment_variables', 'persistent_storages', 'file_storages', 'attributes',
|
|
])->all();
|
|
}
|
|
unset($attributes['id'], $attributes['service_id'], $attributes['created_at'], $attributes['updated_at'], $attributes['deleted_at']);
|
|
$attributes = $this->onlyExistingColumns(ServiceApplication::class, $attributes);
|
|
$attributes['service_id'] = $service->id;
|
|
$attributes['name'] = data_get($attributes, 'name') ?: data_get($appPayload, 'name') ?: 'service-app';
|
|
$attributes['status'] = $adoptMode
|
|
? (data_get($attributes, 'status') ?: data_get($appPayload, 'status') ?: 'running:unknown')
|
|
: 'exited:unhealthy';
|
|
|
|
$serviceApp = new ServiceApplication;
|
|
$serviceApp->forceFill($attributes);
|
|
$serviceApp->uuid = $appUuid;
|
|
$serviceApp->save();
|
|
|
|
$this->importEnvVars(data_get($appPayload, 'environment_variables', []), $serviceApp, false);
|
|
$this->importPersistentStorages(data_get($appPayload, 'persistent_storages', []), $serviceApp);
|
|
$this->importFileStorages(data_get($appPayload, 'file_storages', []), $serviceApp);
|
|
|
|
return $serviceApp;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $dbPayload
|
|
*/
|
|
private function importServiceDatabase(array $dbPayload, Service $service, bool $preserveUuids, bool $adoptMode, int $teamId): ServiceDatabase
|
|
{
|
|
$dbUuid = $preserveUuids && filled(data_get($dbPayload, 'uuid'))
|
|
? (string) data_get($dbPayload, 'uuid')
|
|
: new_public_id();
|
|
if (ServiceDatabase::where('uuid', $dbUuid)->exists()) {
|
|
$dbUuid = new_public_id();
|
|
}
|
|
|
|
$attributes = (array) data_get($dbPayload, 'attributes', []);
|
|
if ($attributes === []) {
|
|
$attributes = collect($dbPayload)->except([
|
|
'uuid', 'environment_variables', 'persistent_storages', 'file_storages', 'scheduled_backups', 'attributes',
|
|
])->all();
|
|
}
|
|
unset($attributes['id'], $attributes['service_id'], $attributes['created_at'], $attributes['updated_at'], $attributes['deleted_at']);
|
|
$attributes = $this->onlyExistingColumns(ServiceDatabase::class, $attributes);
|
|
$attributes['service_id'] = $service->id;
|
|
$attributes['name'] = data_get($attributes, 'name') ?: data_get($dbPayload, 'name') ?: 'service-db';
|
|
$attributes['status'] = $adoptMode
|
|
? (data_get($attributes, 'status') ?: data_get($dbPayload, 'status') ?: 'running:unknown')
|
|
: 'exited:unhealthy';
|
|
|
|
$serviceDb = new ServiceDatabase;
|
|
$serviceDb->forceFill($attributes);
|
|
$serviceDb->uuid = $dbUuid;
|
|
$serviceDb->save();
|
|
|
|
$this->importPersistentStorages(data_get($dbPayload, 'persistent_storages', []), $serviceDb);
|
|
$this->importFileStorages(data_get($dbPayload, 'file_storages', []), $serviceDb);
|
|
$this->importScheduledBackups(data_get($dbPayload, 'scheduled_backups', []), $serviceDb, $teamId);
|
|
|
|
return $serviceDb;
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $variables
|
|
*/
|
|
private function importEnvVars(array $variables, object $resource, bool $isPreview): void
|
|
{
|
|
foreach ($variables as $variable) {
|
|
EnvironmentVariable::withoutEvents(function () use ($variable, $resource, $isPreview) {
|
|
$uuid = filled(data_get($variable, 'uuid')) ? (string) data_get($variable, 'uuid') : new_public_id();
|
|
if (EnvironmentVariable::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$env = new EnvironmentVariable;
|
|
$env->forceFill([
|
|
'key' => data_get($variable, 'key'),
|
|
'value' => data_get($variable, 'value'),
|
|
'is_literal' => (bool) data_get($variable, 'is_literal', false),
|
|
'is_multiline' => (bool) data_get($variable, 'is_multiline', false),
|
|
'is_preview' => $isPreview || (bool) data_get($variable, 'is_preview', false),
|
|
'is_runtime' => (bool) data_get($variable, 'is_runtime', true),
|
|
'is_buildtime' => (bool) data_get($variable, 'is_buildtime', true),
|
|
'is_shown_once' => (bool) data_get($variable, 'is_shown_once', false),
|
|
'is_shared' => (bool) data_get($variable, 'is_shared', false),
|
|
'is_required' => (bool) data_get($variable, 'is_required', false),
|
|
'comment' => data_get($variable, 'comment'),
|
|
'order' => data_get($variable, 'order'),
|
|
'resourceable_type' => $resource->getMorphClass(),
|
|
'resourceable_id' => $resource->id,
|
|
]);
|
|
$env->uuid = $uuid;
|
|
$env->save();
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $volumes
|
|
*/
|
|
private function importPersistentStorages(array $volumes, object $resource): void
|
|
{
|
|
foreach ($volumes as $volume) {
|
|
$uuid = filled(data_get($volume, 'uuid')) ? (string) data_get($volume, 'uuid') : new_public_id();
|
|
if (LocalPersistentVolume::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$volumeModel = new LocalPersistentVolume;
|
|
$volumeModel->forceFill([
|
|
'name' => data_get($volume, 'name'),
|
|
'mount_path' => data_get($volume, 'mount_path'),
|
|
'host_path' => data_get($volume, 'host_path'),
|
|
'is_preview_suffix_enabled' => (bool) data_get($volume, 'is_preview_suffix_enabled', false),
|
|
'resource_type' => $resource->getMorphClass(),
|
|
'resource_id' => $resource->id,
|
|
]);
|
|
$volumeModel->uuid = $uuid;
|
|
$volumeModel->save();
|
|
$this->volumeMap[$uuid] = $volumeModel;
|
|
if (filled(data_get($volume, 'uuid'))) {
|
|
$this->volumeMap[(string) data_get($volume, 'uuid')] = $volumeModel;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $storages
|
|
*/
|
|
private function importFileStorages(array $storages, object $resource): void
|
|
{
|
|
foreach ($storages as $storage) {
|
|
LocalFileVolume::withoutEvents(function () use ($storage, $resource) {
|
|
$uuid = filled(data_get($storage, 'uuid')) ? (string) data_get($storage, 'uuid') : new_public_id();
|
|
if (LocalFileVolume::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
// uuid is not fillable and withoutEvents skips BaseModel's creating hook.
|
|
$file = new LocalFileVolume;
|
|
$file->forceFill([
|
|
'fs_path' => data_get($storage, 'fs_path'),
|
|
'mount_path' => data_get($storage, 'mount_path'),
|
|
'content' => data_get($storage, 'content'),
|
|
'is_directory' => (bool) data_get($storage, 'is_directory', false),
|
|
'is_host_file' => (bool) data_get($storage, 'is_host_file', false),
|
|
'chown' => data_get($storage, 'chown'),
|
|
'chmod' => data_get($storage, 'chmod'),
|
|
'is_based_on_git' => (bool) data_get($storage, 'is_based_on_git', false),
|
|
'is_preview_suffix_enabled' => (bool) data_get($storage, 'is_preview_suffix_enabled', false),
|
|
'resource_type' => $resource->getMorphClass(),
|
|
'resource_id' => $resource->id,
|
|
]);
|
|
$file->uuid = $uuid;
|
|
$file->save();
|
|
$this->volumeMap[$uuid] = $file;
|
|
if (filled(data_get($storage, 'uuid'))) {
|
|
$this->volumeMap[(string) data_get($storage, 'uuid')] = $file;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $tasks
|
|
*/
|
|
private function importScheduledTasks(array $tasks, Application|Service $resource, int $teamId): void
|
|
{
|
|
foreach ($tasks as $task) {
|
|
$uuid = filled(data_get($task, 'uuid')) ? (string) data_get($task, 'uuid') : new_public_id();
|
|
if (ScheduledTask::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$payload = [
|
|
'uuid' => $uuid,
|
|
'name' => data_get($task, 'name'),
|
|
'command' => data_get($task, 'command'),
|
|
'frequency' => data_get($task, 'frequency'),
|
|
'container' => data_get($task, 'container'),
|
|
'timeout' => data_get($task, 'timeout'),
|
|
'enabled' => (bool) data_get($task, 'enabled', true),
|
|
'team_id' => $teamId,
|
|
];
|
|
|
|
if ($resource instanceof Application) {
|
|
$payload['application_id'] = $resource->id;
|
|
} else {
|
|
$payload['service_id'] = $resource->id;
|
|
}
|
|
|
|
ScheduledTask::create($payload);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param list<array{uuid?: string, name?: string}> $tags
|
|
*/
|
|
private function importTags(array $tags, Model $resource, int $teamId): void
|
|
{
|
|
if ($tags === [] || ! method_exists($resource, 'tags')) {
|
|
return;
|
|
}
|
|
|
|
foreach ($tags as $tagPayload) {
|
|
$name = strtolower(trim((string) data_get($tagPayload, 'name', '')));
|
|
if ($name === '') {
|
|
continue;
|
|
}
|
|
|
|
$uuid = filled(data_get($tagPayload, 'uuid')) ? (string) data_get($tagPayload, 'uuid') : new_public_id();
|
|
|
|
$tag = Tag::query()
|
|
->where('team_id', $teamId)
|
|
->where('name', $name)
|
|
->first();
|
|
|
|
if (! $tag) {
|
|
// Prefer preserved uuid when free; otherwise create with a new one.
|
|
if (Tag::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
$tag = new Tag;
|
|
$tag->forceFill([
|
|
'name' => $name,
|
|
'team_id' => $teamId,
|
|
]);
|
|
$tag->uuid = $uuid;
|
|
$tag->save();
|
|
}
|
|
|
|
$resource->tags()->syncWithoutDetaching([$tag->id]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $backups
|
|
*/
|
|
private function importScheduledBackups(array $backups, Model $database, int $teamId): void
|
|
{
|
|
foreach ($backups as $backup) {
|
|
$uuid = filled(data_get($backup, 'uuid')) ? (string) data_get($backup, 'uuid') : new_public_id();
|
|
if (ScheduledDatabaseBackup::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$s3Id = null;
|
|
$s3Uuid = data_get($backup, 's3_storage_uuid');
|
|
if ($s3Uuid && isset($this->s3StorageMap[$s3Uuid])) {
|
|
$s3Id = $this->s3StorageMap[$s3Uuid]->id;
|
|
}
|
|
|
|
ScheduledDatabaseBackup::create([
|
|
'uuid' => $uuid,
|
|
'team_id' => $teamId,
|
|
'description' => data_get($backup, 'description'),
|
|
'enabled' => (bool) data_get($backup, 'enabled', true),
|
|
'save_s3' => (bool) data_get($backup, 'save_s3', false) && $s3Id !== null,
|
|
'frequency' => data_get($backup, 'frequency'),
|
|
'databases_to_backup' => data_get($backup, 'databases_to_backup'),
|
|
'dump_all' => (bool) data_get($backup, 'dump_all', false),
|
|
'database_backup_retention_amount_locally' => data_get($backup, 'database_backup_retention_amount_locally', 0),
|
|
'database_backup_retention_days_locally' => data_get($backup, 'database_backup_retention_days_locally'),
|
|
'database_backup_retention_max_storage_locally' => data_get($backup, 'database_backup_retention_max_storage_locally'),
|
|
'database_backup_retention_amount_s3' => data_get($backup, 'database_backup_retention_amount_s3'),
|
|
'database_backup_retention_days_s3' => data_get($backup, 'database_backup_retention_days_s3'),
|
|
'database_backup_retention_max_storage_s3' => data_get($backup, 'database_backup_retention_max_storage_s3'),
|
|
'timeout' => data_get($backup, 'timeout'),
|
|
'disable_local_backup' => (bool) data_get($backup, 'disable_local_backup', false),
|
|
's3_storage_id' => $s3Id,
|
|
'database_type' => $database->getMorphClass(),
|
|
'database_id' => $database->id,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function importGithubApp(array $payload, int $teamId, bool $preserveUuids): GithubApp
|
|
{
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
$existing = GithubApp::where('uuid', $uuid)->where('team_id', $teamId)->first();
|
|
if ($existing) {
|
|
return $existing;
|
|
}
|
|
if (GithubApp::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$privateKeyId = null;
|
|
$keyUuid = data_get($payload, 'private_key_uuid');
|
|
if ($keyUuid && isset($this->privateKeyMap[$keyUuid])) {
|
|
$privateKeyId = $this->privateKeyMap[$keyUuid]->id;
|
|
}
|
|
|
|
$app = new GithubApp;
|
|
$app->forceFill([
|
|
'team_id' => $teamId,
|
|
'private_key_id' => $privateKeyId,
|
|
'name' => data_get($payload, 'name') ?: 'Imported GitHub App',
|
|
'organization' => data_get($payload, 'organization'),
|
|
'api_url' => data_get($payload, 'api_url') ?: 'https://api.github.com',
|
|
'html_url' => data_get($payload, 'html_url') ?: 'https://github.com',
|
|
'custom_user' => data_get($payload, 'custom_user'),
|
|
'custom_port' => data_get($payload, 'custom_port'),
|
|
'app_id' => data_get($payload, 'app_id'),
|
|
'installation_id' => data_get($payload, 'installation_id'),
|
|
'client_id' => data_get($payload, 'client_id'),
|
|
'client_secret' => data_get($payload, 'client_secret'),
|
|
'webhook_secret' => data_get($payload, 'webhook_secret'),
|
|
'is_system_wide' => false,
|
|
'is_public' => (bool) data_get($payload, 'is_public', false),
|
|
'contents' => data_get($payload, 'contents'),
|
|
'metadata' => data_get($payload, 'metadata'),
|
|
'pull_requests' => data_get($payload, 'pull_requests'),
|
|
'administration' => data_get($payload, 'administration'),
|
|
]);
|
|
$app->uuid = $uuid;
|
|
$app->save();
|
|
|
|
return $app;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function importGitlabApp(array $payload, int $teamId, bool $preserveUuids): GitlabApp
|
|
{
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
$existing = GitlabApp::where('uuid', $uuid)->where('team_id', $teamId)->first();
|
|
if ($existing) {
|
|
return $existing;
|
|
}
|
|
if (GitlabApp::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$privateKeyId = null;
|
|
$keyUuid = data_get($payload, 'private_key_uuid');
|
|
if ($keyUuid && isset($this->privateKeyMap[$keyUuid])) {
|
|
$privateKeyId = $this->privateKeyMap[$keyUuid]->id;
|
|
}
|
|
|
|
$app = new GitlabApp;
|
|
$app->forceFill([
|
|
'team_id' => $teamId,
|
|
'private_key_id' => $privateKeyId,
|
|
'name' => data_get($payload, 'name') ?: 'Imported GitLab App',
|
|
'organization' => data_get($payload, 'organization'),
|
|
'api_url' => data_get($payload, 'api_url'),
|
|
'html_url' => data_get($payload, 'html_url'),
|
|
'custom_port' => data_get($payload, 'custom_port'),
|
|
'custom_user' => data_get($payload, 'custom_user'),
|
|
'is_system_wide' => false,
|
|
'is_public' => (bool) data_get($payload, 'is_public', false),
|
|
'app_id' => data_get($payload, 'app_id'),
|
|
'app_secret' => data_get($payload, 'app_secret'),
|
|
'oauth_id' => data_get($payload, 'oauth_id'),
|
|
'client_id' => data_get($payload, 'client_id'),
|
|
'client_secret' => data_get($payload, 'client_secret'),
|
|
'access_token' => data_get($payload, 'access_token'),
|
|
'refresh_token' => data_get($payload, 'refresh_token'),
|
|
'expires_at' => data_get($payload, 'expires_at'),
|
|
'redirect_uri' => data_get($payload, 'redirect_uri'),
|
|
'group_name' => data_get($payload, 'group_name'),
|
|
'public_key' => data_get($payload, 'public_key'),
|
|
'webhook_token' => data_get($payload, 'webhook_token'),
|
|
'deploy_key_id' => data_get($payload, 'deploy_key_id'),
|
|
]);
|
|
$app->uuid = $uuid;
|
|
$app->save();
|
|
|
|
return $app;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function importS3Storage(array $payload, int $teamId, bool $preserveUuids): S3Storage
|
|
{
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
$existing = S3Storage::where('uuid', $uuid)->where('team_id', $teamId)->first();
|
|
if ($existing) {
|
|
return $existing;
|
|
}
|
|
// Reuse by endpoint+bucket+key if already present for team
|
|
$byIdentity = S3Storage::query()
|
|
->where('team_id', $teamId)
|
|
->where('endpoint', data_get($payload, 'endpoint'))
|
|
->where('bucket', data_get($payload, 'bucket'))
|
|
->first();
|
|
if ($byIdentity) {
|
|
return $byIdentity;
|
|
}
|
|
if (S3Storage::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$storage = new S3Storage;
|
|
$storage->forceFill([
|
|
'team_id' => $teamId,
|
|
'name' => data_get($payload, 'name') ?: 'Imported S3',
|
|
'description' => data_get($payload, 'description'),
|
|
'region' => data_get($payload, 'region'),
|
|
'key' => data_get($payload, 'key'),
|
|
'secret' => data_get($payload, 'secret'),
|
|
'bucket' => data_get($payload, 'bucket'),
|
|
'endpoint' => data_get($payload, 'endpoint'),
|
|
'is_usable' => (bool) data_get($payload, 'is_usable', true),
|
|
]);
|
|
$storage->uuid = $uuid;
|
|
$storage->save();
|
|
|
|
return $storage;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function importCloudProviderToken(array $payload, int $teamId, bool $preserveUuids): CloudProviderToken
|
|
{
|
|
$uuid = $preserveUuids && filled(data_get($payload, 'uuid'))
|
|
? (string) data_get($payload, 'uuid')
|
|
: new_public_id();
|
|
|
|
$existing = CloudProviderToken::where('uuid', $uuid)->where('team_id', $teamId)->first();
|
|
if ($existing) {
|
|
return $existing;
|
|
}
|
|
if (CloudProviderToken::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$token = new CloudProviderToken;
|
|
$token->forceFill([
|
|
'team_id' => $teamId,
|
|
'provider' => data_get($payload, 'provider'),
|
|
'token' => data_get($payload, 'token'),
|
|
'name' => data_get($payload, 'name') ?: 'Imported cloud token',
|
|
'description' => data_get($payload, 'description'),
|
|
]);
|
|
$token->uuid = $uuid;
|
|
$token->save();
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $previews
|
|
*/
|
|
private function importPreviews(array $previews, Application $application, bool $preserveUuids, bool $adoptMode): void
|
|
{
|
|
foreach ($previews as $previewPayload) {
|
|
$uuid = $preserveUuids && filled(data_get($previewPayload, 'uuid'))
|
|
? (string) data_get($previewPayload, 'uuid')
|
|
: new_public_id();
|
|
|
|
$preview = ApplicationPreview::withTrashed()->where('uuid', $uuid)->first();
|
|
if (! $preview) {
|
|
$pullRequestId = data_get($previewPayload, 'pull_request_id');
|
|
if ($pullRequestId !== null) {
|
|
$preview = ApplicationPreview::withTrashed()
|
|
->where('application_id', $application->id)
|
|
->where('pull_request_id', $pullRequestId)
|
|
->first();
|
|
}
|
|
}
|
|
if ($preview && ApplicationPreview::withTrashed()->where('uuid', $uuid)->where('id', '!=', $preview->id)->exists()) {
|
|
$uuid = new_public_id();
|
|
} elseif (! $preview && ApplicationPreview::withTrashed()->where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$fqdn = data_get($previewPayload, 'fqdn');
|
|
// FQDN is globally unique; free it from other rows (including soft-deleted leftovers).
|
|
if (filled($fqdn)) {
|
|
ApplicationPreview::withoutEvents(function () use ($fqdn, $preview) {
|
|
ApplicationPreview::withTrashed()
|
|
->where('fqdn', $fqdn)
|
|
->when($preview, fn ($q) => $q->where('id', '!=', $preview->id))
|
|
->get()
|
|
->each(function (ApplicationPreview $conflict) {
|
|
$conflict->fqdn = null;
|
|
$conflict->saveQuietly();
|
|
});
|
|
});
|
|
}
|
|
|
|
$status = $adoptMode
|
|
? (data_get($previewPayload, 'status') ?: 'exited')
|
|
: 'exited';
|
|
|
|
$attributes = [
|
|
'application_id' => $application->id,
|
|
'pull_request_id' => data_get($previewPayload, 'pull_request_id'),
|
|
'pull_request_html_url' => data_get($previewPayload, 'pull_request_html_url') ?: '',
|
|
'pull_request_issue_comment_id' => data_get($previewPayload, 'pull_request_issue_comment_id'),
|
|
'fqdn' => $fqdn,
|
|
'status' => $status,
|
|
'git_type' => data_get($previewPayload, 'git_type'),
|
|
'docker_compose_domains' => data_get($previewPayload, 'docker_compose_domains'),
|
|
'docker_registry_image_tag' => data_get($previewPayload, 'docker_registry_image_tag'),
|
|
];
|
|
|
|
if ($preview) {
|
|
if ($preview->trashed()) {
|
|
$preview->restore();
|
|
}
|
|
$preview->forceFill($attributes);
|
|
if ($preserveUuids && filled(data_get($previewPayload, 'uuid')) && $preview->uuid !== $uuid) {
|
|
if (! ApplicationPreview::withTrashed()->where('uuid', $uuid)->where('id', '!=', $preview->id)->exists()) {
|
|
$preview->uuid = $uuid;
|
|
}
|
|
}
|
|
$preview->save();
|
|
} else {
|
|
$preview = new ApplicationPreview;
|
|
$preview->forceFill($attributes);
|
|
$preview->uuid = $uuid;
|
|
$preview->save();
|
|
}
|
|
|
|
// Avoid duplicating volumes when re-importing the same preview.
|
|
if ($preview->wasRecentlyCreated || $preview->persistentStorages()->count() === 0) {
|
|
$this->importPersistentStorages(data_get($previewPayload, 'persistent_storages', []), $preview);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $certificates
|
|
*/
|
|
private function importSslCertificates(array $certificates, Server $server): int
|
|
{
|
|
$count = 0;
|
|
foreach ($certificates as $certPayload) {
|
|
$resourceType = null;
|
|
$resourceId = null;
|
|
$kind = data_get($certPayload, 'resource_kind');
|
|
$resourceUuid = data_get($certPayload, 'resource_uuid');
|
|
if ($kind === 'application' && $resourceUuid && isset($this->applicationMap[$resourceUuid])) {
|
|
$resourceType = $this->applicationMap[$resourceUuid]->getMorphClass();
|
|
$resourceId = $this->applicationMap[$resourceUuid]->id;
|
|
} elseif ($kind === 'service' && $resourceUuid && isset($this->serviceMap[$resourceUuid])) {
|
|
$resourceType = $this->serviceMap[$resourceUuid]->getMorphClass();
|
|
$resourceId = $this->serviceMap[$resourceUuid]->id;
|
|
} elseif ($resourceUuid && isset($this->databaseMap[$resourceUuid])) {
|
|
$db = $this->databaseMap[$resourceUuid];
|
|
$resourceType = $db->getMorphClass();
|
|
$resourceId = $db->id;
|
|
}
|
|
|
|
SslCertificate::create([
|
|
'ssl_certificate' => data_get($certPayload, 'ssl_certificate'),
|
|
'ssl_private_key' => data_get($certPayload, 'ssl_private_key'),
|
|
'configuration_dir' => data_get($certPayload, 'configuration_dir'),
|
|
'mount_path' => data_get($certPayload, 'mount_path'),
|
|
'common_name' => data_get($certPayload, 'common_name') ?: 'imported',
|
|
'subject_alternative_names' => data_get($certPayload, 'subject_alternative_names'),
|
|
'valid_until' => data_get($certPayload, 'valid_until') ?: now()->addYear(),
|
|
'is_ca_certificate' => (bool) data_get($certPayload, 'is_ca_certificate', false),
|
|
'server_id' => $server->id,
|
|
'resource_type' => $resourceType,
|
|
'resource_id' => $resourceId,
|
|
]);
|
|
$count++;
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $backups
|
|
*/
|
|
private function importVolumeBackups(array $backups, int $teamId): int
|
|
{
|
|
$count = 0;
|
|
foreach ($backups as $backupPayload) {
|
|
$volumeUuid = (string) data_get($backupPayload, 'backupable_uuid', '');
|
|
$volume = $this->volumeMap[$volumeUuid] ?? null;
|
|
if (! $volume) {
|
|
continue;
|
|
}
|
|
|
|
$uuid = filled(data_get($backupPayload, 'uuid'))
|
|
? (string) data_get($backupPayload, 'uuid')
|
|
: new_public_id();
|
|
if (ScheduledVolumeBackup::where('uuid', $uuid)->exists()) {
|
|
$uuid = new_public_id();
|
|
}
|
|
|
|
$s3Id = null;
|
|
$s3Uuid = data_get($backupPayload, 's3_storage_uuid');
|
|
if ($s3Uuid && isset($this->s3StorageMap[$s3Uuid])) {
|
|
$s3Id = $this->s3StorageMap[$s3Uuid]->id;
|
|
}
|
|
|
|
ScheduledVolumeBackup::create([
|
|
'uuid' => $uuid,
|
|
'backupable_type' => $volume->getMorphClass(),
|
|
'backupable_id' => $volume->id,
|
|
'team_id' => $teamId,
|
|
's3_storage_id' => $s3Id,
|
|
'frequency' => data_get($backupPayload, 'frequency'),
|
|
'enabled' => (bool) data_get($backupPayload, 'enabled', true),
|
|
'save_s3' => (bool) data_get($backupPayload, 'save_s3', false) && $s3Id !== null,
|
|
'disable_local_backup' => (bool) data_get($backupPayload, 'disable_local_backup', false),
|
|
'stop_during_backup' => (bool) data_get($backupPayload, 'stop_during_backup', false),
|
|
'retention_amount_locally' => data_get($backupPayload, 'retention_amount_locally'),
|
|
'retention_days_locally' => data_get($backupPayload, 'retention_days_locally'),
|
|
'retention_max_storage_locally' => data_get($backupPayload, 'retention_max_storage_locally'),
|
|
'retention_amount_s3' => data_get($backupPayload, 'retention_amount_s3'),
|
|
'retention_days_s3' => data_get($backupPayload, 'retention_days_s3'),
|
|
'retention_max_storage_s3' => data_get($backupPayload, 'retention_max_storage_s3'),
|
|
'timeout' => data_get($backupPayload, 'timeout'),
|
|
]);
|
|
$count++;
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, StandaloneDocker|SwarmDocker> $destinationMap
|
|
*/
|
|
private function resolveDestination(?string $uuid, array $destinationMap): StandaloneDocker|SwarmDocker
|
|
{
|
|
if ($uuid && isset($destinationMap[$uuid])) {
|
|
return $destinationMap[$uuid];
|
|
}
|
|
|
|
$first = reset($destinationMap);
|
|
if ($first instanceof StandaloneDocker || $first instanceof SwarmDocker) {
|
|
return $first;
|
|
}
|
|
|
|
throw new RuntimeException('No destination available for imported resource.');
|
|
}
|
|
|
|
/**
|
|
* @param class-string $modelClass
|
|
* @param array<string, mixed> $attributes
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function onlyExistingColumns(string $modelClass, array $attributes): array
|
|
{
|
|
/** @var Model $model */
|
|
$model = new $modelClass;
|
|
$columns = Schema::getColumnListing($model->getTable());
|
|
|
|
return collect($attributes)
|
|
->only($columns)
|
|
->all();
|
|
}
|
|
}
|