fix: resolve Docker validation race conditions and sudo prefix bug

- Fix sudo prefix bug: Use word boundary matching to prevent 'do' keyword from matching 'docker' commands
- Add ensureProxyNetworksExist() helper to create networks before docker compose up
- Ensure networks exist synchronously before dispatching async proxy startup to prevent race conditions
- Update comprehensive unit tests for sudo parsing (50 tests passing)

This resolves issues where Docker commands failed to execute with sudo on non-root servers and where proxy networks were not created before the proxy container started.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2025-11-27 09:04:42 +01:00
parent e5c7459284
commit 246e3cd8a2
6 changed files with 191 additions and 32 deletions

View File

@@ -108,6 +108,37 @@ function connectProxyToNetworks(Server $server)
return $commands->flatten();
}
/**
* Ensures all required networks exist before docker compose up.
* This must be called BEFORE docker compose up since the compose file declares networks as external.
*
* @param Server $server The server to ensure networks on
* @return \Illuminate\Support\Collection Commands to create networks if they don't exist
*/
function ensureProxyNetworksExist(Server $server)
{
['allNetworks' => $networks] = collectDockerNetworksByServer($server);
if ($server->isSwarm()) {
$commands = $networks->map(function ($network) {
return [
"echo 'Ensuring network $network exists...'",
"docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --driver overlay --attachable $network",
];
});
} else {
$commands = $networks->map(function ($network) {
return [
"echo 'Ensuring network $network exists...'",
"docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --attachable $network",
];
});
}
return $commands->flatten();
}
function extractCustomProxyCommands(Server $server, string $existing_config): array
{
$custom_commands = [];

View File

@@ -23,38 +23,51 @@ function shouldChangeOwnership(string $path): bool
function parseCommandsByLineForSudo(Collection $commands, Server $server): array
{
$commands = $commands->map(function ($line) {
if (
! str(trim($line))->startsWith([
'cd',
'command',
'echo',
'true',
'if',
'fi',
'for',
'do',
'done',
'while',
'until',
'case',
'esac',
'select',
'then',
'else',
'elif',
'break',
'continue',
'#',
])
) {
return "sudo $line";
$trimmedLine = trim($line);
// All bash keywords that should not receive sudo prefix
// Using word boundary matching to avoid prefix collisions (e.g., 'do' vs 'docker', 'if' vs 'ifconfig', 'fi' vs 'find')
$bashKeywords = [
'cd',
'command',
'echo',
'true',
'if',
'fi',
'for',
'done',
'while',
'until',
'case',
'esac',
'select',
'then',
'else',
'elif',
'break',
'continue',
'do',
];
// Special case: comments (no collision risk with '#')
if (str_starts_with($trimmedLine, '#')) {
return $line;
}
if (str(trim($line))->startsWith('if')) {
return str_replace('if', 'if sudo', $line);
// Check all keywords with word boundary matching
// Match keyword followed by space, semicolon, or end of line
foreach ($bashKeywords as $keyword) {
if (preg_match('/^'.preg_quote($keyword, '/').'(\s|;|$)/', $trimmedLine)) {
// Special handling for 'if' - insert sudo after 'if '
if ($keyword === 'if') {
return preg_replace('/^(\s*)if\s+/', '$1if sudo ', $line);
}
return $line;
}
}
return $line;
return "sudo $line";
});
$commands = $commands->map(function ($line) use ($server) {