feat: commande app:cloudflare:clean pour lister les zones et nettoyer les _acme-challenge
src/Command/CloudflareDnsCleanCommand.php (nouveau):
- Commande app:cloudflare:clean avec 3 options:
--list-only: affiche uniquement le tableau des zones (nom, ID, statut, plan)
--zone=domain.fr: filtre sur une seule zone
--dry-run: affiche les records a supprimer sans les supprimer
- Sans option: parcourt toutes les zones, trouve les TXT _acme-challenge
et les supprime via l'API Cloudflare
- Affiche le nombre de records trouves et supprimes par zone
- Affiche le total global a la fin
src/Service/CloudflareService.php:
- listZones(): liste toutes les zones du compte avec pagination
(retourne nom, id, status, plan pour chaque zone)
- deleteDnsRecord(): supprime un enregistrement DNS par zoneId + recordId
via DELETE /zones/{zoneId}/dns_records/{recordId}
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
130
src/Command/CloudflareDnsCleanCommand.php
Normal file
130
src/Command/CloudflareDnsCleanCommand.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\CloudflareService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:cloudflare:clean',
|
||||
description: 'Liste les zones Cloudflare et nettoie les enregistrements _acme-challenge obsoletes',
|
||||
)]
|
||||
class CloudflareDnsCleanCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private CloudflareService $cloudflare,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('zone', null, InputOption::VALUE_REQUIRED, 'Nettoyer uniquement cette zone (nom de domaine)')
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Afficher les records a supprimer sans les supprimer')
|
||||
->addOption('list-only', null, InputOption::VALUE_NONE, 'Lister uniquement les zones sans nettoyer');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Cloudflare DNS - Nettoyage des _acme-challenge');
|
||||
|
||||
if (!$this->cloudflare->isAvailable()) {
|
||||
$io->error('API Cloudflare non disponible. Verifiez CLOUDFLARE_KEY.');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$zones = $this->cloudflare->listZones();
|
||||
|
||||
if ([] === $zones) {
|
||||
$io->warning('Aucune zone trouvee.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->section('Zones Cloudflare ('.\count($zones).')');
|
||||
$zoneTable = [];
|
||||
foreach ($zones as $zone) {
|
||||
$zoneTable[] = [
|
||||
$zone['name'],
|
||||
$zone['id'],
|
||||
$zone['status'],
|
||||
$zone['plan']['name'] ?? '?',
|
||||
];
|
||||
}
|
||||
$io->table(['Domaine', 'Zone ID', 'Statut', 'Plan'], $zoneTable);
|
||||
|
||||
if ($input->getOption('list-only')) {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$filterZone = $input->getOption('zone');
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
$totalDeleted = 0;
|
||||
|
||||
foreach ($zones as $zone) {
|
||||
$zoneName = $zone['name'];
|
||||
$zoneId = $zone['id'];
|
||||
|
||||
if (null !== $filterZone && $zoneName !== $filterZone) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$io->section($zoneName.' ('.$zoneId.')');
|
||||
|
||||
$acmeRecords = $this->cloudflare->getDnsRecordsByType($zoneId, 'TXT');
|
||||
$toDelete = [];
|
||||
|
||||
foreach ($acmeRecords as $record) {
|
||||
$name = $record['name'] ?? '';
|
||||
if (str_contains($name, '_acme-challenge')) {
|
||||
$toDelete[] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
if ([] === $toDelete) {
|
||||
$io->text(' Aucun _acme-challenge trouve.');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$io->text(' '.\count($toDelete).' enregistrement(s) _acme-challenge trouve(s) :');
|
||||
foreach ($toDelete as $record) {
|
||||
$io->text(' - '.$record['name'].' => '.substr($record['content'] ?? '', 0, 50));
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$io->text(' [DRY RUN] Aucune suppression effectuee.');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$deleted = 0;
|
||||
foreach ($toDelete as $record) {
|
||||
if ($this->cloudflare->deleteDnsRecord($zoneId, $record['id'])) {
|
||||
++$deleted;
|
||||
} else {
|
||||
$io->text(' <fg=red>ECHEC</> suppression de '.$record['name']);
|
||||
}
|
||||
}
|
||||
|
||||
$totalDeleted += $deleted;
|
||||
$io->text(' <fg=green>'.$deleted.'</> enregistrement(s) supprime(s).');
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$io->success('Dry run termine. Aucun enregistrement supprime.');
|
||||
} else {
|
||||
$io->success($totalDeleted.' enregistrement(s) _acme-challenge supprime(s) au total.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,37 @@ class CloudflareService
|
||||
return $data['result'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lister toutes les zones du compte.
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function listZones(): array
|
||||
{
|
||||
$allZones = [];
|
||||
$page = 1;
|
||||
|
||||
do {
|
||||
$data = $this->request('GET', '/zones', ['per_page' => 50, 'page' => $page]);
|
||||
$zones = $data['result'] ?? [];
|
||||
$allZones = array_merge($allZones, $zones);
|
||||
$totalPages = $data['result_info']['total_pages'] ?? 1;
|
||||
++$page;
|
||||
} while ($page <= $totalPages);
|
||||
|
||||
return $allZones;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer un enregistrement DNS.
|
||||
*/
|
||||
public function deleteDnsRecord(string $zoneId, string $recordId): bool
|
||||
{
|
||||
$data = $this->request('DELETE', '/zones/'.$zoneId.'/dns_records/'.$recordId);
|
||||
|
||||
return true === ($data['success'] ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifier si l'API est fonctionnelle.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user