refactor(proxy-status): refactored how the proxy status is handled on the UI and on the backend
feat(cloudflare): improved cloudflare tunnel automated installation
This commit is contained in:
@@ -104,14 +104,11 @@ class RunRemoteProcess
|
||||
$this->activity->save();
|
||||
if ($this->call_event_on_finish) {
|
||||
try {
|
||||
if ($this->call_event_data) {
|
||||
event(resolve("App\\Events\\$this->call_event_on_finish", [
|
||||
'data' => $this->call_event_data,
|
||||
]));
|
||||
$eventClass = "App\\Events\\$this->call_event_on_finish";
|
||||
if (! is_null($this->call_event_data)) {
|
||||
event(new $eventClass($this->call_event_data));
|
||||
} else {
|
||||
event(resolve("App\\Events\\$this->call_event_on_finish", [
|
||||
'userId' => $this->activity->causer_id,
|
||||
]));
|
||||
event(new $eventClass($this->activity->causer_id));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Error calling event: '.$e->getMessage());
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -12,12 +13,38 @@ class CheckProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
// It should return if the proxy should be started (true) or not (false)
|
||||
public function handle(Server $server, $fromUI = false): bool
|
||||
/**
|
||||
* Determine if the proxy should be started
|
||||
*
|
||||
* @param bool $fromUI Whether this check is initiated from the UI
|
||||
* @return bool True if proxy should be started, false otherwise
|
||||
*/
|
||||
public function handle(Server $server, bool $fromUI = false): bool
|
||||
{
|
||||
// Early validation checks
|
||||
if (! $this->shouldProxyRun($server, $fromUI)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle swarm vs standalone differently
|
||||
if ($server->isSwarm()) {
|
||||
return $this->checkSwarmProxy($server);
|
||||
}
|
||||
|
||||
return $this->checkStandaloneProxy($server, $fromUI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic validation to determine if proxy can run at all
|
||||
*/
|
||||
private function shouldProxyRun(Server $server, bool $fromUI): bool
|
||||
{
|
||||
// Server must be functional
|
||||
if (! $server->isFunctional()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build servers don't need proxy
|
||||
if ($server->isBuildServer()) {
|
||||
if ($server->proxy) {
|
||||
$server->proxy = null;
|
||||
@@ -26,86 +53,308 @@ class CheckProxy
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$proxyType = $server->proxyType();
|
||||
|
||||
// Check if proxy is disabled or force stopped
|
||||
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate proxy configuration
|
||||
if (! $server->isProxyShouldRun()) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception('Proxy should not run. You selected the Custom Proxy.');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine proxy container name based on environment
|
||||
$proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($server->isSwarm()) {
|
||||
$status = getContainerStatus($server, $proxyContainerName);
|
||||
/**
|
||||
* Check proxy status for swarm mode
|
||||
*/
|
||||
private function checkSwarmProxy(Server $server): bool
|
||||
{
|
||||
$proxyContainerName = 'coolify-proxy_traefik';
|
||||
$status = getContainerStatus($server, $proxyContainerName);
|
||||
|
||||
// Update status if changed
|
||||
if ($server->proxy->status !== $status) {
|
||||
$server->proxy->set('status', $status);
|
||||
$server->save();
|
||||
if ($status === 'running') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
$status = getContainerStatus($server, $proxyContainerName);
|
||||
if ($status === 'running') {
|
||||
$server->proxy->set('status', 'running');
|
||||
$server->save();
|
||||
|
||||
return false;
|
||||
}
|
||||
if ($server->settings->is_cloudflare_tunnel) {
|
||||
return false;
|
||||
}
|
||||
$ip = $server->ip;
|
||||
if ($server->id === 0) {
|
||||
$ip = 'host.docker.internal';
|
||||
}
|
||||
$portsToCheck = ['80', '443'];
|
||||
|
||||
foreach ($portsToCheck as $port) {
|
||||
// Use the smart port checker that handles dual-stack properly
|
||||
if ($this->isPortConflict($server, $port, $proxyContainerName)) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br><br>Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a>");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
if ($server->proxyType() !== ProxyTypes::NONE->value) {
|
||||
$proxyCompose = CheckConfiguration::run($server);
|
||||
if (isset($proxyCompose)) {
|
||||
$yaml = Yaml::parse($proxyCompose);
|
||||
$portsToCheck = [];
|
||||
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
|
||||
$ports = data_get($yaml, 'services.traefik.ports');
|
||||
} elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
|
||||
$ports = data_get($yaml, 'services.caddy.ports');
|
||||
}
|
||||
if (isset($ports)) {
|
||||
foreach ($ports as $port) {
|
||||
$portsToCheck[] = str($port)->before(':')->value();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$portsToCheck = [];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error checking proxy: '.$e->getMessage());
|
||||
}
|
||||
if (count($portsToCheck) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If running, no need to start
|
||||
return $status !== 'running';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check proxy status for standalone mode
|
||||
*/
|
||||
private function checkStandaloneProxy(Server $server, bool $fromUI): bool
|
||||
{
|
||||
$proxyContainerName = 'coolify-proxy';
|
||||
$status = getContainerStatus($server, $proxyContainerName);
|
||||
|
||||
if ($server->proxy->status !== $status) {
|
||||
$server->proxy->set('status', $status);
|
||||
$server->save();
|
||||
ProxyStatusChanged::dispatch($server->id);
|
||||
}
|
||||
|
||||
// If already running, no need to start
|
||||
if ($status === 'running') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cloudflare tunnel doesn't need proxy
|
||||
if ($server->settings->is_cloudflare_tunnel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for port conflicts
|
||||
$portsToCheck = $this->getPortsToCheck($server);
|
||||
|
||||
if (empty($portsToCheck)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->validatePortsAvailable($server, $portsToCheck, $proxyContainerName, $fromUI);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of ports that need to be available for the proxy
|
||||
*/
|
||||
private function getPortsToCheck(Server $server): array
|
||||
{
|
||||
$defaultPorts = ['80', '443'];
|
||||
|
||||
try {
|
||||
if ($server->proxyType() === ProxyTypes::NONE->value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$proxyCompose = CheckConfiguration::run($server);
|
||||
if (! $proxyCompose) {
|
||||
return $defaultPorts;
|
||||
}
|
||||
|
||||
$yaml = Yaml::parse($proxyCompose);
|
||||
$ports = $this->extractPortsFromCompose($yaml, $server->proxyType());
|
||||
|
||||
return ! empty($ports) ? $ports : $defaultPorts;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error checking proxy configuration: '.$e->getMessage());
|
||||
|
||||
return $defaultPorts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ports from docker-compose configuration
|
||||
*/
|
||||
private function extractPortsFromCompose(array $yaml, string $proxyType): array
|
||||
{
|
||||
$ports = [];
|
||||
$servicePorts = null;
|
||||
|
||||
if ($proxyType === ProxyTypes::TRAEFIK->value) {
|
||||
$servicePorts = data_get($yaml, 'services.traefik.ports');
|
||||
} elseif ($proxyType === ProxyTypes::CADDY->value) {
|
||||
$servicePorts = data_get($yaml, 'services.caddy.ports');
|
||||
}
|
||||
|
||||
if ($servicePorts) {
|
||||
foreach ($servicePorts as $port) {
|
||||
$ports[] = str($port)->before(':')->value();
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($ports);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that required ports are available (checked in parallel)
|
||||
*/
|
||||
private function validatePortsAvailable(Server $server, array $ports, string $proxyContainerName, bool $fromUI): void
|
||||
{
|
||||
if (empty($ports)) {
|
||||
return;
|
||||
}
|
||||
|
||||
\Log::info('Checking ports in parallel: '.implode(', ', $ports));
|
||||
|
||||
// Check all ports in parallel
|
||||
$conflicts = $this->checkPortsInParallel($server, $ports, $proxyContainerName);
|
||||
|
||||
// Handle any conflicts found
|
||||
foreach ($conflicts as $port) {
|
||||
$message = "Port $port is in use";
|
||||
if ($fromUI) {
|
||||
$message = "Port $port is in use.<br>You must stop the process using this port.<br><br>".
|
||||
"Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>".
|
||||
"Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a>";
|
||||
}
|
||||
throw new \Exception($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check multiple ports for conflicts in parallel using concurrent processes
|
||||
*/
|
||||
private function checkPortsInParallel(Server $server, array $ports, string $proxyContainerName): array
|
||||
{
|
||||
$conflicts = [];
|
||||
|
||||
// First, quickly filter out ports used by our own proxy
|
||||
$ourProxyPorts = $this->getOurProxyPorts($server, $proxyContainerName);
|
||||
$portsToCheck = array_diff($ports, $ourProxyPorts);
|
||||
|
||||
if (empty($portsToCheck)) {
|
||||
\Log::info('All ports are used by our proxy, no conflicts to check');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build a single optimized command to check all ports at once
|
||||
$command = $this->buildBatchPortCheckCommand($portsToCheck, $server);
|
||||
if (! $command) {
|
||||
\Log::warning('No suitable port checking method available, falling back to sequential');
|
||||
|
||||
return $this->checkPortsSequentially($server, $portsToCheck, $proxyContainerName);
|
||||
}
|
||||
|
||||
try {
|
||||
$output = instant_remote_process([$command], $server, true);
|
||||
$results = $this->parseBatchPortCheckOutput($output, $portsToCheck);
|
||||
|
||||
foreach ($results as $port => $isConflict) {
|
||||
if ($isConflict) {
|
||||
$conflicts[] = $port;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning('Batch port check failed, falling back to sequential: '.$e->getMessage());
|
||||
|
||||
return $this->checkPortsSequentially($server, $portsToCheck, $proxyContainerName);
|
||||
}
|
||||
|
||||
return $conflicts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ports currently used by our proxy container
|
||||
*/
|
||||
private function getOurProxyPorts(Server $server, string $proxyContainerName): array
|
||||
{
|
||||
try {
|
||||
$getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
|
||||
$containerId = trim(instant_remote_process([$getProxyContainerId], $server));
|
||||
|
||||
if (empty($containerId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$getPortsCommand = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' 2>/dev/null || echo '{}'";
|
||||
$portsJson = instant_remote_process([$getPortsCommand], $server, true);
|
||||
$portsData = json_decode($portsJson, true);
|
||||
|
||||
$ports = [];
|
||||
if (is_array($portsData)) {
|
||||
foreach (array_keys($portsData) as $portSpec) {
|
||||
if (preg_match('/^(\d+)\/tcp$/', $portSpec, $matches)) {
|
||||
$ports[] = $matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ports;
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning('Failed to get proxy ports: '.$e->getMessage());
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a single command to check multiple ports efficiently
|
||||
*/
|
||||
private function buildBatchPortCheckCommand(array $ports, Server $server): ?string
|
||||
{
|
||||
$portList = implode('|', $ports);
|
||||
|
||||
// Try different commands in order of preference
|
||||
$commands = [
|
||||
// ss - fastest and most reliable
|
||||
[
|
||||
'check' => 'command -v ss >/dev/null 2>&1',
|
||||
'exec' => "ss -Htuln state listening | grep -E ':($portList) ' | awk '{print \$5}' | cut -d: -f2 | sort -u",
|
||||
],
|
||||
// netstat - widely available
|
||||
[
|
||||
'check' => 'command -v netstat >/dev/null 2>&1',
|
||||
'exec' => "netstat -tuln | grep -E ':($portList) ' | grep LISTEN | awk '{print \$4}' | cut -d: -f2 | sort -u",
|
||||
],
|
||||
// lsof - last resort
|
||||
[
|
||||
'check' => 'command -v lsof >/dev/null 2>&1',
|
||||
'exec' => "lsof -iTCP -sTCP:LISTEN -P -n | grep -E ':($portList) ' | awk '{print \$9}' | cut -d: -f2 | sort -u",
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($commands as $cmd) {
|
||||
try {
|
||||
instant_remote_process([$cmd['check']], $server);
|
||||
|
||||
return $cmd['exec'];
|
||||
} catch (\Throwable $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse output from batch port check command
|
||||
*/
|
||||
private function parseBatchPortCheckOutput(string $output, array $portsToCheck): array
|
||||
{
|
||||
$results = [];
|
||||
$usedPorts = array_filter(explode("\n", trim($output)));
|
||||
|
||||
foreach ($portsToCheck as $port) {
|
||||
$results[$port] = in_array($port, $usedPorts);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback to sequential checking if parallel fails
|
||||
*/
|
||||
private function checkPortsSequentially(Server $server, array $ports, string $proxyContainerName): array
|
||||
{
|
||||
$conflicts = [];
|
||||
|
||||
foreach ($ports as $port) {
|
||||
try {
|
||||
if ($this->isPortConflict($server, $port, $proxyContainerName)) {
|
||||
$conflicts[] = $port;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning("Sequential port check failed for port $port: ".$e->getMessage());
|
||||
// Continue checking other ports
|
||||
}
|
||||
}
|
||||
|
||||
return $conflicts;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,137 +363,168 @@ class CheckProxy
|
||||
*/
|
||||
private function isPortConflict(Server $server, string $port, string $proxyContainerName): bool
|
||||
{
|
||||
// First check if our own proxy is using this port (which is fine)
|
||||
// Check if our own proxy is using this port (which is fine)
|
||||
if ($this->isOurProxyUsingPort($server, $port, $proxyContainerName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try different methods to detect port conflicts
|
||||
$portCheckMethods = [
|
||||
'checkWithSs',
|
||||
'checkWithNetstat',
|
||||
'checkWithLsof',
|
||||
'checkWithNetcat',
|
||||
];
|
||||
|
||||
foreach ($portCheckMethods as $method) {
|
||||
try {
|
||||
$result = $this->$method($server, $port);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
continue; // Try next method
|
||||
}
|
||||
}
|
||||
|
||||
// If all methods fail, assume port is free to avoid false positives
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if our own proxy container is using the port
|
||||
*/
|
||||
private function isOurProxyUsingPort(Server $server, string $port, string $proxyContainerName): bool
|
||||
{
|
||||
try {
|
||||
$getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
|
||||
$containerId = trim(instant_remote_process([$getProxyContainerId], $server));
|
||||
|
||||
if (! empty($containerId)) {
|
||||
$checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'";
|
||||
try {
|
||||
instant_remote_process([$checkProxyPort], $server);
|
||||
|
||||
// Our proxy is using the port, which is fine
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
// Our container exists but not using this port
|
||||
}
|
||||
if (empty($containerId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'";
|
||||
instant_remote_process([$checkProxyPort], $server);
|
||||
|
||||
return true; // Our proxy is using the port
|
||||
} catch (\Throwable $e) {
|
||||
// Container not found or error checking, continue with regular checks
|
||||
}
|
||||
|
||||
// Command sets for different ways to check ports, ordered by preference
|
||||
$commandSets = [
|
||||
// Set 1: Use ss to check listener counts by protocol stack
|
||||
[
|
||||
'available' => 'command -v ss >/dev/null 2>&1',
|
||||
'check' => [
|
||||
// Get listening process details
|
||||
"ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"",
|
||||
// Count IPv4 listeners
|
||||
"echo \"\$ss_output\" | grep -c ':$port '",
|
||||
],
|
||||
],
|
||||
// Set 2: Use netstat as alternative to ss
|
||||
[
|
||||
'available' => 'command -v netstat >/dev/null 2>&1',
|
||||
'check' => [
|
||||
// Get listening process details
|
||||
"netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '",
|
||||
// Count listeners
|
||||
"echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'",
|
||||
],
|
||||
],
|
||||
// Set 3: Use lsof as last resort
|
||||
[
|
||||
'available' => 'command -v lsof >/dev/null 2>&1',
|
||||
'check' => [
|
||||
// Get process using the port
|
||||
"lsof -i :$port -P -n | grep 'LISTEN'",
|
||||
// Count listeners
|
||||
"lsof -i :$port -P -n | grep 'LISTEN' | wc -l",
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Try each command set until we find one available
|
||||
foreach ($commandSets as $set) {
|
||||
try {
|
||||
// Check if the command is available
|
||||
instant_remote_process([$set['available']], $server);
|
||||
|
||||
// Run the actual check commands
|
||||
$output = instant_remote_process($set['check'], $server, true);
|
||||
|
||||
// Parse the output lines
|
||||
$lines = explode("\n", trim($output));
|
||||
|
||||
// Get the detailed output and listener count
|
||||
$details = trim($lines[0] ?? '');
|
||||
$count = intval(trim($lines[1] ?? '0'));
|
||||
|
||||
// If no listeners or empty result, port is free
|
||||
if ($count == 0 || empty($details)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to detect if this is our coolify-proxy
|
||||
if (strpos($details, 'docker') !== false || strpos($details, $proxyContainerName) !== false) {
|
||||
// It's likely our docker or proxy, which is fine
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6)
|
||||
// If exactly 2 listeners and both have same port, likely dual-stack
|
||||
if ($count <= 2) {
|
||||
// Check if it looks like a standard dual-stack setup
|
||||
$isDualStack = false;
|
||||
|
||||
// Look for IPv4 and IPv6 in the listing (ss output format)
|
||||
if (preg_match('/LISTEN.*:'.$port.'\s/', $details) &&
|
||||
(preg_match('/\*:'.$port.'\s/', $details) ||
|
||||
preg_match('/:::'.$port.'\s/', $details))) {
|
||||
$isDualStack = true;
|
||||
}
|
||||
|
||||
// For netstat format
|
||||
if (strpos($details, '0.0.0.0:'.$port) !== false &&
|
||||
strpos($details, ':::'.$port) !== false) {
|
||||
$isDualStack = true;
|
||||
}
|
||||
|
||||
// For lsof format (IPv4 and IPv6)
|
||||
if (strpos($details, '*:'.$port) !== false &&
|
||||
preg_match('/\*:'.$port.'.*IPv4/', $details) &&
|
||||
preg_match('/\*:'.$port.'.*IPv6/', $details)) {
|
||||
$isDualStack = true;
|
||||
}
|
||||
|
||||
if ($isDualStack) {
|
||||
return false; // This is just a normal dual-stack setup
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, it's likely a real port conflict
|
||||
return true;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// This command set failed, try the next one
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to simpler check if all above methods fail
|
||||
try {
|
||||
// Just try to bind to the port directly to see if it's available
|
||||
$checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'";
|
||||
$result = instant_remote_process([$checkCommand], $server, true);
|
||||
|
||||
return trim($result) === 'in-use';
|
||||
} catch (\Throwable $e) {
|
||||
// If everything fails, assume the port is free to avoid false positives
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check port usage with ss command
|
||||
*/
|
||||
private function checkWithSs(Server $server, string $port): ?bool
|
||||
{
|
||||
$commands = [
|
||||
'command -v ss >/dev/null 2>&1',
|
||||
"ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"",
|
||||
"echo \"\$ss_output\" | grep -c ':$port '",
|
||||
];
|
||||
|
||||
$output = instant_remote_process($commands, $server, true);
|
||||
|
||||
return $this->parsePortCheckOutput($output, $port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check port usage with netstat command
|
||||
*/
|
||||
private function checkWithNetstat(Server $server, string $port): ?bool
|
||||
{
|
||||
$commands = [
|
||||
'command -v netstat >/dev/null 2>&1',
|
||||
"netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '",
|
||||
"echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'",
|
||||
];
|
||||
|
||||
$output = instant_remote_process($commands, $server, true);
|
||||
|
||||
return $this->parsePortCheckOutput($output, $port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check port usage with lsof command
|
||||
*/
|
||||
private function checkWithLsof(Server $server, string $port): ?bool
|
||||
{
|
||||
$commands = [
|
||||
'command -v lsof >/dev/null 2>&1',
|
||||
"lsof -i :$port -P -n | grep 'LISTEN'",
|
||||
"lsof -i :$port -P -n | grep 'LISTEN' | wc -l",
|
||||
];
|
||||
|
||||
$output = instant_remote_process($commands, $server, true);
|
||||
|
||||
return $this->parsePortCheckOutput($output, $port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check port usage with netcat as fallback
|
||||
*/
|
||||
private function checkWithNetcat(Server $server, string $port): ?bool
|
||||
{
|
||||
$checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'";
|
||||
$result = instant_remote_process([$checkCommand], $server, true);
|
||||
|
||||
return trim($result) === 'in-use';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse output from port checking commands
|
||||
*/
|
||||
private function parsePortCheckOutput(string $output, string $port): ?bool
|
||||
{
|
||||
$lines = explode("\n", trim($output));
|
||||
$details = trim($lines[0] ?? '');
|
||||
$count = intval(trim($lines[1] ?? '0'));
|
||||
|
||||
// No listeners found
|
||||
if ($count === 0 || empty($details)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's likely our docker/proxy process
|
||||
if (strpos($details, 'docker') !== false || strpos($details, 'coolify-proxy') !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle dual-stack scenarios (1-2 listeners for IPv4+IPv6)
|
||||
if ($count <= 2 && $this->isDualStackSetup($details, $port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Real port conflict detected
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if this is a normal dual-stack setup (IPv4 + IPv6)
|
||||
*/
|
||||
private function isDualStackSetup(string $details, string $port): bool
|
||||
{
|
||||
$patterns = [
|
||||
// ss output format
|
||||
'/LISTEN.*:'.$port.'\s/',
|
||||
'/\*:'.$port.'\s/',
|
||||
'/:::'.$port.'\s/',
|
||||
// netstat format
|
||||
'/0\.0\.0\.0:'.$port.'/',
|
||||
'/:::'.$port.'/',
|
||||
// lsof format
|
||||
'/\*:'.$port.'.*IPv[46]/',
|
||||
];
|
||||
|
||||
$matches = 0;
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $details)) {
|
||||
$matches++;
|
||||
}
|
||||
}
|
||||
|
||||
// If we see patterns indicating both IPv4 and IPv6, it's likely dual-stack
|
||||
return $matches >= 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStarted;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
@@ -57,20 +57,19 @@ class StartProxy
|
||||
" echo 'Successfully stopped and removed existing coolify-proxy.'",
|
||||
'fi',
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d',
|
||||
'docker compose up -d --wait',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
$commands = $commands->merge(connectProxyToNetworks($server));
|
||||
}
|
||||
|
||||
if ($async) {
|
||||
return remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server);
|
||||
return remote_process($commands, $server, callEventOnFinish: 'ProxyStatusChanged', callEventData: $server->id);
|
||||
} else {
|
||||
instant_remote_process($commands, $server);
|
||||
$server->proxy->set('status', 'running');
|
||||
$server->proxy->set('type', $proxyType);
|
||||
$server->save();
|
||||
ProxyStarted::dispatch($server);
|
||||
ProxyStatusChanged::dispatch($server->id);
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -24,6 +25,8 @@ class StopProxy
|
||||
$server->save();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
} finally {
|
||||
ProxyStatusChanged::dispatch($server->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Events\CloudflareTunnelConfigured;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ConfigureCloudflared
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, string $cloudflare_token)
|
||||
public function handle(Server $server, string $cloudflare_token, string $ssh_domain): Activity
|
||||
{
|
||||
try {
|
||||
$config = [
|
||||
@@ -24,6 +24,13 @@ class ConfigureCloudflared
|
||||
'command' => 'tunnel run',
|
||||
'environment' => [
|
||||
"TUNNEL_TOKEN={$cloudflare_token}",
|
||||
'TUNNEL_METRICS=127.0.0.1:60123',
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'cloudflared', 'tunnel', '--metrics', '127.0.0.1:60123', 'ready'],
|
||||
'interval' => '5s',
|
||||
'timeout' => '30s',
|
||||
'retries' => 5,
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -34,22 +41,20 @@ class ConfigureCloudflared
|
||||
'mkdir -p /tmp/cloudflared',
|
||||
'cd /tmp/cloudflared',
|
||||
"echo '$docker_compose_yml_base64' | base64 -d | tee docker-compose.yml > /dev/null",
|
||||
'echo Pulling latest Cloudflare Tunnel image.',
|
||||
'docker compose pull',
|
||||
'docker compose down -v --remove-orphans > /dev/null 2>&1',
|
||||
'docker compose up -d --remove-orphans',
|
||||
'echo Stopping existing Cloudflare Tunnel container.',
|
||||
'docker rm -f coolify-cloudflared || true',
|
||||
'echo Starting new Cloudflare Tunnel container.',
|
||||
'docker compose up --wait --wait-timeout 15 --remove-orphans || docker logs coolify-cloudflared',
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
} catch (\Throwable $e) {
|
||||
$server->settings->is_cloudflare_tunnel = false;
|
||||
$server->settings->save();
|
||||
throw $e;
|
||||
} finally {
|
||||
CloudflareTunnelConfigured::dispatch($server->team_id);
|
||||
|
||||
$commands = collect([
|
||||
'rm -fr /tmp/cloudflared',
|
||||
return remote_process($commands, $server, callEventOnFinish: 'CloudflareTunnelChanged', callEventData: [
|
||||
'server_id' => $server->id,
|
||||
'ssh_domain' => $ssh_domain,
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
} catch (\Throwable $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
app/Events/CloudflareTunnelChanged.php
Normal file
14
app/Events/CloudflareTunnelChanged.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CloudflareTunnelChanged
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public function __construct(public $data) {}
|
||||
}
|
||||
@@ -3,33 +3,12 @@
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProxyStatusChanged implements ShouldBroadcast
|
||||
class ProxyStatusChanged
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
public function __construct(public $data) {}
|
||||
}
|
||||
|
||||
35
app/Events/ProxyStatusChangedUI.php
Normal file
35
app/Events/ProxyStatusChangedUI.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProxyStatusChangedUI implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct(int $teamId)
|
||||
{
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -213,8 +213,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (! $application) {
|
||||
return;
|
||||
}
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
if ($application->status !== $containerStatus) {
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function updateApplicationPreviewStatus(string $applicationId, string $pullRequestId, string $containerStatus)
|
||||
@@ -249,8 +251,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
if ($application->status !== 'exited') {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -286,8 +290,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
return;
|
||||
}
|
||||
$applicationPreview->status = 'exited';
|
||||
$applicationPreview->save();
|
||||
if ($applicationPreview->status !== 'exited') {
|
||||
$applicationPreview->status = 'exited';
|
||||
$applicationPreview->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -318,8 +324,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (! $database) {
|
||||
return;
|
||||
}
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
if ($database->status !== $containerStatus) {
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
}
|
||||
if ($this->isRunning($containerStatus) && $tcpProxy) {
|
||||
$tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) {
|
||||
return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running';
|
||||
@@ -339,8 +347,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$notFoundDatabaseUuids->each(function ($databaseUuid) {
|
||||
$database = $this->databases->where('uuid', $databaseUuid)->first();
|
||||
if ($database) {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
if ($database->status !== 'exited') {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
}
|
||||
if ($database->is_public) {
|
||||
StopDatabaseProxy::dispatch($database);
|
||||
}
|
||||
@@ -358,14 +368,18 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($subType === 'application') {
|
||||
$application = $service->applications()->where('id', $subId)->first();
|
||||
if ($application) {
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
if ($application->status !== $containerStatus) {
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
} elseif ($subType === 'database') {
|
||||
$database = $service->databases()->where('id', $subId)->first();
|
||||
if ($database) {
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
if ($database->status !== $containerStatus) {
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,8 +392,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$notFoundServiceApplicationIds->each(function ($serviceApplicationId) {
|
||||
$application = ServiceApplication::find($serviceApplicationId);
|
||||
if ($application) {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
if ($application->status !== 'exited') {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -387,8 +403,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) {
|
||||
$database = ServiceDatabase::find($serviceDatabaseId);
|
||||
if ($database) {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
if ($database->status !== 'exited') {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,9 +36,10 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
|
||||
StartProxy::run($this->server, force: true);
|
||||
|
||||
CheckProxy::run($this->server, true);
|
||||
// CheckProxy::run($this->server, true);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
|
||||
66
app/Listeners/CloudflareTunnelChangedNotification.php
Normal file
66
app/Listeners/CloudflareTunnelChangedNotification.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\CloudflareTunnelChanged;
|
||||
use App\Events\CloudflareTunnelConfigured;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Sleep;
|
||||
|
||||
class CloudflareTunnelChangedNotification
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(CloudflareTunnelChanged $event): void
|
||||
{
|
||||
$server_id = data_get($event, 'data.server_id');
|
||||
$ssh_domain = data_get($event, 'data.ssh_domain');
|
||||
|
||||
$this->server = Server::find($server_id)->firstOrFail();
|
||||
|
||||
// Check if cloudflare tunnel is running (container is healthy) - try 3 times with 5 second intervals
|
||||
$cloudflareHealthy = false;
|
||||
$attempts = 3;
|
||||
|
||||
for ($i = 1; $i <= $attempts; $i++) {
|
||||
\Log::debug("Cloudflare health check attempt {$i}/{$attempts}", ['server_id' => $server_id]);
|
||||
$result = instant_remote_process_with_timeout(['docker inspect coolify-cloudflared | jq -e ".[0].State.Health.Status == \"healthy\""'], $this->server, false, 10);
|
||||
|
||||
if (blank($result)) {
|
||||
\Log::debug("Cloudflare Tunnels container not found on attempt {$i}", ['server_id' => $server_id]);
|
||||
} elseif ($result === 'true') {
|
||||
\Log::debug("Cloudflare Tunnels container healthy on attempt {$i}", ['server_id' => $server_id]);
|
||||
$cloudflareHealthy = true;
|
||||
break;
|
||||
} else {
|
||||
\Log::debug("Cloudflare Tunnels container not healthy on attempt {$i}", ['server_id' => $server_id, 'result' => $result]);
|
||||
}
|
||||
|
||||
// Sleep between attempts (except after the last attempt)
|
||||
if ($i < $attempts) {
|
||||
Sleep::for(5)->seconds();
|
||||
}
|
||||
}
|
||||
|
||||
if (! $cloudflareHealthy) {
|
||||
\Log::error('Cloudflare Tunnels container failed all health checks.', ['server_id' => $server_id, 'attempts' => $attempts]);
|
||||
|
||||
return;
|
||||
}
|
||||
$this->server->settings->update([
|
||||
'is_cloudflare_tunnel' => true,
|
||||
]);
|
||||
|
||||
// Only update IP if it's not already set to the ssh_domain or if it's empty
|
||||
if ($this->server->ip !== $ssh_domain && ! empty($ssh_domain)) {
|
||||
\Log::debug('Cloudflare Tunnels configuration updated - updating IP address.', ['old_ip' => $this->server->ip, 'new_ip' => $ssh_domain]);
|
||||
$this->server->update(['ip' => $ssh_domain]);
|
||||
} else {
|
||||
\Log::debug('Cloudflare Tunnels configuration updated - IP address unchanged.', ['current_ip' => $this->server->ip]);
|
||||
}
|
||||
$teamId = $this->server->team_id;
|
||||
CloudflareTunnelConfigured::dispatch($teamId);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\ProxyStarted;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Models\Server;
|
||||
|
||||
class ProxyStartedNotification
|
||||
@@ -18,5 +19,6 @@ class ProxyStartedNotification
|
||||
$this->server->setupDynamicProxyConfiguration();
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
// ProxyStatusChanged::dispatch( $this->server->id);
|
||||
}
|
||||
}
|
||||
|
||||
34
app/Listeners/ProxyStatusChangedNotification.php
Normal file
34
app/Listeners/ProxyStatusChangedNotification.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
|
||||
|
||||
class ProxyStatusChangedNotification implements ShouldQueueAfterCommit
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(ProxyStatusChanged $event)
|
||||
{
|
||||
$serverId = $event->data;
|
||||
$server = Server::find($serverId);
|
||||
if (is_null($server)) {
|
||||
return;
|
||||
}
|
||||
$proxyContainerName = 'coolify-proxy';
|
||||
$status = getContainerStatus($server, $proxyContainerName);
|
||||
$server->proxy->set('status', $status);
|
||||
$server->save();
|
||||
|
||||
if ($status === 'running') {
|
||||
$server->setupDefaultRedirect();
|
||||
$server->setupDynamicProxyConfiguration();
|
||||
$server->proxy->force_stop = false;
|
||||
$server->save();
|
||||
}
|
||||
ProxyStatusChangedUI::dispatch($server->team_id);
|
||||
}
|
||||
}
|
||||
95
app/Livewire/Server/CloudflareTunnel.php
Normal file
95
app/Livewire/Server/CloudflareTunnel.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\ConfigureCloudflared;
|
||||
use App\Models\Server;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class CloudflareTunnel extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
#[Validate(['required', 'string'])]
|
||||
public string $cloudflare_token;
|
||||
|
||||
#[Validate(['required', 'string'])]
|
||||
public string $ssh_domain;
|
||||
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $isCloudflareTunnelsEnabled;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
public function refresh()
|
||||
{
|
||||
$this->mount($this->server->uuid);
|
||||
}
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
||||
if ($this->server->isLocalhost()) {
|
||||
return redirect()->route('server.show', ['server_uuid' => $server_uuid]);
|
||||
}
|
||||
$this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel;
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleCloudflareTunnels()
|
||||
{
|
||||
try {
|
||||
remote_process(['docker rm -f coolify-cloudflared'], $this->server, false, 10);
|
||||
$this->isCloudflareTunnelsEnabled = false;
|
||||
$this->server->settings->is_cloudflare_tunnel = false;
|
||||
$this->server->settings->save();
|
||||
if ($this->server->ip_previous) {
|
||||
$this->server->update(['ip' => $this->server->ip_previous]);
|
||||
$this->dispatch('success', 'Cloudflare Tunnel disabled.<br><br>Manually updated the server IP address to its previous IP address.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Cloudflare Tunnel disabled. Please update the server IP address to its real IP address in the server settings.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCloudflareConfig()
|
||||
{
|
||||
$this->isCloudflareTunnelsEnabled = true;
|
||||
$this->server->settings->is_cloudflare_tunnel = true;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Cloudflare Tunnel enabled.');
|
||||
}
|
||||
|
||||
public function automatedCloudflareConfig()
|
||||
{
|
||||
try {
|
||||
if (str($this->ssh_domain)->contains('https://')) {
|
||||
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
|
||||
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
|
||||
}
|
||||
$activity = ConfigureCloudflared::run($this->server, $this->cloudflare_token, $this->ssh_domain);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.cloudflare-tunnel');
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class CloudflareTunnels extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $isCloudflareTunnelsEnabled;
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
||||
if ($this->server->isLocalhost()) {
|
||||
return redirect()->route('server.show', ['server_uuid' => $server_uuid]);
|
||||
}
|
||||
$this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel;
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->server->settings->is_cloudflare_tunnel = $this->isCloudflareTunnelsEnabled;
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCloudflareConfig()
|
||||
{
|
||||
$this->isCloudflareTunnelsEnabled = true;
|
||||
$this->server->settings->is_cloudflare_tunnel = true;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.cloudflare-tunnels');
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\ConfigureCloudflared;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class ConfigureCloudflareTunnels extends Component
|
||||
{
|
||||
public $server_id;
|
||||
|
||||
public string $cloudflare_token;
|
||||
|
||||
public string $ssh_domain;
|
||||
|
||||
public function alreadyConfigured()
|
||||
{
|
||||
try {
|
||||
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
|
||||
$server->settings->is_cloudflare_tunnel = true;
|
||||
$server->settings->save();
|
||||
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
if (str($this->ssh_domain)->contains('https://')) {
|
||||
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
|
||||
// remove / from the end
|
||||
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
|
||||
}
|
||||
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
|
||||
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
|
||||
$server->settings->is_cloudflare_tunnel = true;
|
||||
$server->ip = $this->ssh_domain;
|
||||
$server->save();
|
||||
$server->settings->save();
|
||||
$this->dispatch('info', 'Cloudflare Tunnels configuration started.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.configure-cloudflare-tunnels');
|
||||
}
|
||||
}
|
||||
119
app/Livewire/Server/Navbar.php
Normal file
119
app/Livewire/Server/Navbar.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
use App\Jobs\RestartProxyJob;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Navbar extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public bool $isChecking = false;
|
||||
|
||||
public ?string $currentRoute = null;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ProxyStatusChangedUI" => 'showNotification',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(Server $server)
|
||||
{
|
||||
$this->server = $server;
|
||||
$this->currentRoute = request()->route()->getName();
|
||||
}
|
||||
|
||||
public function restart()
|
||||
{
|
||||
try {
|
||||
RestartProxyJob::dispatch($this->server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkProxy()
|
||||
{
|
||||
try {
|
||||
CheckProxy::run($this->server, true);
|
||||
$this->dispatch('startProxy')->self();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function startProxy()
|
||||
{
|
||||
try {
|
||||
$activity = StartProxy::run($this->server, force: true);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function stop(bool $forceStop = true)
|
||||
{
|
||||
try {
|
||||
StopProxy::dispatch($this->server, $forceStop);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkProxyStatus()
|
||||
{
|
||||
if ($this->isChecking) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->isChecking = true;
|
||||
CheckProxy::run($this->server, true);
|
||||
$this->showNotification();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->isChecking = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function showNotification()
|
||||
{
|
||||
$status = $this->server->proxy->status ?? 'unknown';
|
||||
$forceStop = $this->server->proxy->force_stop ?? false;
|
||||
|
||||
switch ($status) {
|
||||
case 'running':
|
||||
$this->dispatch('success', 'Proxy is running.');
|
||||
break;
|
||||
case 'restarting':
|
||||
$this->dispatch('info', 'Initiating proxy restart.');
|
||||
break;
|
||||
case 'exited':
|
||||
if ($forceStop) {
|
||||
$this->dispatch('info', 'Proxy is stopped manually.');
|
||||
} else {
|
||||
$this->dispatch('info', 'Proxy is stopped manually. Starting in a moment.');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$this->dispatch('warning', 'Proxy is not running.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.navbar');
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class Proxy extends Component
|
||||
|
||||
public ?string $redirect_url = null;
|
||||
|
||||
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
|
||||
protected $listeners = ['saveConfiguration' => 'submit'];
|
||||
|
||||
protected $rules = [
|
||||
'server.settings.generate_exact_labels' => 'required|boolean',
|
||||
@@ -32,10 +32,10 @@ class Proxy extends Component
|
||||
$this->redirect_url = data_get($this->server, 'proxy.redirect_url');
|
||||
}
|
||||
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
$this->dispatch('refresh')->self();
|
||||
}
|
||||
// public function proxyStatusUpdated()
|
||||
// {
|
||||
// $this->dispatch('refresh')->self();
|
||||
// }
|
||||
|
||||
public function changeProxy()
|
||||
{
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server\Proxy;
|
||||
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Jobs\RestartProxyJob;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Deploy extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public bool $traefikDashboardAvailable = false;
|
||||
|
||||
public ?string $currentRoute = null;
|
||||
|
||||
public ?string $serverIp = null;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ProxyStatusChanged" => 'proxyStarted',
|
||||
'proxyStatusUpdated',
|
||||
'traefikDashboardAvailable',
|
||||
'serverRefresh' => 'proxyStatusUpdated',
|
||||
'checkProxy',
|
||||
'startProxy',
|
||||
'proxyChanged' => 'proxyStatusUpdated',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if ($this->server->id === 0) {
|
||||
$this->serverIp = base_ip();
|
||||
} else {
|
||||
$this->serverIp = $this->server->ip;
|
||||
}
|
||||
$this->currentRoute = request()->route()->getName();
|
||||
}
|
||||
|
||||
public function traefikDashboardAvailable(bool $data)
|
||||
{
|
||||
$this->traefikDashboardAvailable = $data;
|
||||
}
|
||||
|
||||
public function proxyStarted()
|
||||
{
|
||||
CheckProxy::run($this->server, true);
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
$this->server->refresh();
|
||||
}
|
||||
|
||||
public function restart()
|
||||
{
|
||||
try {
|
||||
RestartProxyJob::dispatch($this->server);
|
||||
$this->dispatch('checkProxy');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkProxy()
|
||||
{
|
||||
try {
|
||||
CheckProxy::run($this->server, true);
|
||||
$this->dispatch('startProxyPolling');
|
||||
$this->dispatch('proxyChecked');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function startProxy()
|
||||
{
|
||||
try {
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
$activity = StartProxy::run($this->server, force: true);
|
||||
$this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function stop(bool $forceStop = true)
|
||||
{
|
||||
try {
|
||||
StopProxy::run($this->server, $forceStop);
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class DynamicConfigurations extends Component
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations',
|
||||
"echo-private:team.{$teamId},ProxyStatusChangedUI" => 'loadDynamicConfigurations',
|
||||
'loadDynamicConfigurations',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ class Show extends Component
|
||||
|
||||
public $parameters = [];
|
||||
|
||||
protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
|
||||
// protected $listeners = ['proxyStatusUpdated'];
|
||||
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
$this->server->refresh();
|
||||
}
|
||||
// public function proxyStatusUpdated()
|
||||
// {
|
||||
// $this->server->refresh();
|
||||
// }
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server\Proxy;
|
||||
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Status extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public bool $polling = false;
|
||||
|
||||
public int $numberOfPolls = 0;
|
||||
|
||||
protected $listeners = [
|
||||
'proxyStatusUpdated',
|
||||
'startProxyPolling',
|
||||
];
|
||||
|
||||
public function startProxyPolling()
|
||||
{
|
||||
$this->checkProxy();
|
||||
}
|
||||
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
$this->server->refresh();
|
||||
}
|
||||
|
||||
public function checkProxy(bool $notification = false)
|
||||
{
|
||||
try {
|
||||
if ($this->polling) {
|
||||
if ($this->numberOfPolls >= 10) {
|
||||
$this->polling = false;
|
||||
$this->numberOfPolls = 0;
|
||||
$notification && $this->dispatch('error', 'Proxy is not running.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->numberOfPolls++;
|
||||
}
|
||||
$shouldStart = CheckProxy::run($this->server, true);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
}
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
if ($this->server->proxy->status === 'running') {
|
||||
$this->polling = false;
|
||||
$notification && $this->dispatch('success', 'Proxy is running.');
|
||||
} elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) {
|
||||
$notification && $this->dispatch('error', 'Proxy has exited.');
|
||||
} elseif ($this->server->proxy->force_stop) {
|
||||
$notification && $this->dispatch('error', 'Proxy is stopped manually.');
|
||||
} else {
|
||||
$notification && $this->dispatch('error', 'Proxy is not running.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getProxyStatus()
|
||||
{
|
||||
try {
|
||||
GetContainersStatus::run($this->server);
|
||||
// dispatch_sync(new ContainerStatusJob($this->server));
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,10 +86,7 @@ class Show extends Component
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh',
|
||||
'refreshServerShow' => 'refresh',
|
||||
];
|
||||
}
|
||||
@@ -211,7 +208,6 @@ class Show extends Component
|
||||
$this->server->settings->is_usable = $this->isUsable = true;
|
||||
$this->server->settings->save();
|
||||
ServerReachabilityChanged::dispatch($this->server);
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
} else {
|
||||
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ class Server extends BaseModel
|
||||
}
|
||||
if ($server->ip) {
|
||||
$payload['ip'] = str($server->ip)->trim();
|
||||
|
||||
// Update ip_previous when ip is being changed
|
||||
if ($server->isDirty('ip') && $server->getOriginal('ip')) {
|
||||
$payload['ip_previous'] = $server->getOriginal('ip');
|
||||
}
|
||||
}
|
||||
$server->forceFill($payload);
|
||||
});
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Events\ProxyStarted;
|
||||
use App\Listeners\MaintenanceModeDisabledNotification;
|
||||
use App\Listeners\MaintenanceModeEnabledNotification;
|
||||
use App\Listeners\ProxyStartedNotification;
|
||||
use Illuminate\Foundation\Events\MaintenanceModeDisabled;
|
||||
use Illuminate\Foundation\Events\MaintenanceModeEnabled;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
@@ -30,9 +28,6 @@ class EventServiceProvider extends ServiceProvider
|
||||
GoogleExtendSocialite::class.'@handle',
|
||||
InfomaniakExtendSocialite::class.'@handle',
|
||||
],
|
||||
ProxyStarted::class => [
|
||||
ProxyStartedNotification::class,
|
||||
],
|
||||
];
|
||||
|
||||
public function boot(): void
|
||||
@@ -42,6 +37,6 @@ class EventServiceProvider extends ServiceProvider
|
||||
|
||||
public function shouldDiscoverEvents(): bool
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user