Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions src/Database/Manager/MittwaldApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ public function create(): void
// Additionally, even after the user is "ready", the DNS entry for the database host
// (e.g. mysql-xyz.pg-s-xxx.db.project.host) may not be resolvable yet and can flap
// intermittently for several minutes. The TCP connectivity check below catches the
// initial delay; the DNS-to-IP resolution in feature_sync.php::resolveDatabaseHostToIp()
// handles the ongoing flapping by eliminating DNS dependency entirely.
// initial delay, and the immediate hostname-to-IP resolution eliminates DNS dependency
// for all subsequent operations (template rendering, db_sync_tool, TYPO3 CLI, etc.).
$ready = $this->checkForDatabaseReadyStatus(
$responseBody->getUserId(),
(int) get('mittwald_database_wait', 30),
Expand All @@ -108,9 +108,15 @@ public function create(): void
);
}

// Resolve hostname to IP immediately while DNS is known to be working.
// Mittwald DNS entries flap intermittently for several minutes after database creation.
// By resolving now, the .env template will contain the IP address, eliminating DNS
// dependency for db_sync_tool, TYPO3 CLI, and all subsequent remote commands.
$databaseHost = $this->resolveHostnameToIp($database->getHostName());

$user = $this->getDatabaseUser($responseBody->getUserId());

$this->initDatabaseConfiguration($database, $user);
$this->initDatabaseConfiguration($database, $user, $databaseHost);
}

/**
Expand Down Expand Up @@ -229,14 +235,41 @@ private function getDatabaseUser(string $id): MysqlUser
return $response->getBody();
}

private function initDatabaseConfiguration(MySqlDatabase $database, MySqlUser $user): void
private function initDatabaseConfiguration(MySqlDatabase $database, MySqlUser $user, string $databaseHost): void
{
set('database_user', $user->getName());
set('database_name', $database->getName());
set('database_host', $database->getHostName());
set('database_host', $databaseHost);
set('database_password', VarUtility::getDatabasePassword());
}

/**
* Resolves a database hostname to its IP address on the remote server.
*
* This is called immediately after the TCP connectivity check passes, when DNS is
* known to be working. The resolved IP is used in the .env template instead of the
* hostname, bypassing Mittwald's intermittent DNS flapping for all subsequent commands.
*
* Falls back to the original hostname if resolution fails.
*/
private function resolveHostnameToIp(string $hostname): string
{
try {
$resolveCmd = sprintf('echo gethostbyname("%s");', addslashes($hostname));
$ip = trim(run("php -r " . escapeshellarg($resolveCmd)));

if ($ip !== $hostname) {
info("Resolved database host {$hostname} to {$ip}");
return $ip;
}
} catch (\Throwable $e) {
debug("Could not resolve {$hostname} to IP: " . $e->getMessage());
}

debug("Could not resolve {$hostname} to IP, using hostname as fallback.");
return $hostname;
}
Comment on lines 255 to 271
Copy link

@coderabbitai coderabbitai bot Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

gethostbyname() is IPv4-only — document the limitation.

The PHP docs confirm gethostbyname() "returns the IPv4 address or a string containing the unmodified hostname on failure" and only accepts IPv4 addresses, not IPv6. If Mittwald ever introduces an IPv6-only database host, the resolution will silently return the original hostname (the $ip !== $hostname + FILTER_VALIDATE_IP gate correctly handles this), but the DNS-flapping workaround won't apply. Worth noting in the docblock so future maintainers don't assume IPv6 coverage.

📝 Suggested docblock addition
 /**
  * Resolves a database hostname to its IP address on the remote server.
  *
+ * Note: uses PHP's gethostbyname() which resolves to IPv4 only.
+ * IPv6-only hosts will fall back to the original hostname silently.
+ *
  * This is called immediately after the TCP connectivity check passes, when DNS is
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Manager/MittwaldApi.php` around lines 255 - 271, Add a docblock
to the resolveHostnameToIp method explaining that it uses PHP's gethostbyname()
(invoked via the inline PHP command in $resolveCmd) which is IPv4-only and will
return the original hostname on failure (so IPv6-only hosts won't be resolved by
this method); mention the current guard ($ip !== $hostname && filter_var($ip,
FILTER_VALIDATE_IP)) handles this by falling back to the hostname and note the
limitation and potential need for a different resolver (e.g., dns_get_record or
getaddrinfo) if Mittwald introduces IPv6-only DB hosts so future maintainers
don’t assume IPv6 coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Acknowledged. The existing docblock already states "Falls back to the original hostname if resolution fails", which covers this behavior. Mittwald MySQL hosts are IPv4 — adding IPv6 future-proofing notes would be speculative at this point.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@konradmichalik Fair point — the fallback documentation is already there, and if the infrastructure is IPv4 today, explicitly calling out a hypothetical IPv6 scenario adds noise rather than value. Thanks for the context!


private function checkDatabaseHostReachable(string $hostname, int $waitingTime, int $maxRetries): bool
{
$port = (int) get('database_port', 3306);
Expand Down