feat: implement Hetzner deletion failure notification system with email and messaging support
This commit is contained in:
parent
bbaef03602
commit
513f6b54f7
5 changed files with 205 additions and 0 deletions
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Server\HetznerDeletionFailed;
|
||||
use App\Services\HetznerService;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
|
|
@ -92,6 +94,10 @@ private function deleteFromHetznerById(int $hetznerServerId, ?int $cloudProvider
|
|||
'hetzner_server_id' => $hetznerServerId,
|
||||
'team_id' => $teamId,
|
||||
]);
|
||||
|
||||
// Notify the team about the failure
|
||||
$team = Team::find($teamId);
|
||||
$team?->notify(new HetznerDeletionFailed($hetznerServerId, $teamId, $e->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
app/Notifications/Server/HetznerDeletionFailed.php
Normal file
71
app/Notifications/Server/HetznerDeletionFailed.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications\Server;
|
||||
|
||||
use App\Notifications\CustomEmailNotification;
|
||||
use App\Notifications\Dto\DiscordMessage;
|
||||
use App\Notifications\Dto\PushoverMessage;
|
||||
use App\Notifications\Dto\SlackMessage;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class HetznerDeletionFailed extends CustomEmailNotification
|
||||
{
|
||||
public function __construct(public int $hetznerServerId, public int $teamId, public string $errorMessage)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
ray('hello');
|
||||
ray($notifiable);
|
||||
|
||||
return $notifiable->getEnabledChannels('hetzner_deletion_failed');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage;
|
||||
$mail->subject("Coolify: [ACTION REQUIRED] Failed to delete Hetzner server #{$this->hetznerServerId}");
|
||||
$mail->view('emails.hetzner-deletion-failed', [
|
||||
'hetznerServerId' => $this->hetznerServerId,
|
||||
'errorMessage' => $this->errorMessage,
|
||||
]);
|
||||
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function toDiscord(): DiscordMessage
|
||||
{
|
||||
return new DiscordMessage(
|
||||
title: ':cross_mark: Coolify: [ACTION REQUIRED] Failed to delete Hetzner server',
|
||||
description: "Failed to delete Hetzner server #{$this->hetznerServerId} from Hetzner Cloud.\n\n**Error:** {$this->errorMessage}\n\nThe server has been removed from Coolify, but may still exist in your Hetzner Cloud account. Please check your Hetzner Cloud console and manually delete the server if needed.",
|
||||
color: DiscordMessage::errorColor(),
|
||||
);
|
||||
}
|
||||
|
||||
public function toTelegram(): array
|
||||
{
|
||||
return [
|
||||
'message' => "Coolify: [ACTION REQUIRED] Failed to delete Hetzner server #{$this->hetznerServerId} from Hetzner Cloud.\n\nError: {$this->errorMessage}\n\nThe server has been removed from Coolify, but may still exist in your Hetzner Cloud account. Please check your Hetzner Cloud console and manually delete the server if needed.",
|
||||
];
|
||||
}
|
||||
|
||||
public function toPushover(): PushoverMessage
|
||||
{
|
||||
return new PushoverMessage(
|
||||
title: 'Hetzner Server Deletion Failed',
|
||||
level: 'error',
|
||||
message: "[ACTION REQUIRED] Failed to delete Hetzner server #{$this->hetznerServerId}.\n\nError: {$this->errorMessage}\n\nThe server has been removed from Coolify, but may still exist in your Hetzner Cloud account. Please check and manually delete if needed.",
|
||||
);
|
||||
}
|
||||
|
||||
public function toSlack(): SlackMessage
|
||||
{
|
||||
return new SlackMessage(
|
||||
title: 'Coolify: [ACTION REQUIRED] Hetzner Server Deletion Failed',
|
||||
description: "Failed to delete Hetzner server #{$this->hetznerServerId} from Hetzner Cloud.\n\nError: {$this->errorMessage}\n\nThe server has been removed from Coolify, but may still exist in your Hetzner Cloud account. Please check your Hetzner Cloud console and manually delete the server if needed.",
|
||||
color: SlackMessage::errorColor()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ trait HasNotificationSettings
|
|||
'general',
|
||||
'test',
|
||||
'ssl_certificate_renewal',
|
||||
'hetzner_deletion_failure',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
13
resources/views/emails/hetzner-deletion-failed.blade.php
Normal file
13
resources/views/emails/hetzner-deletion-failed.blade.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<x-emails.layout>
|
||||
Failed to delete Hetzner server #{{ $hetznerServerId }} from Hetzner Cloud.
|
||||
|
||||
Error:
|
||||
<pre>
|
||||
{{ $errorMessage }}
|
||||
</pre>
|
||||
|
||||
The server has been removed from Coolify, but may still exist in your Hetzner Cloud account.
|
||||
|
||||
Please check your Hetzner Cloud console and manually delete the server if needed to avoid ongoing charges.
|
||||
|
||||
</x-emails.layout>
|
||||
114
tests/Unit/HetznerDeletionFailedNotificationTest.php
Normal file
114
tests/Unit/HetznerDeletionFailedNotificationTest.php
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
use App\Notifications\Server\HetznerDeletionFailed;
|
||||
use Mockery;
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
it('can be instantiated with correct properties', function () {
|
||||
$notification = new HetznerDeletionFailed(
|
||||
hetznerServerId: 12345,
|
||||
teamId: 1,
|
||||
errorMessage: 'Hetzner API error: Server not found'
|
||||
);
|
||||
|
||||
expect($notification)->toBeInstanceOf(HetznerDeletionFailed::class)
|
||||
->and($notification->hetznerServerId)->toBe(12345)
|
||||
->and($notification->teamId)->toBe(1)
|
||||
->and($notification->errorMessage)->toBe('Hetzner API error: Server not found');
|
||||
});
|
||||
|
||||
it('uses hetzner_deletion_failed event for channels', function () {
|
||||
$notification = new HetznerDeletionFailed(
|
||||
hetznerServerId: 12345,
|
||||
teamId: 1,
|
||||
errorMessage: 'Test error'
|
||||
);
|
||||
|
||||
$mockNotifiable = Mockery::mock();
|
||||
$mockNotifiable->shouldReceive('getEnabledChannels')
|
||||
->with('hetzner_deletion_failed')
|
||||
->once()
|
||||
->andReturn([]);
|
||||
|
||||
$channels = $notification->via($mockNotifiable);
|
||||
|
||||
expect($channels)->toBeArray();
|
||||
});
|
||||
|
||||
it('generates correct mail content', function () {
|
||||
$notification = new HetznerDeletionFailed(
|
||||
hetznerServerId: 67890,
|
||||
teamId: 1,
|
||||
errorMessage: 'Connection timeout'
|
||||
);
|
||||
|
||||
$mail = $notification->toMail();
|
||||
|
||||
expect($mail->subject)->toBe('Coolify: [ACTION REQUIRED] Failed to delete Hetzner server #67890')
|
||||
->and($mail->view)->toBe('emails.hetzner-deletion-failed')
|
||||
->and($mail->viewData['hetznerServerId'])->toBe(67890)
|
||||
->and($mail->viewData['errorMessage'])->toBe('Connection timeout');
|
||||
});
|
||||
|
||||
it('generates correct discord content', function () {
|
||||
$notification = new HetznerDeletionFailed(
|
||||
hetznerServerId: 11111,
|
||||
teamId: 1,
|
||||
errorMessage: 'API rate limit exceeded'
|
||||
);
|
||||
|
||||
$discord = $notification->toDiscord();
|
||||
|
||||
expect($discord->title)->toContain('Failed to delete Hetzner server')
|
||||
->and($discord->description)->toContain('#11111')
|
||||
->and($discord->description)->toContain('API rate limit exceeded')
|
||||
->and($discord->description)->toContain('may still exist in your Hetzner Cloud account');
|
||||
});
|
||||
|
||||
it('generates correct telegram content', function () {
|
||||
$notification = new HetznerDeletionFailed(
|
||||
hetznerServerId: 22222,
|
||||
teamId: 1,
|
||||
errorMessage: 'Invalid token'
|
||||
);
|
||||
|
||||
$telegram = $notification->toTelegram();
|
||||
|
||||
expect($telegram)->toBeArray()
|
||||
->and($telegram)->toHaveKey('message')
|
||||
->and($telegram['message'])->toContain('#22222')
|
||||
->and($telegram['message'])->toContain('Invalid token')
|
||||
->and($telegram['message'])->toContain('ACTION REQUIRED');
|
||||
});
|
||||
|
||||
it('generates correct pushover content', function () {
|
||||
$notification = new HetznerDeletionFailed(
|
||||
hetznerServerId: 33333,
|
||||
teamId: 1,
|
||||
errorMessage: 'Network error'
|
||||
);
|
||||
|
||||
$pushover = $notification->toPushover();
|
||||
|
||||
expect($pushover->title)->toBe('Hetzner Server Deletion Failed')
|
||||
->and($pushover->level)->toBe('error')
|
||||
->and($pushover->message)->toContain('#33333')
|
||||
->and($pushover->message)->toContain('Network error');
|
||||
});
|
||||
|
||||
it('generates correct slack content', function () {
|
||||
$notification = new HetznerDeletionFailed(
|
||||
hetznerServerId: 44444,
|
||||
teamId: 1,
|
||||
errorMessage: 'Permission denied'
|
||||
);
|
||||
|
||||
$slack = $notification->toSlack();
|
||||
|
||||
expect($slack->title)->toContain('Hetzner Server Deletion Failed')
|
||||
->and($slack->description)->toContain('#44444')
|
||||
->and($slack->description)->toContain('Permission denied');
|
||||
});
|
||||
Loading…
Reference in a new issue