mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 14:16:28 +00:00
Merge remote-tracking branch 'origin/next' into v5-parallel-inertia-react
This commit is contained in:
commit
ae32ab5d7b
139 files changed
+8677
-545
No files matched your search
@@ -286,6 +286,7 @@ class Advanced extends Component
|
||||
$this->application->settings->save();
|
||||
|
||||
$this->dispatch('success', 'Stop grace period updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -489,6 +489,7 @@ class General extends Component
|
||||
if ($this->isContainerLabelReadonlyEnabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@ class Source extends Component
|
||||
'source_id' => $source->id,
|
||||
'source_type' => $sourceType,
|
||||
]);
|
||||
$this->dispatch('configurationChanged');
|
||||
|
||||
['repository' => $customRepository] = $this->application->customRepository();
|
||||
$repository = githubApi($this->application->source, "repos/{$customRepository}");
|
||||
|
||||
@@ -57,6 +57,7 @@ class Swarm extends Component
|
||||
$this->authorize('update', $this->application);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Swarm settings updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -68,6 +69,7 @@ class Swarm extends Component
|
||||
$this->authorize('update', $this->application);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Swarm settings updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class CloneMe extends Component
|
||||
|
||||
public ?int $selectedServer = null;
|
||||
|
||||
public ?int $selectedDestination = null;
|
||||
public ?string $selectedDestination = null;
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
@@ -76,9 +76,9 @@ class CloneMe extends Component
|
||||
return view('livewire.project.clone-me');
|
||||
}
|
||||
|
||||
public function selectServer($server_id, $destination_id)
|
||||
public function selectServer($server_id, $destination_uuid)
|
||||
{
|
||||
if ($server_id == $this->selectedServer && $destination_id == $this->selectedDestination) {
|
||||
if ($server_id == $this->selectedServer && $destination_uuid === $this->selectedDestination) {
|
||||
$this->selectedServer = null;
|
||||
$this->selectedDestination = null;
|
||||
$this->server = null;
|
||||
@@ -86,7 +86,7 @@ class CloneMe extends Component
|
||||
return;
|
||||
}
|
||||
$this->selectedServer = $server_id;
|
||||
$this->selectedDestination = $destination_id;
|
||||
$this->selectedDestination = $destination_uuid;
|
||||
$this->server = $this->servers->where('id', $server_id)->first();
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ class CloneMe extends Component
|
||||
'selectedDestination' => 'required',
|
||||
'newName' => ValidationPatterns::nameRules(),
|
||||
]);
|
||||
$selectedDestination = find_resource_destination_for_current_team($this->selectedDestination);
|
||||
if (! $selectedDestination) {
|
||||
throw new \Exception('Destination not found.');
|
||||
}
|
||||
if ($type === 'project') {
|
||||
$foundProject = Project::where('name', $this->newName)->first();
|
||||
if ($foundProject) {
|
||||
@@ -130,7 +134,6 @@ class CloneMe extends Component
|
||||
$databases = $this->environment->databases();
|
||||
$services = $this->environment->services;
|
||||
foreach ($applications as $application) {
|
||||
$selectedDestination = $this->servers->flatMap(fn ($server) => $server->destinations())->where('id', $this->selectedDestination)->first();
|
||||
clone_application($application, $selectedDestination, [
|
||||
'environment_id' => $environment->id,
|
||||
], $this->cloneVolumeData);
|
||||
@@ -147,7 +150,8 @@ class CloneMe extends Component
|
||||
'status' => 'exited',
|
||||
'started_at' => null,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $this->selectedDestination,
|
||||
'destination_id' => $selectedDestination->id,
|
||||
'destination_type' => $selectedDestination->getMorphClass(),
|
||||
]);
|
||||
$newDatabase->save();
|
||||
|
||||
@@ -265,7 +269,9 @@ class CloneMe extends Component
|
||||
])->fill([
|
||||
'uuid' => $uuid,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $this->selectedDestination,
|
||||
'destination_id' => $selectedDestination->id,
|
||||
'destination_type' => $selectedDestination->getMorphClass(),
|
||||
'server_id' => $selectedDestination->server_id,
|
||||
]);
|
||||
$newService->save();
|
||||
|
||||
|
||||
@@ -22,13 +22,7 @@ class Index extends Component
|
||||
if (! $database) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
// No backups
|
||||
if (
|
||||
$database->getMorphClass() === \App\Models\StandaloneRedis::class ||
|
||||
$database->getMorphClass() === \App\Models\StandaloneKeydb::class ||
|
||||
$database->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
|
||||
$database->getMorphClass() === \App\Models\StandaloneClickhouse::class
|
||||
) {
|
||||
if (! $database->isBackupSolutionAvailable()) {
|
||||
return redirect()->route('project.database.configuration', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
|
||||
@@ -48,6 +48,12 @@ class CreateScheduledBackup extends Component
|
||||
try {
|
||||
$this->authorize('manageBackups', $this->database);
|
||||
|
||||
if (! $this->database->isBackupSolutionAvailable()) {
|
||||
$this->dispatch('error', 'Scheduled backups are not supported for this database type.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validate();
|
||||
|
||||
if ($this->saveToS3) {
|
||||
@@ -87,6 +93,8 @@ class CreateScheduledBackup extends Component
|
||||
$payload['databases_to_backup'] = $this->database->mysql_database;
|
||||
} elseif ($this->database->type() === 'standalone-mariadb') {
|
||||
$payload['databases_to_backup'] = $this->database->mariadb_database;
|
||||
} elseif ($this->database->type() === 'standalone-clickhouse') {
|
||||
$payload['databases_to_backup'] = $this->database->clickhouse_db;
|
||||
}
|
||||
|
||||
$databaseBackup = ScheduledDatabaseBackup::create($payload);
|
||||
|
||||
@@ -47,19 +47,20 @@ class DockerCompose extends Component
|
||||
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
|
||||
|
||||
$destination_uuid = $this->query['destination'] ?? null;
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
$destination = find_resource_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found.');
|
||||
}
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
$service = Service::create([
|
||||
$service = new Service([
|
||||
'docker_compose_raw' => $this->dockerComposeRaw,
|
||||
'environment_id' => $environment->id,
|
||||
'server_id' => $destination->server_id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination_class,
|
||||
]);
|
||||
$service->save();
|
||||
|
||||
$variables = parseEnvFormatToArray($this->envFile);
|
||||
foreach ($variables as $key => $data) {
|
||||
|
||||
@@ -115,7 +115,7 @@ class DockerImage extends Component
|
||||
$parser->parse($dockerImage);
|
||||
|
||||
$destination_uuid = $this->query['destination'] ?? null;
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
$destination = find_resource_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found.');
|
||||
}
|
||||
@@ -133,7 +133,7 @@ class DockerImage extends Component
|
||||
// Determine the image tag based on whether it's a hash or regular tag
|
||||
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
|
||||
|
||||
$application = Application::create([
|
||||
$application = new Application([
|
||||
'name' => 'docker-image-'.new_public_id(),
|
||||
'repository_project_id' => 0,
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
@@ -147,6 +147,7 @@ class DockerImage extends Component
|
||||
'destination_type' => $destination_class,
|
||||
'health_check_enabled' => false,
|
||||
]);
|
||||
$application->save();
|
||||
|
||||
$fqdn = generateUrl(server: $destination->server, random: $application->uuid);
|
||||
$application->update([
|
||||
|
||||
@@ -192,7 +192,7 @@ class GithubPrivateRepository extends Component
|
||||
}
|
||||
|
||||
$destination_uuid = $this->query['destination'] ?? null;
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
$destination = find_resource_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found.');
|
||||
}
|
||||
@@ -201,7 +201,7 @@ class GithubPrivateRepository extends Component
|
||||
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
|
||||
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
|
||||
|
||||
$application = Application::create([
|
||||
$application = new Application([
|
||||
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
|
||||
'repository_project_id' => $this->selected_repository_id,
|
||||
'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(),
|
||||
@@ -216,6 +216,7 @@ class GithubPrivateRepository extends Component
|
||||
'source_id' => $this->github_app->id,
|
||||
'source_type' => $this->github_app->getMorphClass(),
|
||||
]);
|
||||
$application->save();
|
||||
$application->settings->is_static = $this->is_static;
|
||||
$application->settings->save();
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
$this->validate();
|
||||
try {
|
||||
$destination_uuid = $this->query['destination'] ?? null;
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
$destination = find_resource_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found.');
|
||||
}
|
||||
@@ -185,7 +185,8 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
$application_init['docker_compose_location'] = $this->docker_compose_location;
|
||||
$application_init['base_directory'] = $this->base_directory;
|
||||
}
|
||||
$application = Application::create($application_init);
|
||||
$application = new Application($application_init);
|
||||
$application->save();
|
||||
$application->settings->is_static = $this->is_static;
|
||||
$application->settings->save();
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ class PublicGitRepository extends Component
|
||||
$project_uuid = $this->parameters['project_uuid'];
|
||||
$environment_uuid = $this->parameters['environment_uuid'];
|
||||
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
$destination = find_resource_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found.');
|
||||
}
|
||||
@@ -336,7 +336,8 @@ class PublicGitRepository extends Component
|
||||
$application_init['docker_compose_location'] = $this->docker_compose_location;
|
||||
$application_init['base_directory'] = $this->base_directory;
|
||||
}
|
||||
$application = Application::create($application_init);
|
||||
$application = new Application($application_init);
|
||||
$application->save();
|
||||
|
||||
$application->settings->is_static = $this->isStatic;
|
||||
$application->settings->save();
|
||||
|
||||
@@ -24,6 +24,8 @@ class Select extends Component
|
||||
|
||||
public Collection|null|Server $servers;
|
||||
|
||||
public ?Collection $buildServers = null;
|
||||
|
||||
public bool $onlyBuildServerAvailable = false;
|
||||
|
||||
public ?Collection $standaloneDockers;
|
||||
@@ -380,7 +382,7 @@ class Select extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
if (count($this->servers) === 1) {
|
||||
if (count($this->servers) === 1 && $this->buildServers?->isEmpty()) {
|
||||
$server = $this->servers->first();
|
||||
if ($server instanceof Server) {
|
||||
$this->setServer($server);
|
||||
@@ -452,12 +454,8 @@ class Select extends Component
|
||||
public function loadServers()
|
||||
{
|
||||
$this->servers = Server::isUsable()->get()->sortBy('name');
|
||||
$this->allServers = $this->servers;
|
||||
|
||||
if ($this->allServers && $this->allServers->isNotEmpty()) {
|
||||
$this->onlyBuildServerAvailable = $this->allServers->every(function ($server) {
|
||||
return $server->isBuildServer();
|
||||
});
|
||||
}
|
||||
$this->buildServers = Server::isUsableBuildServer()->get()->sortBy('name');
|
||||
$this->allServers = $this->servers->concat($this->buildServers);
|
||||
$this->onlyBuildServerAvailable = $this->servers->isEmpty() && $this->buildServers->isNotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ CMD ["nginx", "-g", "daemon off;"]
|
||||
'dockerfile' => 'required',
|
||||
]);
|
||||
$destination_uuid = $this->query['destination'] ?? null;
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
$destination = find_resource_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found.');
|
||||
}
|
||||
@@ -51,7 +51,7 @@ CMD ["nginx", "-g", "daemon off;"]
|
||||
if (! $port) {
|
||||
$port = 80;
|
||||
}
|
||||
$application = Application::create([
|
||||
$application = new Application([
|
||||
'name' => 'dockerfile-'.new_public_id(),
|
||||
'repository_project_id' => 0,
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
@@ -66,6 +66,7 @@ CMD ["nginx", "-g", "daemon off;"]
|
||||
'source_id' => 0,
|
||||
'source_type' => GithubApp::class,
|
||||
]);
|
||||
$application->save();
|
||||
|
||||
$fqdn = generateUrl(server: $destination->server, random: $application->uuid);
|
||||
$application->update([
|
||||
|
||||
@@ -33,7 +33,7 @@ class Create extends Component
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
if (isset($type) && isset($destination_uuid)) {
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
$destination = find_resource_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
@@ -96,7 +96,8 @@ class Create extends Component
|
||||
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
|
||||
data_set($service_payload, 'connect_to_docker_network', true);
|
||||
}
|
||||
$service = Service::create($service_payload);
|
||||
$service = new Service($service_payload);
|
||||
$service->save();
|
||||
$service->name = "$oneClickServiceName-".$service->uuid;
|
||||
$service->save();
|
||||
if ($oneClickDotEnvs?->count() > 0) {
|
||||
|
||||
@@ -118,9 +118,8 @@ class Destination extends Component
|
||||
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
|
||||
$network = StandaloneDocker::ownedByCurrentTeam()->where('server_id', $server->id)->findOrFail($network_id);
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->resource->getConnection()->transaction(function () use ($network, $server) {
|
||||
$main_destination = $this->resource->destination;
|
||||
$mainDestination = $this->resource->destination;
|
||||
$this->resource->update([
|
||||
'destination_id' => $network->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
@@ -128,7 +127,7 @@ class Destination extends Component
|
||||
$this->resource->additional_networks()
|
||||
->wherePivot('server_id', $server->id)
|
||||
->detach($network->id);
|
||||
$this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]);
|
||||
$this->resource->additional_networks()->attach($mainDestination->id, ['server_id' => $mainDestination->server->id]);
|
||||
});
|
||||
$this->resource->refresh();
|
||||
$this->refreshServers();
|
||||
|
||||
@@ -76,6 +76,7 @@ class All extends Component
|
||||
$this->resource->settings->save();
|
||||
$this->getDevView();
|
||||
$this->dispatch('success', 'Environment variable settings updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ class HealthChecks extends Component
|
||||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Health check updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
}
|
||||
|
||||
public function submit()
|
||||
@@ -178,6 +179,7 @@ class HealthChecks extends Component
|
||||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Health check updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -213,6 +215,7 @@ class HealthChecks extends Component
|
||||
} else {
|
||||
$this->dispatch('success', 'Health check '.($this->healthCheckEnabled ? 'enabled' : 'disabled').'.');
|
||||
}
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
@@ -19,7 +18,6 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Models\SwarmDocker;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -37,6 +35,8 @@ class ResourceOperations extends Component
|
||||
|
||||
public $servers;
|
||||
|
||||
public $buildServers;
|
||||
|
||||
public bool $cloneVolumeData = false;
|
||||
|
||||
public function mount()
|
||||
@@ -45,7 +45,9 @@ class ResourceOperations extends Component
|
||||
$this->projectUuid = data_get($parameters, 'project_uuid');
|
||||
$this->environmentUuid = data_get($parameters, 'environment_uuid');
|
||||
$this->projects = Project::ownedByCurrentTeamCached();
|
||||
$this->servers = currentTeam()->servers->filter(fn ($server) => ! $server->isBuildServer());
|
||||
$servers = currentTeam()->servers()->get();
|
||||
$this->servers = $servers->reject(fn ($server) => $server->isBuildServer());
|
||||
$this->buildServers = $servers->filter(fn ($server) => $server->isBuildServer());
|
||||
}
|
||||
|
||||
public function toggleVolumeCloning(bool $value)
|
||||
@@ -53,20 +55,20 @@ class ResourceOperations extends Component
|
||||
$this->cloneVolumeData = $value;
|
||||
}
|
||||
|
||||
public function cloneTo($destination_id)
|
||||
public function cloneTo($destination_uuid)
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$new_destination = StandaloneDocker::ownedByCurrentTeam()->find($destination_id);
|
||||
if (! $new_destination) {
|
||||
$new_destination = SwarmDocker::ownedByCurrentTeam()->find($destination_id);
|
||||
}
|
||||
$new_destination = find_resource_destination_for_current_team($destination_uuid);
|
||||
if (! $new_destination) {
|
||||
return $this->addError('destination_id', 'Destination not found.');
|
||||
}
|
||||
$uuid = new_public_id();
|
||||
$server = $new_destination->server;
|
||||
if (! $server->canHostResources()) {
|
||||
return $this->addError('destination_id', 'The selected server cannot host resources.');
|
||||
}
|
||||
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
|
||||
@@ -99,6 +101,7 @@ class ResourceOperations extends Component
|
||||
'status' => 'exited',
|
||||
'started_at' => null,
|
||||
'destination_id' => $new_destination->id,
|
||||
'destination_type' => $new_destination->getMorphClass(),
|
||||
]);
|
||||
$new_resource->save();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Services\DigitalOceanService;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -402,11 +403,11 @@ class ByDigitalOcean extends Component
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{droplet: array, ip: string|null}
|
||||
* Create the droplet on DigitalOcean and return the raw droplet payload.
|
||||
* The public IP may not be assigned yet at this point.
|
||||
*/
|
||||
private function createDigitalOceanDroplet(string $token): array
|
||||
private function createDigitalOceanDroplet(DigitalOceanService $digitalOceanService): array
|
||||
{
|
||||
$digitalOceanService = new DigitalOceanService($token);
|
||||
$privateKey = PrivateKey::ownedByCurrentTeam()->findOrFail($this->private_key_id);
|
||||
$md5Fingerprint = PrivateKey::generateMd5Fingerprint($privateKey->private_key);
|
||||
|
||||
@@ -442,20 +443,17 @@ class ByDigitalOcean extends Component
|
||||
$params['user_data'] = $this->cloud_init_script;
|
||||
}
|
||||
|
||||
$droplet = $digitalOceanService->createDroplet($params);
|
||||
$droplet = $digitalOceanService->waitForPublicIp($droplet, true, $this->enable_ipv6);
|
||||
$ipAddress = $digitalOceanService->getPublicIpAddress($droplet, true, $this->enable_ipv6);
|
||||
|
||||
return [
|
||||
'droplet' => $droplet,
|
||||
'ip' => $ipAddress,
|
||||
];
|
||||
return $digitalOceanService->createDroplet($params);
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$digitalOceanService = null;
|
||||
$dropletId = null;
|
||||
$server = null;
|
||||
|
||||
try {
|
||||
$this->authorize('create', Server::class);
|
||||
|
||||
@@ -473,30 +471,46 @@ class ByDigitalOcean extends Component
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $this->createDigitalOceanDroplet($this->getDigitalOceanToken());
|
||||
$droplet = $result['droplet'];
|
||||
$ipAddress = $result['ip'];
|
||||
$digitalOceanService = new DigitalOceanService($this->getDigitalOceanToken());
|
||||
$droplet = $this->createDigitalOceanDroplet($digitalOceanService);
|
||||
$dropletId = (int) $droplet['id'];
|
||||
|
||||
if (! $ipAddress) {
|
||||
throw new \Exception('No public IP address available for the new droplet.');
|
||||
// Persist the server immediately so the droplet is always tracked
|
||||
// in Coolify, even if waiting for the public IP fails below.
|
||||
$server = DB::transaction(function () use ($dropletId, $droplet): Server {
|
||||
$server = Server::create([
|
||||
'name' => strtolower(trim($this->server_name)),
|
||||
'ip' => Server::PLACEHOLDER_IP,
|
||||
'user' => 'root',
|
||||
'port' => 22,
|
||||
'team_id' => currentTeam()->id,
|
||||
'private_key_id' => $this->private_key_id,
|
||||
'cloud_provider_token_id' => $this->selected_token_id,
|
||||
'digitalocean_droplet_id' => $dropletId,
|
||||
'digitalocean_droplet_status' => $droplet['status'] ?? null,
|
||||
]);
|
||||
|
||||
$server->proxy->set('status', 'exited');
|
||||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
|
||||
return $server;
|
||||
});
|
||||
|
||||
try {
|
||||
$droplet = $digitalOceanService->waitForPublicIp($droplet, true, $this->enable_ipv6);
|
||||
$ipAddress = $digitalOceanService->getPublicIpAddress($droplet, true, $this->enable_ipv6);
|
||||
if ($ipAddress) {
|
||||
$server->update([
|
||||
'ip' => $ipAddress,
|
||||
'digitalocean_droplet_status' => $droplet['status'] ?? $server->digitalocean_droplet_status,
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Non-fatal: the server page polling backfills the IP later.
|
||||
report($e);
|
||||
}
|
||||
|
||||
$server = Server::create([
|
||||
'name' => strtolower(trim($this->server_name)),
|
||||
'ip' => $ipAddress,
|
||||
'user' => 'root',
|
||||
'port' => 22,
|
||||
'team_id' => currentTeam()->id,
|
||||
'private_key_id' => $this->private_key_id,
|
||||
'cloud_provider_token_id' => $this->selected_token_id,
|
||||
'digitalocean_droplet_id' => $droplet['id'],
|
||||
'digitalocean_droplet_status' => $droplet['status'] ?? null,
|
||||
]);
|
||||
|
||||
$server->proxy->set('status', 'exited');
|
||||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
|
||||
if ($this->from_onboarding) {
|
||||
currentTeam()->update([
|
||||
'show_boarding' => false,
|
||||
@@ -506,10 +520,25 @@ class ByDigitalOcean extends Component
|
||||
|
||||
return redirectRoute($this, 'server.show', [$server->uuid]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->deleteUntrackedDroplet($digitalOceanService, $dropletId, $server);
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteUntrackedDroplet(?DigitalOceanService $digitalOceanService, ?int $dropletId, ?Server $server): void
|
||||
{
|
||||
if (! $digitalOceanService || ! $dropletId || $server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$digitalOceanService->deleteDroplet($dropletId);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.new.by-digital-ocean');
|
||||
|
||||
@@ -717,20 +717,19 @@ class ByHetzner extends Component
|
||||
$ipAddress = $hetznerServer['public_net']['ipv6']['ip'];
|
||||
}
|
||||
|
||||
if (! $ipAddress) {
|
||||
throw new \Exception('No public IP address available. Enable at least one of IPv4 or IPv6.');
|
||||
}
|
||||
|
||||
// Create server in Coolify database
|
||||
// Create server in Coolify database immediately so the Hetzner
|
||||
// server is always tracked, even when no IP is assigned yet —
|
||||
// the server page polling backfills the placeholder IP later.
|
||||
$server = Server::create([
|
||||
'name' => $this->server_name,
|
||||
'ip' => $ipAddress,
|
||||
'ip' => $ipAddress ?? Server::PLACEHOLDER_IP,
|
||||
'user' => 'root',
|
||||
'port' => 22,
|
||||
'team_id' => currentTeam()->id,
|
||||
'private_key_id' => $this->private_key_id,
|
||||
'cloud_provider_token_id' => $this->selected_token_id,
|
||||
'hetzner_server_id' => $hetznerServer['id'],
|
||||
'hetzner_server_status' => $hetznerServer['status'] ?? null,
|
||||
]);
|
||||
|
||||
$server->proxy->set('status', 'exited');
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Services\VultrService;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -377,9 +378,8 @@ class ByVultr extends Component
|
||||
return "{$providerName} API error: {$details}";
|
||||
}
|
||||
|
||||
private function createVultrServer(string $token): array
|
||||
private function createVultrServer(VultrService $vultrService): array
|
||||
{
|
||||
$vultrService = new VultrService($token);
|
||||
$privateKey = PrivateKey::ownedByCurrentTeam()->findOrFail($this->private_key_id);
|
||||
$publicKey = $privateKey->getPublicKey();
|
||||
$existingKey = $this->findMatchingSshKey($vultrService->getSshKeys(), $publicKey);
|
||||
@@ -419,6 +419,10 @@ class ByVultr extends Component
|
||||
return null;
|
||||
}
|
||||
|
||||
$vultrService = null;
|
||||
$vultrInstanceId = null;
|
||||
$server = null;
|
||||
|
||||
try {
|
||||
$this->authorize('create', Server::class);
|
||||
|
||||
@@ -437,33 +441,43 @@ class ByVultr extends Component
|
||||
}
|
||||
|
||||
$vultrService = new VultrService($this->getVultrToken());
|
||||
$vultrInstance = $this->createVultrServer($this->getVultrToken());
|
||||
$ipAddress = $vultrService->getPublicIp($vultrInstance, $this->disable_public_ipv4, $this->enable_ipv6) ?? '0.0.0.0';
|
||||
$vultrInstance = $this->createVultrServer($vultrService);
|
||||
$vultrInstanceId = (string) $vultrInstance['id'];
|
||||
$ipAddress = $vultrService->getPublicIp($vultrInstance, $this->disable_public_ipv4, $this->enable_ipv6) ?? Server::PLACEHOLDER_IP;
|
||||
|
||||
$server = Server::create([
|
||||
'name' => strtolower(trim($this->server_name)),
|
||||
'ip' => $ipAddress,
|
||||
'user' => 'root',
|
||||
'port' => 22,
|
||||
'team_id' => currentTeam()->id,
|
||||
'private_key_id' => $this->private_key_id,
|
||||
'cloud_provider_token_id' => $this->selected_token_id,
|
||||
'vultr_instance_id' => $vultrInstance['id'],
|
||||
'vultr_instance_status' => $vultrInstance['status'] ?? null,
|
||||
]);
|
||||
|
||||
$vultrInstance = $vultrService->waitForPublicIp($vultrInstance, $this->disable_public_ipv4, $this->enable_ipv6);
|
||||
$assignedIpAddress = $vultrService->getPublicIp($vultrInstance, $this->disable_public_ipv4, $this->enable_ipv6);
|
||||
if ($assignedIpAddress && $assignedIpAddress !== $server->ip) {
|
||||
$server->update([
|
||||
'ip' => $assignedIpAddress,
|
||||
'vultr_instance_status' => $vultrInstance['status'] ?? $server->vultr_instance_status,
|
||||
$server = DB::transaction(function () use ($ipAddress, $vultrInstanceId, $vultrInstance): Server {
|
||||
$server = Server::create([
|
||||
'name' => strtolower(trim($this->server_name)),
|
||||
'ip' => $ipAddress,
|
||||
'user' => 'root',
|
||||
'port' => 22,
|
||||
'team_id' => currentTeam()->id,
|
||||
'private_key_id' => $this->private_key_id,
|
||||
'cloud_provider_token_id' => $this->selected_token_id,
|
||||
'vultr_instance_id' => $vultrInstanceId,
|
||||
'vultr_instance_status' => $vultrInstance['status'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
$server->proxy->set('status', 'exited');
|
||||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
$server->proxy->set('status', 'exited');
|
||||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
|
||||
return $server;
|
||||
});
|
||||
|
||||
try {
|
||||
$vultrInstance = $vultrService->waitForPublicIp($vultrInstance, $this->disable_public_ipv4, $this->enable_ipv6);
|
||||
$assignedIpAddress = $vultrService->getPublicIp($vultrInstance, $this->disable_public_ipv4, $this->enable_ipv6);
|
||||
if ($assignedIpAddress && $assignedIpAddress !== $server->ip) {
|
||||
$server->update([
|
||||
'ip' => $assignedIpAddress,
|
||||
'vultr_instance_status' => $vultrInstance['status'] ?? $server->vultr_instance_status,
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Non-fatal: the server page polling backfills the IP later.
|
||||
report($e);
|
||||
}
|
||||
|
||||
if ($this->from_onboarding) {
|
||||
currentTeam()->update([
|
||||
@@ -474,10 +488,25 @@ class ByVultr extends Component
|
||||
|
||||
return redirectRoute($this, 'server.show', [$server->uuid]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->deleteUntrackedInstance($vultrService, $vultrInstanceId, $server);
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteUntrackedInstance(?VultrService $vultrService, ?string $vultrInstanceId, ?Server $server): void
|
||||
{
|
||||
if (! $vultrService || ! $vultrInstanceId || $server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$vultrService->deleteInstance($vultrInstanceId);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.new.by-vultr');
|
||||
|
||||
@@ -202,7 +202,7 @@ class Show extends Component
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
||||
$this->syncData();
|
||||
if (! $this->server->isEmpty()) {
|
||||
if (! $this->server->isBuildServer() && ! $this->server->isEmpty()) {
|
||||
$this->isBuildServerLocked = true;
|
||||
}
|
||||
// Load saved Hetzner status and validation state
|
||||
@@ -409,6 +409,12 @@ class Show extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if ($value === true && ! $this->server->isEmpty()) {
|
||||
$this->isBuildServer = false;
|
||||
$this->dispatch('error', 'A server with existing resources cannot be configured as a build server.');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($value === true && $this->isSentinelEnabled) {
|
||||
$this->isSentinelEnabled = false;
|
||||
$this->isMetricsEnabled = false;
|
||||
@@ -488,6 +494,11 @@ class Show extends Component
|
||||
$this->server->hetzner_server_status = $this->hetznerServerStatus;
|
||||
$this->server->update(['hetzner_server_status' => $this->hetznerServerStatus]);
|
||||
}
|
||||
|
||||
$assignedIp = data_get($serverData, 'public_net.ipv4.ip') ?? data_get($serverData, 'public_net.ipv6.ip');
|
||||
if ($this->server->backfillPlaceholderIp($assignedIp)) {
|
||||
$this->ip = $this->server->ip;
|
||||
}
|
||||
if ($manual) {
|
||||
$this->dispatch('success', 'Server status refreshed: '.ucfirst($this->hetznerServerStatus ?? 'unknown'));
|
||||
}
|
||||
@@ -606,6 +617,7 @@ class Show extends Component
|
||||
public function startVultrInstance()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if (! $this->server->vultr_instance_id || ! $this->server->cloudProviderToken) {
|
||||
$this->dispatch('error', 'This server is not associated with a Vultr instance or token.');
|
||||
|
||||
|
||||
Reference in new issue
Block a user