authorize('viewAny', CloudProviderToken::class); $this->loadTokens(); $this->selectTokenFromUrl($selectedTokenUuid); $this->loadSavedCloudInitScripts(); $this->server_name = generate_random_name(); $this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get(); if ($this->private_keys->count() > 0) { $this->private_key_id = $this->private_keys->first()->id; } if ($this->selectedTokenUuid) { $this->current_step = 2; $this->loading_data = true; } } catch (\Throwable $e) { return handleError($e, $this); } } public function loadSavedCloudInitScripts(): void { $this->saved_cloud_init_scripts = CloudInitScript::ownedByCurrentTeam()->get(); } public function getListeners(): array { return [ 'tokenAdded.digitalocean' => 'handleTokenAdded', 'privateKeyCreated' => 'handlePrivateKeyCreated', 'modalClosed' => 'resetSelection', ]; } public function resetSelection(): void { $this->selected_token_id = null; $this->current_step = 1; $this->cloud_init_script = null; $this->save_cloud_init_script = false; $this->cloud_init_script_name = null; $this->selected_cloud_init_script_id = null; $this->show_cloud_init_script = false; $this->selectedDigitalOceanSshKeyIds = []; } public function loadTokens(): void { $this->available_tokens = CloudProviderToken::ownedByCurrentTeam() ->where('provider', 'digitalocean') ->get(); } public function handleTokenAdded($tokenId): void { $this->loadTokens(); $this->selected_token_id = $tokenId; $this->nextStep(); } public function handlePrivateKeyCreated($keyId): void { $this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get(); $this->private_key_id = $keyId; $this->resetErrorBag('private_key_id'); } protected function rules(): array { $rules = [ 'selected_token_id' => 'required|integer|exists:cloud_provider_tokens,id', ]; if ($this->current_step === 2) { $rules = array_merge($rules, [ 'server_name' => ['required', 'string', 'max:253', new ValidHostname], 'selected_region' => 'required|string', 'selected_image' => 'required', 'selected_size' => 'required|string', 'private_key_id' => 'required|integer|exists:private_keys,id,team_id,'.currentTeam()->id, 'selectedDigitalOceanSshKeyIds' => 'nullable|array', 'selectedDigitalOceanSshKeyIds.*' => 'integer', 'enable_ipv6' => 'required|boolean', 'monitoring' => 'required|boolean', 'show_cloud_init_script' => 'boolean', 'cloud_init_script' => ['nullable', 'string', new ValidCloudInitYaml], 'save_cloud_init_script' => 'boolean', 'cloud_init_script_name' => 'nullable|string|max:255', 'selected_cloud_init_script_id' => 'nullable|integer|exists:cloud_init_scripts,id', ]); } return $rules; } protected function messages(): array { return [ 'selected_token_id.required' => 'Please select a DigitalOcean token.', 'selected_token_id.exists' => 'Selected token not found.', ]; } public function selectToken(int $tokenId): mixed { $this->selected_token_id = $tokenId; return $this->nextStep(); } private function selectTokenFromUrl(?string $selectedTokenUuid): void { if (! $selectedTokenUuid) { return; } $token = $this->available_tokens->firstWhere('uuid', $selectedTokenUuid); if (! $token) { return; } $this->selectedTokenUuid = $selectedTokenUuid; $this->selected_token_id = $token->id; } private function getDigitalOceanToken(): string { if ($this->selected_token_id) { $token = $this->available_tokens->firstWhere('id', $this->selected_token_id); return $token ? $token->token : ''; } return ''; } public function nextStep() { $this->validate([ 'selected_token_id' => 'required|integer|exists:cloud_provider_tokens,id', ]); try { if (! $this->selectedTokenUuid) { $token = $this->available_tokens->firstWhere('id', $this->selected_token_id); if ($token) { return $this->redirectRoute('server.create.token', [ 'type' => 'digital-ocean', 'token_uuid' => $token->uuid, ], navigate: true); } } $this->current_step = 2; $this->loading_data = true; } catch (\Throwable $e) { return handleError($e, $this); } } public function previousStep(): mixed { if ($this->selectedTokenUuid) { return $this->redirectRoute('server.create.type', ['type' => 'digital-ocean'], navigate: true); } $this->current_step = 1; return null; } public function loadDigitalOceanData(): void { $token = $this->getDigitalOceanToken(); if (! $token) { $this->loading_data = false; $this->dispatch('error', 'Please select a valid DigitalOcean token.'); return; } $this->loading_data = true; $this->provider_data_error = null; try { $digitalOceanService = new DigitalOceanService($token); $this->regions = $digitalOceanService->getRegions(); $this->sizes = $digitalOceanService->getSizes(); $this->images = collect($digitalOceanService->getImages()) ->sortBy(fn (array $image) => ($image['distribution'] ?? '').' '.($image['name'] ?? '')) ->values() ->toArray(); $this->digitalOceanSshKeys = $digitalOceanService->getSshKeys(); $this->loading_data = false; } catch (\Throwable $e) { $this->loading_data = false; $this->provider_data_error = $this->providerDataErrorMessage('DigitalOcean', $e, 'message'); $this->dispatch('error', $this->provider_data_error); } } private function providerDataErrorMessage(string $providerName, \Throwable $e, string $jsonMessageKey): string { $details = $e->getMessage(); if ($e instanceof RequestException && $e->response) { $details = data_get($e->response->json(), $jsonMessageKey) ?: $e->response->body() ?: $details; } return "{$providerName} API error: {$details}"; } public function getAvailableSizesProperty(): array { if (! $this->selected_region) { return $this->sizes; } return collect($this->sizes) ->filter(fn (array $size) => in_array($this->selected_region, $size['regions'] ?? [])) ->values() ->toArray(); } public function getAvailableImagesProperty(): array { if (! $this->selected_region) { return $this->images; } return collect($this->images) ->filter(fn (array $image) => in_array($this->selected_region, $image['regions'] ?? [])) ->values() ->toArray(); } public function getSelectedDropletPriceProperty(): ?string { if (! $this->selected_size) { return null; } $size = collect($this->sizes)->firstWhere('slug', $this->selected_size); if (! $size || ! isset($size['price_monthly'])) { return null; } return '$'.number_format((float) $size['price_monthly'], 2); } public function updatedSelectedRegion(): void { $this->selected_size = null; $this->selected_image = null; } public function updatedSelectedSize(): void { $this->selected_image = null; } public function updatedSelectedCloudInitScriptId($value): void { if ($value) { $script = CloudInitScript::ownedByCurrentTeam()->findOrFail($value); $this->cloud_init_script = $script->script; $this->cloud_init_script_name = $script->name; $this->show_cloud_init_script = true; } } public function updatedSaveCloudInitScript(bool $value): void { if (! $value) { $this->cloud_init_script_name = null; } } public function showCloudInitScript(): void { $this->show_cloud_init_script = true; } public function getAdvancedDigitalOceanOptionsSummaryProperty(): array { $summary = []; if (count($this->selectedDigitalOceanSshKeyIds) > 0) { $summary[] = count($this->selectedDigitalOceanSshKeyIds).' extra SSH '.str('key')->plural(count($this->selectedDigitalOceanSshKeyIds)); } if (! $this->enable_ipv6) { $summary[] = 'IPv4 only'; } if (! $this->monitoring) { $summary[] = 'Monitoring off'; } if ($this->show_cloud_init_script || filled($this->cloud_init_script) || filled($this->selected_cloud_init_script_id)) { $summary[] = 'Cloud-init'; } return $summary; } public function clearCloudInitScript(): void { $this->selected_cloud_init_script_id = null; $this->cloud_init_script = ''; $this->cloud_init_script_name = ''; $this->save_cloud_init_script = false; $this->show_cloud_init_script = false; } /** * 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(DigitalOceanService $digitalOceanService): array { $privateKey = PrivateKey::ownedByCurrentTeam()->findOrFail($this->private_key_id); $md5Fingerprint = PrivateKey::generateMd5Fingerprint($privateKey->private_key); $sshKeyId = null; foreach ($digitalOceanService->getSshKeys() as $key) { if (($key['fingerprint'] ?? null) === $md5Fingerprint) { $sshKeyId = (int) $key['id']; break; } } if (! $sshKeyId) { $uploadedKey = $digitalOceanService->uploadSshKey($privateKey->name, $privateKey->getPublicKey()); $sshKeyId = (int) $uploadedKey['id']; } $sshKeys = array_values(array_unique(array_merge( [$sshKeyId], $this->selectedDigitalOceanSshKeyIds ))); $params = [ 'name' => strtolower(trim($this->server_name)), 'region' => $this->selected_region, 'size' => $this->selected_size, 'image' => $this->selected_image, 'ssh_keys' => $sshKeys, 'ipv6' => $this->enable_ipv6, 'monitoring' => $this->monitoring, ]; if (! empty($this->cloud_init_script)) { $params['user_data'] = $this->cloud_init_script; } return $digitalOceanService->createDroplet($params); } public function submit() { $this->validate(); $digitalOceanService = null; $dropletId = null; $server = null; try { $this->authorize('create', Server::class); if (Team::serverLimitReached()) { return $this->dispatch('error', 'You have reached the server limit for your subscription.'); } if ($this->save_cloud_init_script && ! empty($this->cloud_init_script) && ! empty($this->cloud_init_script_name)) { $this->authorize('create', CloudInitScript::class); CloudInitScript::create([ 'team_id' => currentTeam()->id, 'name' => $this->cloud_init_script_name, 'script' => $this->cloud_init_script, ]); } $digitalOceanService = new DigitalOceanService($this->getDigitalOceanToken()); $droplet = $this->createDigitalOceanDroplet($digitalOceanService); $dropletId = (int) $droplet['id']; // 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); } if ($this->from_onboarding) { currentTeam()->update([ 'show_boarding' => false, ]); refreshSession(); } 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'); } }