mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
fixed applications, tested with minecraft server
This commit is contained in:
parent
31b10e7f74
commit
3e24a8075d
8 changed files with 1413 additions and 7 deletions
|
|
@ -4,6 +4,7 @@ namespace App\Actions\Service;
|
|||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Models\Service;
|
||||
use App\Services\EdgeProxyRemotePortForwardService;
|
||||
use App\Services\EdgeProxyRemoteRouteService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
|
@ -64,6 +65,14 @@ class DeleteService
|
|||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
try {
|
||||
app(EdgeProxyRemotePortForwardService::class)->deleteService($service);
|
||||
} catch (\Throwable $exception) {
|
||||
Log::warning('Failed to delete edge port proxy for service.', [
|
||||
'service_uuid' => $service->uuid,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($deleteConfigurations) {
|
||||
$service->deleteConfigurations();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Actions\Service;
|
||||
|
||||
use App\Models\Service;
|
||||
use App\Services\EdgeProxyRemotePortForwardService;
|
||||
use App\Services\EdgeProxyRemoteRouteService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
|
@ -18,6 +19,7 @@ class StartService
|
|||
{
|
||||
$service->parse();
|
||||
$edgeRoutingWarnings = [];
|
||||
$edgePortForwardWarnings = [];
|
||||
try {
|
||||
$edgeRoutingWarnings = app(EdgeProxyRemoteRouteService::class)->syncService($service);
|
||||
} catch (\Throwable $exception) {
|
||||
|
|
@ -27,6 +29,15 @@ class StartService
|
|||
]);
|
||||
$edgeRoutingWarnings[] = 'Failed to sync edge proxy route configuration. Check edge proxy connectivity and server settings.';
|
||||
}
|
||||
try {
|
||||
$edgePortForwardWarnings = app(EdgeProxyRemotePortForwardService::class)->syncService($service);
|
||||
} catch (\Throwable $exception) {
|
||||
Log::warning('Failed to sync edge port forwarding for service start.', [
|
||||
'service_uuid' => $service->uuid,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
$edgePortForwardWarnings[] = 'Failed to sync edge port forwarding configuration. Check edge proxy connectivity, published ports, and server settings.';
|
||||
}
|
||||
if ($stopBeforeStart) {
|
||||
StopService::run(service: $service, dockerCleanup: false);
|
||||
}
|
||||
|
|
@ -38,6 +49,9 @@ class StartService
|
|||
foreach ($edgeRoutingWarnings as $warning) {
|
||||
$commands[] = 'echo '.escapeshellarg("Edge proxy routing warning: {$warning}");
|
||||
}
|
||||
foreach ($edgePortForwardWarnings as $warning) {
|
||||
$commands[] = 'echo '.escapeshellarg("Edge port forwarding warning: {$warning}");
|
||||
}
|
||||
// Ensure .env exists in the correct directory before docker compose tries to load it
|
||||
// This is defensive programming - saveComposeConfigs() already creates it,
|
||||
// but we guarantee it here in case of any edge cases or manual deployments
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use App\Models\StandaloneDocker;
|
|||
use App\Models\SwarmDocker;
|
||||
use App\Notifications\Application\DeploymentFailed;
|
||||
use App\Notifications\Application\DeploymentSuccess;
|
||||
use App\Services\EdgeProxyRemotePortForwardService;
|
||||
use App\Services\EdgeProxyRemoteRouteService;
|
||||
use App\Traits\EnvironmentVariableAnalyzer;
|
||||
use App\Traits\ExecuteRemoteCommand;
|
||||
|
|
@ -525,6 +526,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
'stderr'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$edgePortForwardWarnings = app(EdgeProxyRemotePortForwardService::class)
|
||||
->syncApplicationOnDeploymentServer($this->application, $this->mainServer);
|
||||
foreach ($edgePortForwardWarnings as $warning) {
|
||||
$this->application_deployment_queue->addLogEntry("Edge port forwarding warning: {$warning}", 'stderr');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning('Failed to sync edge port forwarding for application deployment '.$this->deployment_uuid.': '.$e->getMessage());
|
||||
$this->application_deployment_queue->addLogEntry(
|
||||
'Edge port forwarding warning: Failed to sync edge port forwarding configuration. Check master domain server settings, published ports, and edge proxy connectivity.',
|
||||
'stderr'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ use App\Actions\Database\StopDatabase;
|
|||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Actions\Service\DeleteService;
|
||||
use App\Actions\Service\StopService;
|
||||
use App\Services\EdgeProxyRemoteRouteService;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Service;
|
||||
|
|
@ -19,6 +18,8 @@ use App\Models\StandaloneMongodb;
|
|||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Services\EdgeProxyRemotePortForwardService;
|
||||
use App\Services\EdgeProxyRemoteRouteService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
|
@ -109,6 +110,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
|||
} catch (\Throwable $e) {
|
||||
\Log::warning('Failed to delete edge proxy route file for application '.$this->resource->uuid.': '.$e->getMessage());
|
||||
}
|
||||
try {
|
||||
app(EdgeProxyRemotePortForwardService::class)->deleteApplication($this->resource);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning('Failed to delete edge port proxy for application '.$this->resource->uuid.': '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->resource->forceDelete();
|
||||
|
|
|
|||
868
app/Services/EdgeProxyRemotePortForwardService.php
Normal file
868
app/Services/EdgeProxyRemotePortForwardService.php
Normal file
|
|
@ -0,0 +1,868 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Support\Collection;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class EdgeProxyRemotePortForwardService
|
||||
{
|
||||
private const array RESERVED_EDGE_PORTS = [80, 443];
|
||||
|
||||
public function syncService(Service $service): array
|
||||
{
|
||||
$deploymentServer = $this->resolveServiceDeploymentServer($service);
|
||||
if (! $deploymentServer instanceof Server) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->syncServiceWithServers(
|
||||
$service,
|
||||
$this->resolveEdgeProxyServerByTeamId($this->extractServiceTeamId($service)),
|
||||
$deploymentServer
|
||||
);
|
||||
}
|
||||
|
||||
public function syncServiceWithServers(Service $service, ?Server $edgeProxyServer, Server $deploymentServer): array
|
||||
{
|
||||
return $this->syncResourcePortProxy(
|
||||
resourceType: 'service',
|
||||
resourceUuid: $service->uuid,
|
||||
edgeProxyServer: $edgeProxyServer,
|
||||
deploymentServer: $deploymentServer,
|
||||
publishedPortMappings: $this->servicePublishedPortMappings($service)
|
||||
);
|
||||
}
|
||||
|
||||
public function syncApplication(Application $application): array
|
||||
{
|
||||
$deploymentServer = $this->resolveApplicationDeploymentServer($application);
|
||||
if (! $deploymentServer instanceof Server) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->syncApplicationWithServers(
|
||||
$application,
|
||||
$this->resolveEdgeProxyServerByTeamId($this->extractApplicationTeamId($application)),
|
||||
$deploymentServer
|
||||
);
|
||||
}
|
||||
|
||||
public function syncApplicationOnDeploymentServer(Application $application, Server $deploymentServer): array
|
||||
{
|
||||
return $this->syncApplicationWithServers(
|
||||
$application,
|
||||
$this->resolveEdgeProxyServerByTeamId($this->extractApplicationTeamId($application)),
|
||||
$deploymentServer
|
||||
);
|
||||
}
|
||||
|
||||
public function syncApplicationWithServers(Application $application, ?Server $edgeProxyServer, Server $deploymentServer): array
|
||||
{
|
||||
return $this->syncResourcePortProxy(
|
||||
resourceType: 'application',
|
||||
resourceUuid: $application->uuid,
|
||||
edgeProxyServer: $edgeProxyServer,
|
||||
deploymentServer: $deploymentServer,
|
||||
publishedPortMappings: $this->applicationPublishedPortMappings($application)
|
||||
);
|
||||
}
|
||||
|
||||
public function deleteService(Service $service): void
|
||||
{
|
||||
$edgeProxyServer = $this->resolveEdgeProxyServerByTeamId($this->extractServiceTeamId($service));
|
||||
if (! $edgeProxyServer instanceof Server) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->deleteServiceWithServer($service, $edgeProxyServer);
|
||||
}
|
||||
|
||||
public function deleteServiceWithServer(Service $service, Server $edgeProxyServer): void
|
||||
{
|
||||
$this->deleteResourcePortProxy($edgeProxyServer, 'service', $service->uuid);
|
||||
}
|
||||
|
||||
public function deleteApplication(Application $application): void
|
||||
{
|
||||
$edgeProxyServer = $this->resolveEdgeProxyServerByTeamId($this->extractApplicationTeamId($application));
|
||||
if (! $edgeProxyServer instanceof Server) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->deleteApplicationWithServer($application, $edgeProxyServer);
|
||||
}
|
||||
|
||||
public function deleteApplicationWithServer(Application $application, Server $edgeProxyServer): void
|
||||
{
|
||||
$this->deleteResourcePortProxy($edgeProxyServer, 'application', $application->uuid);
|
||||
}
|
||||
|
||||
protected function runRemoteCommands(Server $server, array $commands, bool $throwError = true): ?string
|
||||
{
|
||||
return instant_remote_process($commands, $server, $throwError);
|
||||
}
|
||||
|
||||
protected function resolveEdgeProxyServerByTeamId(?int $teamId): ?Server
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Server::query()
|
||||
->where('team_id', $teamId)
|
||||
->whereRelation('settings', 'is_master_domain_router_enabled', true)
|
||||
->orderBy('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
protected function logWarning(string $message): void
|
||||
{
|
||||
if (app()->bound('log')) {
|
||||
app('log')->warning($message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
error_log($message);
|
||||
}
|
||||
|
||||
private function syncResourcePortProxy(
|
||||
string $resourceType,
|
||||
string $resourceUuid,
|
||||
?Server $edgeProxyServer,
|
||||
Server $deploymentServer,
|
||||
Collection $publishedPortMappings
|
||||
): array {
|
||||
if (! $edgeProxyServer instanceof Server) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($edgeProxyServer->id === $deploymentServer->id) {
|
||||
$this->deleteResourcePortProxy($edgeProxyServer, $resourceType, $resourceUuid);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($publishedPortMappings->isEmpty()) {
|
||||
$this->deleteResourcePortProxy($edgeProxyServer, $resourceType, $resourceUuid);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$warnings = [];
|
||||
$publishedPortMappings = $this->filterReservedEdgePorts($resourceType, $resourceUuid, $publishedPortMappings, $warnings);
|
||||
if ($publishedPortMappings->isEmpty()) {
|
||||
$this->deleteResourcePortProxy($edgeProxyServer, $resourceType, $resourceUuid);
|
||||
|
||||
foreach ($warnings as $warning) {
|
||||
$this->logWarning($warning);
|
||||
}
|
||||
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
$remoteHost = $this->resolveRemoteHost($deploymentServer);
|
||||
if (is_null($remoteHost)) {
|
||||
$warning = sprintf(
|
||||
'Edge port forwarding skipped for %s %s: remote host is missing. Configure a tunnel host (proxy.wireguard_ip/proxy.wg_ip/proxy.tunnel_ip/proxy.tunnel_host) or set the server IP/domain.',
|
||||
$resourceType,
|
||||
$resourceUuid
|
||||
);
|
||||
$this->logWarning($warning);
|
||||
$this->deleteResourcePortProxy($edgeProxyServer, $resourceType, $resourceUuid);
|
||||
$warnings[] = $warning;
|
||||
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->writeResourcePortProxy($edgeProxyServer, $resourceType, $resourceUuid, $remoteHost, $publishedPortMappings);
|
||||
} catch (\Throwable $exception) {
|
||||
$warning = sprintf(
|
||||
'Edge port forwarding warning for %s %s: failed to start edge port proxy (%s). Check whether one of the published ports is already in use on the edge server.',
|
||||
$resourceType,
|
||||
$resourceUuid,
|
||||
$exception->getMessage()
|
||||
);
|
||||
$this->logWarning($warning);
|
||||
$warnings[] = $warning;
|
||||
}
|
||||
|
||||
foreach ($warnings as $warning) {
|
||||
$this->logWarning($warning);
|
||||
}
|
||||
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
private function servicePublishedPortMappings(Service $service): Collection
|
||||
{
|
||||
return $this->composePublishedPortMappings(
|
||||
$this->parseServiceCompose($service),
|
||||
$this->serviceEnvironmentMap($service)
|
||||
);
|
||||
}
|
||||
|
||||
private function applicationPublishedPortMappings(Application $application): Collection
|
||||
{
|
||||
$environmentMap = $this->applicationEnvironmentMap($application);
|
||||
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
return $this->composePublishedPortMappings(
|
||||
$this->parseApplicationCompose($application),
|
||||
$environmentMap
|
||||
);
|
||||
}
|
||||
|
||||
return $this->parsePublishedPortMappings($application->ports_mappings_array ?? [], $environmentMap);
|
||||
}
|
||||
|
||||
private function composePublishedPortMappings(array $compose, array $environmentMap): Collection
|
||||
{
|
||||
$services = data_get($compose, 'services', []);
|
||||
if (! is_array($services) || empty($services)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return collect($services)
|
||||
->filter(fn (mixed $serviceConfig) => is_array($serviceConfig))
|
||||
->flatMap(function (array $serviceConfig) use ($environmentMap) {
|
||||
return $this->parsePublishedPortMappings(
|
||||
(array) data_get($serviceConfig, 'ports', []),
|
||||
$this->mergeComposeEnvironmentMap($serviceConfig, $environmentMap)
|
||||
);
|
||||
})
|
||||
->unique(fn (array $mapping) => $mapping['protocol'].':'.$mapping['published'])
|
||||
->sortBy(fn (array $mapping) => sprintf('%05d:%s', $mapping['published'], $mapping['protocol']))
|
||||
->values();
|
||||
}
|
||||
|
||||
private function parsePublishedPortMappings(array $ports, array $environmentMap): Collection
|
||||
{
|
||||
$mappings = collect();
|
||||
|
||||
foreach ($ports as $portDefinition) {
|
||||
if (is_array($portDefinition)) {
|
||||
$protocol = strtolower((string) data_get($portDefinition, 'protocol', 'tcp'));
|
||||
if (! in_array($protocol, ['tcp', 'udp'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$published = $this->resolvePortValue(data_get($portDefinition, 'published'), $environmentMap);
|
||||
if (! is_null($published)) {
|
||||
$mappings->push([
|
||||
'published' => $published,
|
||||
'protocol' => $protocol,
|
||||
]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($portDefinition) || is_int($portDefinition)) {
|
||||
$mapping = $this->parsePublishedPortMappingFromString((string) $portDefinition, $environmentMap);
|
||||
if (! is_null($mapping) && ! is_null($mapping['published'])) {
|
||||
$mappings->push($mapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $mappings->values();
|
||||
}
|
||||
|
||||
private function parsePublishedPortMappingFromString(string $portDefinition, array $environmentMap): ?array
|
||||
{
|
||||
$normalizedPortDefinition = trim($portDefinition);
|
||||
if ($normalizedPortDefinition === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$protocol = 'tcp';
|
||||
if (preg_match('/\/(tcp|udp)$/i', $normalizedPortDefinition, $protocolMatches)) {
|
||||
$protocol = strtolower($protocolMatches[1]);
|
||||
$normalizedPortDefinition = preg_replace('/\/(tcp|udp)$/i', '', $normalizedPortDefinition) ?? $normalizedPortDefinition;
|
||||
}
|
||||
|
||||
if (! str_contains($normalizedPortDefinition, ':')) {
|
||||
return [
|
||||
'published' => null,
|
||||
'protocol' => $protocol,
|
||||
];
|
||||
}
|
||||
|
||||
$segments = $this->splitPortDefinitionSegments($normalizedPortDefinition);
|
||||
if (count($segments) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
array_pop($segments);
|
||||
$publishedPort = $this->resolvePortValue(array_pop($segments), $environmentMap);
|
||||
|
||||
return [
|
||||
'published' => $publishedPort,
|
||||
'protocol' => $protocol,
|
||||
];
|
||||
}
|
||||
|
||||
private function filterReservedEdgePorts(string $resourceType, string $resourceUuid, Collection $publishedPortMappings, array &$warnings): Collection
|
||||
{
|
||||
return $publishedPortMappings
|
||||
->reject(function (array $mapping) use ($resourceType, $resourceUuid, &$warnings) {
|
||||
if (! in_array($mapping['published'], self::RESERVED_EDGE_PORTS, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$warnings[] = sprintf(
|
||||
'Edge port forwarding skipped for %s %s on port %d/%s: this port is reserved on the edge server for HTTP/HTTPS proxying.',
|
||||
$resourceType,
|
||||
$resourceUuid,
|
||||
$mapping['published'],
|
||||
$mapping['protocol']
|
||||
);
|
||||
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
private function writeResourcePortProxy(
|
||||
Server $edgeProxyServer,
|
||||
string $resourceType,
|
||||
string $resourceUuid,
|
||||
string $remoteHost,
|
||||
Collection $publishedPortMappings
|
||||
): void {
|
||||
$containerName = $this->proxyContainerName($resourceType, $resourceUuid);
|
||||
$configurationDirectory = $this->configurationDirectory($resourceType, $resourceUuid);
|
||||
$escapedConfigurationDirectory = escapeshellarg($configurationDirectory);
|
||||
$escapedNginxPath = escapeshellarg($configurationDirectory.'/nginx.conf');
|
||||
$escapedComposePath = escapeshellarg($configurationDirectory.'/docker-compose.yaml');
|
||||
$escapedContainerName = escapeshellarg($containerName);
|
||||
|
||||
$nginxConfig = base64_encode($this->generateNginxStreamConfig($remoteHost, $publishedPortMappings));
|
||||
$dockerCompose = base64_encode(Yaml::dump(
|
||||
$this->generateDockerCompose($containerName, $configurationDirectory, $publishedPortMappings),
|
||||
6,
|
||||
2
|
||||
));
|
||||
|
||||
$this->runRemoteCommands($edgeProxyServer, [
|
||||
"docker rm -f $escapedContainerName >/dev/null 2>&1 || true",
|
||||
"mkdir -p $escapedConfigurationDirectory",
|
||||
"echo '{$nginxConfig}' | base64 -d | tee $escapedNginxPath > /dev/null",
|
||||
"echo '{$dockerCompose}' | base64 -d | tee $escapedComposePath > /dev/null",
|
||||
"docker compose --project-directory $escapedConfigurationDirectory pull",
|
||||
"docker compose --project-directory $escapedConfigurationDirectory up -d",
|
||||
]);
|
||||
}
|
||||
|
||||
private function deleteResourcePortProxy(Server $edgeProxyServer, string $resourceType, string $resourceUuid): void
|
||||
{
|
||||
$escapedContainerName = escapeshellarg($this->proxyContainerName($resourceType, $resourceUuid));
|
||||
|
||||
$this->runRemoteCommands($edgeProxyServer, [
|
||||
"docker rm -f $escapedContainerName",
|
||||
], false);
|
||||
}
|
||||
|
||||
private function generateNginxStreamConfig(string $remoteHost, Collection $publishedPortMappings): string
|
||||
{
|
||||
$serverBlocks = $publishedPortMappings
|
||||
->map(function (array $mapping) use ($remoteHost) {
|
||||
$listen = (string) $mapping['published'];
|
||||
if ($mapping['protocol'] === 'udp') {
|
||||
$listen .= ' udp';
|
||||
}
|
||||
|
||||
return <<<EOF
|
||||
server {
|
||||
listen {$listen};
|
||||
proxy_pass {$remoteHost}:{$mapping['published']};
|
||||
}
|
||||
EOF;
|
||||
})
|
||||
->implode("\n");
|
||||
|
||||
return <<<EOF
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
stream {
|
||||
{$serverBlocks}
|
||||
}
|
||||
EOF;
|
||||
}
|
||||
|
||||
private function generateDockerCompose(string $containerName, string $configurationDirectory, Collection $publishedPortMappings): array
|
||||
{
|
||||
$ports = $publishedPortMappings
|
||||
->map(function (array $mapping) {
|
||||
$port = "{$mapping['published']}:{$mapping['published']}";
|
||||
|
||||
return $mapping['protocol'] === 'udp' ? "{$port}/udp" : $port;
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [
|
||||
'services' => [
|
||||
$containerName => [
|
||||
'image' => 'nginx:stable-alpine',
|
||||
'container_name' => $containerName,
|
||||
'restart' => RESTART_MODE,
|
||||
'ports' => $ports,
|
||||
'volumes' => [[
|
||||
'type' => 'bind',
|
||||
'source' => $configurationDirectory.'/nginx.conf',
|
||||
'target' => '/etc/nginx/nginx.conf',
|
||||
]],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
'stat /etc/nginx/nginx.conf || exit 1',
|
||||
],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 3,
|
||||
'start_period' => '1s',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function configurationDirectory(string $resourceType, string $resourceUuid): string
|
||||
{
|
||||
return match ($resourceType) {
|
||||
'application' => application_configuration_dir()."/{$resourceUuid}/edge-port-proxy",
|
||||
'service' => service_configuration_dir()."/{$resourceUuid}/edge-port-proxy",
|
||||
default => base_configuration_dir()."/{$resourceType}/{$resourceUuid}/edge-port-proxy",
|
||||
};
|
||||
}
|
||||
|
||||
private function proxyContainerName(string $resourceType, string $resourceUuid): string
|
||||
{
|
||||
return "{$resourceType}-{$resourceUuid}-edge-port-proxy";
|
||||
}
|
||||
|
||||
private function resolveServiceDeploymentServer(Service $service): ?Server
|
||||
{
|
||||
$server = data_get($service, 'server');
|
||||
if ($server instanceof Server) {
|
||||
return $server;
|
||||
}
|
||||
|
||||
$server = data_get($service, 'destination.server');
|
||||
if ($server instanceof Server) {
|
||||
return $server;
|
||||
}
|
||||
|
||||
if ($service->exists && ! is_null($service->server_id)) {
|
||||
return Server::query()->find($service->server_id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveApplicationDeploymentServer(Application $application): ?Server
|
||||
{
|
||||
$server = data_get($application, 'server');
|
||||
if ($server instanceof Server) {
|
||||
return $server;
|
||||
}
|
||||
|
||||
$server = data_get($application, 'destination.server');
|
||||
if ($server instanceof Server) {
|
||||
return $server;
|
||||
}
|
||||
|
||||
if ($application->exists) {
|
||||
$application->loadMissing('destination.server');
|
||||
|
||||
$server = data_get($application, 'destination.server');
|
||||
if ($server instanceof Server) {
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractServiceTeamId(Service $service): ?int
|
||||
{
|
||||
$teamId = data_get($service, 'environment.project.team_id');
|
||||
if (! is_null($teamId)) {
|
||||
return (int) $teamId;
|
||||
}
|
||||
|
||||
if ($service->exists) {
|
||||
$service->loadMissing('environment.project');
|
||||
$teamId = data_get($service, 'environment.project.team_id');
|
||||
if (! is_null($teamId)) {
|
||||
return (int) $teamId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractApplicationTeamId(Application $application): ?int
|
||||
{
|
||||
$teamId = data_get($application, 'environment.project.team_id');
|
||||
if (! is_null($teamId)) {
|
||||
return (int) $teamId;
|
||||
}
|
||||
|
||||
if ($application->exists) {
|
||||
$application->loadMissing('environment.project');
|
||||
$teamId = data_get($application, 'environment.project.team_id');
|
||||
if (! is_null($teamId)) {
|
||||
return (int) $teamId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveRemoteHost(Server $deploymentServer): ?string
|
||||
{
|
||||
$candidates = [
|
||||
data_get($deploymentServer, 'proxy.wireguard_ip'),
|
||||
data_get($deploymentServer, 'proxy.wg_ip'),
|
||||
data_get($deploymentServer, 'proxy.tunnel_ip'),
|
||||
data_get($deploymentServer, 'proxy.tunnel_host'),
|
||||
data_get($deploymentServer, 'proxy.tunnel_domain'),
|
||||
data_get($deploymentServer, 'ip'),
|
||||
];
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
$normalizedHost = $this->normalizeRemoteHost((string) $candidate);
|
||||
if (! is_null($normalizedHost)) {
|
||||
return $normalizedHost;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parseServiceCompose(Service $service): array
|
||||
{
|
||||
if (blank($service->docker_compose_raw)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$parsedCompose = Yaml::parse($service->docker_compose_raw);
|
||||
|
||||
return is_array($parsedCompose) ? $parsedCompose : [];
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function serviceEnvironmentMap(Service $service): array
|
||||
{
|
||||
if ($service->relationLoaded('environment_variables')) {
|
||||
return $service->environment_variables
|
||||
->mapWithKeys(fn ($environmentVariable) => [$environmentVariable->key => (string) $environmentVariable->value])
|
||||
->all();
|
||||
}
|
||||
|
||||
if (! $service->exists) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $service->environment_variables()
|
||||
->get()
|
||||
->mapWithKeys(fn ($environmentVariable) => [$environmentVariable->key => (string) $environmentVariable->value])
|
||||
->all();
|
||||
}
|
||||
|
||||
private function parseApplicationCompose(Application $application): array
|
||||
{
|
||||
if ($application->build_pack !== 'dockercompose') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rawCompose = filled($application->docker_compose_raw)
|
||||
? $application->docker_compose_raw
|
||||
: $application->docker_compose;
|
||||
|
||||
if (blank($rawCompose)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$parsedCompose = Yaml::parse($rawCompose);
|
||||
|
||||
return is_array($parsedCompose) ? $parsedCompose : [];
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function applicationEnvironmentMap(Application $application): array
|
||||
{
|
||||
if ($application->relationLoaded('environment_variables')) {
|
||||
return $application->environment_variables
|
||||
->mapWithKeys(fn ($environmentVariable) => [$environmentVariable->key => (string) $environmentVariable->value])
|
||||
->all();
|
||||
}
|
||||
|
||||
if (! $application->exists) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $application->environment_variables()
|
||||
->get()
|
||||
->mapWithKeys(fn ($environmentVariable) => [$environmentVariable->key => (string) $environmentVariable->value])
|
||||
->all();
|
||||
}
|
||||
|
||||
private function mergeComposeEnvironmentMap(array $serviceConfig, array $environmentMap): array
|
||||
{
|
||||
$resolvedEnvironmentMap = $environmentMap;
|
||||
|
||||
foreach ($this->composeEnvironmentDefinitions($serviceConfig) as $environmentKey => $rawValue) {
|
||||
if (
|
||||
array_key_exists($environmentKey, $environmentMap) &&
|
||||
trim((string) $environmentMap[$environmentKey]) !== ''
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resolvedValue = $this->resolveEnvironmentValue($rawValue, $resolvedEnvironmentMap);
|
||||
if (! is_null($resolvedValue)) {
|
||||
$resolvedEnvironmentMap[$environmentKey] = $resolvedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $resolvedEnvironmentMap;
|
||||
}
|
||||
|
||||
private function composeEnvironmentDefinitions(array $serviceConfig): array
|
||||
{
|
||||
$environmentDefinitions = [];
|
||||
$environment = data_get($serviceConfig, 'environment', []);
|
||||
|
||||
if (! is_array($environment)) {
|
||||
return $environmentDefinitions;
|
||||
}
|
||||
|
||||
foreach ($environment as $key => $value) {
|
||||
if (is_int($key)) {
|
||||
$environmentPair = explode('=', (string) $value, 2);
|
||||
if (count($environmentPair) !== 2 || trim($environmentPair[0]) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$environmentDefinitions[trim($environmentPair[0])] = $environmentPair[1];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! is_string($key) || trim($key) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! is_scalar($value) && ! is_null($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$environmentDefinitions[trim($key)] = $value;
|
||||
}
|
||||
|
||||
return $environmentDefinitions;
|
||||
}
|
||||
|
||||
private function resolveEnvironmentValue(mixed $rawValue, array $environmentMap): ?string
|
||||
{
|
||||
if (is_null($rawValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_bool($rawValue)) {
|
||||
return $rawValue ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_int($rawValue) || is_float($rawValue)) {
|
||||
return (string) $rawValue;
|
||||
}
|
||||
|
||||
$normalizedValue = trim((string) $rawValue);
|
||||
if ($normalizedValue === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
preg_match(
|
||||
'/^\$\{([A-Za-z_][A-Za-z0-9_]*)(?:(:?[-?])([^}]*))?\}$/',
|
||||
$normalizedValue,
|
||||
$matches
|
||||
)
|
||||
) {
|
||||
$environmentKey = $matches[1];
|
||||
$defaultValue = trim((string) ($matches[3] ?? ''));
|
||||
$resolvedEnvironmentValue = $environmentMap[$environmentKey] ?? null;
|
||||
|
||||
if (! is_null($resolvedEnvironmentValue) && trim((string) $resolvedEnvironmentValue) !== '') {
|
||||
return trim((string) $resolvedEnvironmentValue);
|
||||
}
|
||||
|
||||
return $defaultValue !== '' ? $defaultValue : null;
|
||||
}
|
||||
|
||||
if (preg_match('/^\$([A-Za-z_][A-Za-z0-9_]*)$/', $normalizedValue, $matches)) {
|
||||
$environmentKey = $matches[1];
|
||||
$resolvedEnvironmentValue = $environmentMap[$environmentKey] ?? null;
|
||||
|
||||
if (! is_null($resolvedEnvironmentValue) && trim((string) $resolvedEnvironmentValue) !== '') {
|
||||
return trim((string) $resolvedEnvironmentValue);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (array_key_exists($normalizedValue, $environmentMap)) {
|
||||
$resolvedEnvironmentValue = trim((string) $environmentMap[$normalizedValue]);
|
||||
|
||||
return $resolvedEnvironmentValue !== '' ? $resolvedEnvironmentValue : null;
|
||||
}
|
||||
|
||||
return $normalizedValue;
|
||||
}
|
||||
|
||||
private function splitPortDefinitionSegments(string $portDefinition): array
|
||||
{
|
||||
$segments = [];
|
||||
$currentSegment = '';
|
||||
$braceDepth = 0;
|
||||
$length = strlen($portDefinition);
|
||||
|
||||
for ($index = 0; $index < $length; $index++) {
|
||||
$character = $portDefinition[$index];
|
||||
|
||||
if ($character === '{') {
|
||||
$braceDepth++;
|
||||
} elseif ($character === '}' && $braceDepth > 0) {
|
||||
$braceDepth--;
|
||||
}
|
||||
|
||||
if ($character === ':' && $braceDepth === 0) {
|
||||
$segments[] = $currentSegment;
|
||||
$currentSegment = '';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentSegment .= $character;
|
||||
}
|
||||
|
||||
$segments[] = $currentSegment;
|
||||
|
||||
return $segments;
|
||||
}
|
||||
|
||||
private function resolvePortValue(mixed $rawPortValue, array $environmentMap): ?int
|
||||
{
|
||||
if (is_int($rawPortValue)) {
|
||||
return $rawPortValue;
|
||||
}
|
||||
|
||||
$normalizedPortValue = trim((string) $rawPortValue);
|
||||
if ($normalizedPortValue === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^\d+\s*-\s*\d+$/', $normalizedPortValue) === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^\d+$/', $normalizedPortValue)) {
|
||||
return (int) $normalizedPortValue;
|
||||
}
|
||||
|
||||
if (
|
||||
preg_match(
|
||||
'/^\$\{([A-Za-z_][A-Za-z0-9_]*)(?:(:?[-?])([^}]*))?\}$/',
|
||||
$normalizedPortValue,
|
||||
$matches
|
||||
)
|
||||
) {
|
||||
$environmentKey = $matches[1];
|
||||
$defaultPort = trim((string) ($matches[3] ?? ''));
|
||||
|
||||
$resolvedEnvironmentPort = $environmentMap[$environmentKey] ?? null;
|
||||
if (! is_null($resolvedEnvironmentPort) && is_numeric(trim((string) $resolvedEnvironmentPort))) {
|
||||
return (int) trim((string) $resolvedEnvironmentPort);
|
||||
}
|
||||
|
||||
if ($defaultPort !== '' && is_numeric($defaultPort)) {
|
||||
return (int) $defaultPort;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^\$([A-Za-z_][A-Za-z0-9_]*)$/', $normalizedPortValue, $matches)) {
|
||||
$environmentKey = $matches[1];
|
||||
$resolvedEnvironmentPort = $environmentMap[$environmentKey] ?? null;
|
||||
if (! is_null($resolvedEnvironmentPort) && is_numeric(trim((string) $resolvedEnvironmentPort))) {
|
||||
return (int) trim((string) $resolvedEnvironmentPort);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (array_key_exists($normalizedPortValue, $environmentMap) && is_numeric(trim((string) $environmentMap[$normalizedPortValue]))) {
|
||||
return (int) trim((string) $environmentMap[$normalizedPortValue]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function normalizeRemoteHost(string $rawHost): ?string
|
||||
{
|
||||
$host = trim($rawHost);
|
||||
if ($host === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_starts_with($host, 'http://') || str_starts_with($host, '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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1093,12 +1093,14 @@ class EdgeProxyRemoteRouteService
|
|||
|
||||
private function resolvePublishedPort(array $compose, string $serviceName, ?int $requestedInternalPort, array $environmentMap): ?int
|
||||
{
|
||||
$ports = $this->resolveComposeServicePorts($compose, $serviceName);
|
||||
if (! is_array($ports)) {
|
||||
$serviceConfig = $this->resolveComposeServiceConfig($compose, $serviceName);
|
||||
if (! is_array($serviceConfig)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$portMappings = $this->parsePortMappings($ports, $environmentMap)
|
||||
$resolvedEnvironmentMap = $this->mergeComposeEnvironmentMap($serviceConfig, $environmentMap);
|
||||
|
||||
$portMappings = $this->parsePortMappings((array) data_get($serviceConfig, 'ports', []), $resolvedEnvironmentMap)
|
||||
->filter(fn (array $mapping) => ! is_null($mapping['published']))
|
||||
->values();
|
||||
|
||||
|
|
@ -1116,17 +1118,132 @@ class EdgeProxyRemoteRouteService
|
|||
return collect();
|
||||
}
|
||||
|
||||
$ports = $this->parsePortMappings((array) data_get($serviceConfig, 'ports', []), $environmentMap)
|
||||
$resolvedEnvironmentMap = $this->mergeComposeEnvironmentMap($serviceConfig, $environmentMap);
|
||||
|
||||
$ports = $this->parsePortMappings((array) data_get($serviceConfig, 'ports', []), $resolvedEnvironmentMap)
|
||||
->pluck('target')
|
||||
->filter(fn (mixed $port) => ! is_null($port));
|
||||
|
||||
$exposedPorts = collect((array) data_get($serviceConfig, 'expose', []))
|
||||
->map(fn (mixed $port) => $this->resolvePortValue($port, $environmentMap))
|
||||
->map(fn (mixed $port) => $this->resolvePortValue($port, $resolvedEnvironmentMap))
|
||||
->filter(fn (?int $port) => ! is_null($port));
|
||||
|
||||
return $ports->merge($exposedPorts)->values();
|
||||
}
|
||||
|
||||
private function mergeComposeEnvironmentMap(array $serviceConfig, array $environmentMap): array
|
||||
{
|
||||
$resolvedEnvironmentMap = $environmentMap;
|
||||
|
||||
foreach ($this->composeEnvironmentDefinitions($serviceConfig) as $environmentKey => $rawValue) {
|
||||
if (
|
||||
array_key_exists($environmentKey, $environmentMap) &&
|
||||
trim((string) $environmentMap[$environmentKey]) !== ''
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resolvedValue = $this->resolveEnvironmentValue($rawValue, $resolvedEnvironmentMap);
|
||||
if (! is_null($resolvedValue)) {
|
||||
$resolvedEnvironmentMap[$environmentKey] = $resolvedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $resolvedEnvironmentMap;
|
||||
}
|
||||
|
||||
private function composeEnvironmentDefinitions(array $serviceConfig): array
|
||||
{
|
||||
$environmentDefinitions = [];
|
||||
$environment = data_get($serviceConfig, 'environment', []);
|
||||
|
||||
if (! is_array($environment)) {
|
||||
return $environmentDefinitions;
|
||||
}
|
||||
|
||||
foreach ($environment as $key => $value) {
|
||||
if (is_int($key)) {
|
||||
$environmentPair = explode('=', (string) $value, 2);
|
||||
if (count($environmentPair) !== 2 || trim($environmentPair[0]) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$environmentDefinitions[trim($environmentPair[0])] = $environmentPair[1];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! is_string($key) || trim($key) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! is_scalar($value) && ! is_null($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$environmentDefinitions[trim($key)] = $value;
|
||||
}
|
||||
|
||||
return $environmentDefinitions;
|
||||
}
|
||||
|
||||
private function resolveEnvironmentValue(mixed $rawValue, array $environmentMap): ?string
|
||||
{
|
||||
if (is_null($rawValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_bool($rawValue)) {
|
||||
return $rawValue ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_int($rawValue) || is_float($rawValue)) {
|
||||
return (string) $rawValue;
|
||||
}
|
||||
|
||||
$normalizedValue = trim((string) $rawValue);
|
||||
if ($normalizedValue === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
preg_match(
|
||||
'/^\$\{([A-Za-z_][A-Za-z0-9_]*)(?:(:?[-?])([^}]*))?\}$/',
|
||||
$normalizedValue,
|
||||
$matches
|
||||
)
|
||||
) {
|
||||
$environmentKey = $matches[1];
|
||||
$defaultValue = trim((string) ($matches[3] ?? ''));
|
||||
$resolvedEnvironmentValue = $environmentMap[$environmentKey] ?? null;
|
||||
|
||||
if (! is_null($resolvedEnvironmentValue) && trim((string) $resolvedEnvironmentValue) !== '') {
|
||||
return trim((string) $resolvedEnvironmentValue);
|
||||
}
|
||||
|
||||
return $defaultValue !== '' ? $defaultValue : null;
|
||||
}
|
||||
|
||||
if (preg_match('/^\$([A-Za-z_][A-Za-z0-9_]*)$/', $normalizedValue, $matches)) {
|
||||
$environmentKey = $matches[1];
|
||||
$resolvedEnvironmentValue = $environmentMap[$environmentKey] ?? null;
|
||||
|
||||
if (! is_null($resolvedEnvironmentValue) && trim((string) $resolvedEnvironmentValue) !== '') {
|
||||
return trim((string) $resolvedEnvironmentValue);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (array_key_exists($normalizedValue, $environmentMap)) {
|
||||
$resolvedEnvironmentValue = trim((string) $environmentMap[$normalizedValue]);
|
||||
|
||||
return $resolvedEnvironmentValue !== '' ? $resolvedEnvironmentValue : null;
|
||||
}
|
||||
|
||||
return $normalizedValue;
|
||||
}
|
||||
|
||||
private function selectPublishedPortFromMappings(Collection $portMappings, ?int $requestedInternalPort, ?int $fallbackInternalPort = null): ?int
|
||||
{
|
||||
if (! is_null($requestedInternalPort)) {
|
||||
|
|
|
|||
327
tests/Unit/EdgeProxyRemotePortForwardServiceTest.php
Normal file
327
tests/Unit/EdgeProxyRemotePortForwardServiceTest.php
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Services\EdgeProxyRemotePortForwardService;
|
||||
use Illuminate\Container\Container;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
$originalLogger = null;
|
||||
|
||||
beforeEach(function () use (&$originalLogger) {
|
||||
$container = Container::getInstance();
|
||||
$originalLogger = $container->bound('log') ? $container->make('log') : null;
|
||||
$container->instance('log', new NullLogger);
|
||||
});
|
||||
|
||||
afterEach(function () use (&$originalLogger) {
|
||||
if (is_null($originalLogger)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Container::getInstance()->instance('log', $originalLogger);
|
||||
});
|
||||
|
||||
it('mirrors application published tcp ports onto the edge server for remote deployments', function () {
|
||||
$edgeProxyServer = Mockery::mock(Server::class)->makePartial();
|
||||
$edgeProxyServer->id = 1;
|
||||
|
||||
$deploymentServer = Mockery::mock(Server::class)->makePartial();
|
||||
$deploymentServer->id = 2;
|
||||
$deploymentServer->ip = '10.8.0.15';
|
||||
|
||||
$application = new Application;
|
||||
$application->uuid = 'application-port-forward';
|
||||
$application->build_pack = 'nixpacks';
|
||||
$application->ports_mappings = '25565:25565';
|
||||
|
||||
$manager = new class extends EdgeProxyRemotePortForwardService
|
||||
{
|
||||
public array $calls = [];
|
||||
|
||||
protected function runRemoteCommands(Server $server, array $commands, bool $throwError = true): ?string
|
||||
{
|
||||
$this->calls[] = [
|
||||
'server_id' => $server->id,
|
||||
'commands' => $commands,
|
||||
'throw_error' => $throwError,
|
||||
];
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$warnings = $manager->syncApplicationWithServers($application, $edgeProxyServer, $deploymentServer);
|
||||
|
||||
expect($warnings)->toBe([])
|
||||
->and($manager->calls)->toHaveCount(1)
|
||||
->and($manager->calls[0]['server_id'])->toBe(1);
|
||||
|
||||
preg_match("/echo '([^']+)' \\| base64 -d \\| tee .*nginx\\.conf/", $manager->calls[0]['commands'][2], $nginxMatches);
|
||||
$nginxConf = base64_decode($nginxMatches[1] ?? '');
|
||||
expect($nginxConf)->toContain('listen 25565;')
|
||||
->and($nginxConf)->toContain('proxy_pass 10.8.0.15:25565;');
|
||||
|
||||
preg_match("/echo '([^']+)' \\| base64 -d \\| tee .*docker-compose\\.yaml/", $manager->calls[0]['commands'][3], $composeMatches);
|
||||
$dockerCompose = base64_decode($composeMatches[1] ?? '');
|
||||
$parsedCompose = Yaml::parse($dockerCompose);
|
||||
|
||||
expect(data_get($parsedCompose, 'services.application-application-port-forward-edge-port-proxy.container_name'))
|
||||
->toBe('application-application-port-forward-edge-port-proxy')
|
||||
->and(data_get($parsedCompose, 'services.application-application-port-forward-edge-port-proxy.ports'))
|
||||
->toBe(['25565:25565']);
|
||||
});
|
||||
|
||||
it('mirrors application published udp ports onto the edge server for remote deployments', function () {
|
||||
$edgeProxyServer = Mockery::mock(Server::class)->makePartial();
|
||||
$edgeProxyServer->id = 11;
|
||||
|
||||
$deploymentServer = Mockery::mock(Server::class)->makePartial();
|
||||
$deploymentServer->id = 12;
|
||||
$deploymentServer->ip = '10.8.0.25';
|
||||
|
||||
$application = new Application;
|
||||
$application->uuid = 'application-udp-port-forward';
|
||||
$application->build_pack = 'nixpacks';
|
||||
$application->ports_mappings = '19132:19132/udp';
|
||||
|
||||
$manager = new class extends EdgeProxyRemotePortForwardService
|
||||
{
|
||||
public array $calls = [];
|
||||
|
||||
protected function runRemoteCommands(Server $server, array $commands, bool $throwError = true): ?string
|
||||
{
|
||||
$this->calls[] = [
|
||||
'server_id' => $server->id,
|
||||
'commands' => $commands,
|
||||
'throw_error' => $throwError,
|
||||
];
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$warnings = $manager->syncApplicationWithServers($application, $edgeProxyServer, $deploymentServer);
|
||||
|
||||
expect($warnings)->toBe([])
|
||||
->and($manager->calls)->toHaveCount(1);
|
||||
|
||||
preg_match("/echo '([^']+)' \\| base64 -d \\| tee .*nginx\\.conf/", $manager->calls[0]['commands'][2], $nginxMatches);
|
||||
$nginxConf = base64_decode($nginxMatches[1] ?? '');
|
||||
expect($nginxConf)->toContain('listen 19132 udp;')
|
||||
->and($nginxConf)->toContain('proxy_pass 10.8.0.25:19132;');
|
||||
|
||||
preg_match("/echo '([^']+)' \\| base64 -d \\| tee .*docker-compose\\.yaml/", $manager->calls[0]['commands'][3], $composeMatches);
|
||||
$dockerCompose = base64_decode($composeMatches[1] ?? '');
|
||||
$parsedCompose = Yaml::parse($dockerCompose);
|
||||
|
||||
expect(data_get($parsedCompose, 'services.application-application-udp-port-forward-edge-port-proxy.ports'))
|
||||
->toBe(['19132:19132/udp']);
|
||||
});
|
||||
|
||||
it('mirrors published compose ports for services onto the edge server', function () {
|
||||
$edgeProxyServer = Mockery::mock(Server::class)->makePartial();
|
||||
$edgeProxyServer->id = 21;
|
||||
|
||||
$deploymentServer = Mockery::mock(Server::class)->makePartial();
|
||||
$deploymentServer->id = 22;
|
||||
$deploymentServer->ip = '10.8.0.35';
|
||||
|
||||
$service = new Service;
|
||||
$service->uuid = 'service-port-forward';
|
||||
$service->docker_compose_raw = <<<'YAML'
|
||||
services:
|
||||
mc:
|
||||
ports:
|
||||
- "25565:25565"
|
||||
- "19132:19132/udp"
|
||||
YAML;
|
||||
|
||||
$manager = new class extends EdgeProxyRemotePortForwardService
|
||||
{
|
||||
public array $calls = [];
|
||||
|
||||
protected function runRemoteCommands(Server $server, array $commands, bool $throwError = true): ?string
|
||||
{
|
||||
$this->calls[] = [
|
||||
'server_id' => $server->id,
|
||||
'commands' => $commands,
|
||||
'throw_error' => $throwError,
|
||||
];
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$warnings = $manager->syncServiceWithServers($service, $edgeProxyServer, $deploymentServer);
|
||||
|
||||
expect($warnings)->toBe([])
|
||||
->and($manager->calls)->toHaveCount(1)
|
||||
->and($manager->calls[0]['server_id'])->toBe(21);
|
||||
|
||||
preg_match("/echo '([^']+)' \\| base64 -d \\| tee .*nginx\\.conf/", $manager->calls[0]['commands'][2], $nginxMatches);
|
||||
$nginxConf = base64_decode($nginxMatches[1] ?? '');
|
||||
expect($nginxConf)->toContain('listen 25565;')
|
||||
->and($nginxConf)->toContain('proxy_pass 10.8.0.35:25565;')
|
||||
->and($nginxConf)->toContain('listen 19132 udp;')
|
||||
->and($nginxConf)->toContain('proxy_pass 10.8.0.35:19132;');
|
||||
});
|
||||
|
||||
it('mirrors compose application published ports resolved from compose environment defaults', function () {
|
||||
$edgeProxyServer = Mockery::mock(Server::class)->makePartial();
|
||||
$edgeProxyServer->id = 23;
|
||||
|
||||
$deploymentServer = Mockery::mock(Server::class)->makePartial();
|
||||
$deploymentServer->id = 24;
|
||||
$deploymentServer->ip = '10.8.0.55';
|
||||
|
||||
$application = new Application;
|
||||
$application->uuid = 'application-compose-env-default-port';
|
||||
$application->build_pack = 'dockercompose';
|
||||
$application->docker_compose_raw = <<<'YAML'
|
||||
services:
|
||||
mc:
|
||||
ports:
|
||||
- "${PORT}:25565"
|
||||
environment:
|
||||
- PORT=${PORT:-25565}
|
||||
YAML;
|
||||
|
||||
$manager = new class extends EdgeProxyRemotePortForwardService
|
||||
{
|
||||
public array $calls = [];
|
||||
|
||||
protected function runRemoteCommands(Server $server, array $commands, bool $throwError = true): ?string
|
||||
{
|
||||
$this->calls[] = [
|
||||
'server_id' => $server->id,
|
||||
'commands' => $commands,
|
||||
'throw_error' => $throwError,
|
||||
];
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$warnings = $manager->syncApplicationWithServers($application, $edgeProxyServer, $deploymentServer);
|
||||
|
||||
expect($warnings)->toBe([])
|
||||
->and($manager->calls)->toHaveCount(1);
|
||||
|
||||
preg_match("/echo '([^']+)' \\| base64 -d \\| tee .*nginx\\.conf/", $manager->calls[0]['commands'][2], $nginxMatches);
|
||||
$nginxConf = base64_decode($nginxMatches[1] ?? '');
|
||||
expect($nginxConf)->toContain('listen 25565;')
|
||||
->and($nginxConf)->toContain('proxy_pass 10.8.0.55:25565;');
|
||||
});
|
||||
|
||||
it('warns and removes stale application edge port proxy when remote host is missing', function () {
|
||||
$edgeProxyServer = Mockery::mock(Server::class)->makePartial();
|
||||
$edgeProxyServer->id = 31;
|
||||
|
||||
$deploymentServer = Mockery::mock(Server::class)->makePartial();
|
||||
$deploymentServer->id = 32;
|
||||
$deploymentServer->ip = '';
|
||||
|
||||
$application = new Application;
|
||||
$application->uuid = 'application-missing-remote-host';
|
||||
$application->build_pack = 'nixpacks';
|
||||
$application->ports_mappings = '25565:25565';
|
||||
|
||||
$manager = new class extends EdgeProxyRemotePortForwardService
|
||||
{
|
||||
public array $calls = [];
|
||||
|
||||
protected function runRemoteCommands(Server $server, array $commands, bool $throwError = true): ?string
|
||||
{
|
||||
$this->calls[] = [
|
||||
'server_id' => $server->id,
|
||||
'commands' => $commands,
|
||||
'throw_error' => $throwError,
|
||||
];
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$warnings = $manager->syncApplicationWithServers($application, $edgeProxyServer, $deploymentServer);
|
||||
|
||||
expect($warnings)->toHaveCount(1)
|
||||
->and($warnings[0])->toContain('remote host is missing')
|
||||
->and($manager->calls)->toHaveCount(1)
|
||||
->and($manager->calls[0]['commands'][0])->toContain('docker rm -f');
|
||||
});
|
||||
|
||||
it('skips reserved edge ports while keeping other published application ports', function () {
|
||||
$edgeProxyServer = Mockery::mock(Server::class)->makePartial();
|
||||
$edgeProxyServer->id = 41;
|
||||
|
||||
$deploymentServer = Mockery::mock(Server::class)->makePartial();
|
||||
$deploymentServer->id = 42;
|
||||
$deploymentServer->ip = '10.8.0.45';
|
||||
|
||||
$application = new Application;
|
||||
$application->uuid = 'application-reserved-ports';
|
||||
$application->build_pack = 'nixpacks';
|
||||
$application->ports_mappings = '80:80,25565:25565,443:443';
|
||||
|
||||
$manager = new class extends EdgeProxyRemotePortForwardService
|
||||
{
|
||||
public array $calls = [];
|
||||
|
||||
protected function runRemoteCommands(Server $server, array $commands, bool $throwError = true): ?string
|
||||
{
|
||||
$this->calls[] = [
|
||||
'server_id' => $server->id,
|
||||
'commands' => $commands,
|
||||
'throw_error' => $throwError,
|
||||
];
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$warnings = $manager->syncApplicationWithServers($application, $edgeProxyServer, $deploymentServer);
|
||||
|
||||
expect($warnings)->toHaveCount(2)
|
||||
->and($warnings[0])->toContain('reserved on the edge server')
|
||||
->and($warnings[1])->toContain('reserved on the edge server')
|
||||
->and($manager->calls)->toHaveCount(1);
|
||||
|
||||
preg_match("/echo '([^']+)' \\| base64 -d \\| tee .*nginx\\.conf/", $manager->calls[0]['commands'][2], $nginxMatches);
|
||||
$nginxConf = base64_decode($nginxMatches[1] ?? '');
|
||||
expect($nginxConf)->toContain('listen 25565;')
|
||||
->and($nginxConf)->not->toContain('listen 80;')
|
||||
->and($nginxConf)->not->toContain('listen 443;');
|
||||
});
|
||||
|
||||
it('deletes application edge port proxy containers', function () {
|
||||
$edgeProxyServer = Mockery::mock(Server::class)->makePartial();
|
||||
$edgeProxyServer->id = 51;
|
||||
|
||||
$application = new Application;
|
||||
$application->uuid = 'application-delete-port-proxy';
|
||||
|
||||
$manager = new class extends EdgeProxyRemotePortForwardService
|
||||
{
|
||||
public array $calls = [];
|
||||
|
||||
protected function runRemoteCommands(Server $server, array $commands, bool $throwError = true): ?string
|
||||
{
|
||||
$this->calls[] = [
|
||||
'server_id' => $server->id,
|
||||
'commands' => $commands,
|
||||
'throw_error' => $throwError,
|
||||
];
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$manager->deleteApplicationWithServer($application, $edgeProxyServer);
|
||||
|
||||
expect($manager->calls)->toHaveCount(1)
|
||||
->and($manager->calls[0]['server_id'])->toBe(51)
|
||||
->and($manager->calls[0]['commands'][0])->toContain('application-application-delete-port-proxy-edge-port-proxy');
|
||||
});
|
||||
|
|
@ -1207,7 +1207,7 @@ it('deletes application edge route when deployment server is the same as edge pr
|
|||
|
||||
expect($warnings)->toBe([])
|
||||
->and($manager->calls)->toHaveCount(1)
|
||||
->and(implode("\n", $manager->calls[0]['commands']))->toContain("/tmp/proxy/dynamic/application-remote-application-edge-self.yaml")
|
||||
->and(implode("\n", $manager->calls[0]['commands']))->toContain('/tmp/proxy/dynamic/application-remote-application-edge-self.yaml')
|
||||
->and(implode("\n", $manager->calls[0]['commands']))->not->toContain('tee');
|
||||
});
|
||||
|
||||
|
|
@ -1352,6 +1352,56 @@ YAML;
|
|||
expect($payload)->toContain('http://10.8.0.36:9091');
|
||||
});
|
||||
|
||||
it('resolves compose application published port from compose environment defaults', 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 = 37;
|
||||
$deploymentServer->ip = '10.8.0.37';
|
||||
$deploymentServer->proxy = ['type' => 'NONE'];
|
||||
|
||||
$application = new Application;
|
||||
$application->uuid = 'application-compose-env-default-port';
|
||||
$application->build_pack = 'dockercompose';
|
||||
$application->docker_compose_domains = json_encode([
|
||||
'web' => ['domain' => 'https://compose-env-default.example.com:3000'],
|
||||
]);
|
||||
$application->docker_compose_raw = <<<'YAML'
|
||||
services:
|
||||
web:
|
||||
ports:
|
||||
- "${APP_HOST_PORT}:3000"
|
||||
environment:
|
||||
- APP_HOST_PORT=${APP_HOST_PORT:-9082}
|
||||
YAML;
|
||||
|
||||
$warnings = $manager->syncApplicationWithServers($application, $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.37:9082');
|
||||
});
|
||||
|
||||
it('returns warning for unsupported application domain protocol while preserving valid http route', function () {
|
||||
$manager = new class extends EdgeProxyRemoteRouteService
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue