mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
feature realiability and added coverage. Fail softly
This commit is contained in:
parent
c6e561afc8
commit
35a5a77526
3 changed files with 183 additions and 6 deletions
|
|
@ -4,6 +4,7 @@ namespace App\Actions\Service;
|
|||
|
||||
use App\Models\Service;
|
||||
use App\Services\EdgeProxyRemoteRouteService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
|
|
@ -16,7 +17,16 @@ class StartService
|
|||
public function handle(Service $service, bool $pullLatestImages = false, bool $stopBeforeStart = false)
|
||||
{
|
||||
$service->parse();
|
||||
$edgeRoutingWarnings = app(EdgeProxyRemoteRouteService::class)->syncService($service);
|
||||
$edgeRoutingWarnings = [];
|
||||
try {
|
||||
$edgeRoutingWarnings = app(EdgeProxyRemoteRouteService::class)->syncService($service);
|
||||
} catch (\Throwable $exception) {
|
||||
Log::warning('Failed to sync edge proxy route for service start.', [
|
||||
'service_uuid' => $service->uuid,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
$edgeRoutingWarnings[] = 'Failed to sync edge proxy route configuration. Check edge proxy connectivity and server settings.';
|
||||
}
|
||||
if ($stopBeforeStart) {
|
||||
StopService::run(service: $service, dockerCleanup: false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,21 @@ class EdgeProxyRemoteRouteService
|
|||
$edgeProxyServer = $this->resolveEdgeProxyServer($service);
|
||||
$deploymentServer = $this->resolveDeploymentServer($service);
|
||||
|
||||
if (! $edgeProxyServer instanceof Server || ! $deploymentServer instanceof Server) {
|
||||
if (! $deploymentServer instanceof Server) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! $edgeProxyServer instanceof Server) {
|
||||
if ($deploymentServer->id !== 0) {
|
||||
$warning = sprintf(
|
||||
'Edge proxy route skipped for service %s: edge proxy server (id=0) was not found for the current team.',
|
||||
$service->uuid
|
||||
);
|
||||
$this->logWarning($warning);
|
||||
|
||||
return [$warning];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +126,17 @@ class EdgeProxyRemoteRouteService
|
|||
}
|
||||
|
||||
$config = $this->generateTraefikConfig($service->uuid, $routes);
|
||||
$this->writeRouteFile($edgeProxyServer, $service->uuid, $config);
|
||||
try {
|
||||
$this->writeRouteFile($edgeProxyServer, $service->uuid, $config);
|
||||
} catch (\Throwable $exception) {
|
||||
$warning = sprintf(
|
||||
'Edge proxy route partially applied for service %s: failed to write dynamic route configuration on edge proxy (%s).',
|
||||
$service->uuid,
|
||||
$exception->getMessage()
|
||||
);
|
||||
$this->logWarning($warning);
|
||||
$warnings[] = $warning;
|
||||
}
|
||||
|
||||
return $warnings;
|
||||
}
|
||||
|
|
@ -323,9 +347,9 @@ class EdgeProxyRemoteRouteService
|
|||
];
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
$value = trim((string) $candidate);
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
$normalizedHost = $this->normalizeRemoteHost((string) $candidate);
|
||||
if (! is_null($normalizedHost)) {
|
||||
return $normalizedHost;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -547,4 +571,42 @@ class EdgeProxyRemoteRouteService
|
|||
|
||||
error_log($message);
|
||||
}
|
||||
|
||||
private function normalizeRemoteHost(string $rawHost): ?string
|
||||
{
|
||||
$host = trim($rawHost);
|
||||
if ($host === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allow values like https://10.8.0.15:8080/path and extract only host.
|
||||
if (Str::startsWith($host, ['http://', 'https://'])) {
|
||||
$parsedHost = parse_url($host, PHP_URL_HOST);
|
||||
$host = is_string($parsedHost) ? $parsedHost : '';
|
||||
} elseif (str_contains($host, '/')) {
|
||||
$parsedHost = parse_url('http://'.$host, PHP_URL_HOST);
|
||||
$host = is_string($parsedHost) ? $parsedHost : '';
|
||||
}
|
||||
|
||||
$host = trim($host, '[]');
|
||||
if ($host === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Drop accidental host:port values so published compose port remains authoritative.
|
||||
if (str_contains($host, ':') && ! filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$parsedHost = parse_url('http://'.$host, PHP_URL_HOST);
|
||||
$host = is_string($parsedHost) ? $parsedHost : '';
|
||||
}
|
||||
|
||||
if ($host === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
return '['.$host.']';
|
||||
}
|
||||
|
||||
return $host;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,3 +267,108 @@ YAML;
|
|||
->and(implode("\n", $manager->calls[0]['commands']))->toContain('/tmp/proxy/dynamic/service-remote-service-without-tunnel-host.yaml')
|
||||
->and(implode("\n", $manager->calls[0]['commands']))->not->toContain('tee');
|
||||
});
|
||||
|
||||
it('returns warning instead of throwing when edge route file write fails', function () {
|
||||
$manager = new class extends EdgeProxyRemoteRouteService
|
||||
{
|
||||
public array $calls = [];
|
||||
|
||||
protected function runRemoteCommands(Server $server, array $commands, bool $throwError = true): ?string
|
||||
{
|
||||
$this->calls[] = [
|
||||
'commands' => $commands,
|
||||
'throw_error' => $throwError,
|
||||
];
|
||||
|
||||
if (str_contains(implode("\n", $commands), 'tee')) {
|
||||
throw new RuntimeException('edge ssh unavailable');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$edgeProxyServer = Mockery::mock(Server::class)->makePartial();
|
||||
$edgeProxyServer->id = 0;
|
||||
$edgeProxyServer->shouldReceive('proxyType')->andReturn('TRAEFIK');
|
||||
$edgeProxyServer->shouldReceive('proxyPath')->andReturn('/tmp/proxy');
|
||||
|
||||
$deploymentServer = Mockery::mock(Server::class)->makePartial();
|
||||
$deploymentServer->id = 14;
|
||||
$deploymentServer->ip = '10.8.0.19';
|
||||
$deploymentServer->proxy = ['type' => 'NONE'];
|
||||
|
||||
$service = new Service;
|
||||
$service->uuid = 'service-write-failure';
|
||||
$service->docker_compose_raw = <<<'YAML'
|
||||
services:
|
||||
app:
|
||||
ports:
|
||||
- "9010:3000"
|
||||
YAML;
|
||||
|
||||
$application = new ServiceApplication;
|
||||
$application->name = 'app';
|
||||
$application->fqdn = 'https://write-failure.example.com:3000';
|
||||
|
||||
$service->setRelation('applications', collect([$application]));
|
||||
$application->setRelation('service', $service);
|
||||
|
||||
$warnings = $manager->syncServiceWithServers($service, $edgeProxyServer, $deploymentServer);
|
||||
|
||||
expect($warnings)->not->toBeEmpty()
|
||||
->and(collect($warnings)->contains(fn (string $warning) => str_contains($warning, 'failed to write dynamic route configuration')))
|
||||
->and($manager->calls)->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('normalizes remote tunnel host values before generating upstream url', function () {
|
||||
$manager = new class extends EdgeProxyRemoteRouteService
|
||||
{
|
||||
public array $calls = [];
|
||||
|
||||
protected function runRemoteCommands(Server $server, array $commands, bool $throwError = true): ?string
|
||||
{
|
||||
$this->calls[] = [
|
||||
'commands' => $commands,
|
||||
'throw_error' => $throwError,
|
||||
];
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$edgeProxyServer = Mockery::mock(Server::class)->makePartial();
|
||||
$edgeProxyServer->id = 0;
|
||||
$edgeProxyServer->shouldReceive('proxyType')->andReturn('TRAEFIK');
|
||||
$edgeProxyServer->shouldReceive('proxyPath')->andReturn('/tmp/proxy');
|
||||
|
||||
$deploymentServer = Mockery::mock(Server::class)->makePartial();
|
||||
$deploymentServer->id = 15;
|
||||
$deploymentServer->ip = '';
|
||||
$deploymentServer->proxy = ['type' => 'NONE', 'tunnel_host' => 'https://10.8.0.20:9443/path'];
|
||||
|
||||
$service = new Service;
|
||||
$service->uuid = 'service-normalized-host';
|
||||
$service->docker_compose_raw = <<<'YAML'
|
||||
services:
|
||||
app:
|
||||
ports:
|
||||
- "9010:3000"
|
||||
YAML;
|
||||
|
||||
$application = new ServiceApplication;
|
||||
$application->name = 'app';
|
||||
$application->fqdn = 'https://normalized.example.com:3000';
|
||||
|
||||
$service->setRelation('applications', collect([$application]));
|
||||
$application->setRelation('service', $service);
|
||||
|
||||
$warnings = $manager->syncServiceWithServers($service, $edgeProxyServer, $deploymentServer);
|
||||
expect($warnings)->toBe([]);
|
||||
|
||||
preg_match("/echo '([^']+)' \\| base64 -d/", $manager->calls[0]['commands'][1], $payloadMatches);
|
||||
$payload = base64_decode($payloadMatches[1]);
|
||||
|
||||
expect($payload)->toContain('http://10.8.0.20:9010')
|
||||
->and($payload)->not->toContain('9443');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue