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:
Serreau Jovann
2026-04-02 22:01:05 +02:00
parent 8ef9711179
commit 4fc14177d8
2 changed files with 161 additions and 0 deletions

View 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;
}
}

View File

@@ -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.
*/