fix: cloudflare tunnel configuration, ui, etc
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Actions\Server;
|
namespace App\Actions\Server;
|
||||||
|
|
||||||
|
use App\Events\CloudflareTunnelConfigured;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
@@ -40,12 +41,17 @@ class ConfigureCloudflared
|
|||||||
instant_remote_process($commands, $server);
|
instant_remote_process($commands, $server);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
ray($e);
|
ray($e);
|
||||||
|
$server->settings->is_cloudflare_tunnel = false;
|
||||||
|
$server->settings->save();
|
||||||
throw $e;
|
throw $e;
|
||||||
} finally {
|
} finally {
|
||||||
|
CloudflareTunnelConfigured::dispatch($server->team_id);
|
||||||
|
|
||||||
$commands = collect([
|
$commands = collect([
|
||||||
'rm -fr /tmp/cloudflared',
|
'rm -fr /tmp/cloudflared',
|
||||||
]);
|
]);
|
||||||
instant_remote_process($commands, $server);
|
instant_remote_process($commands, $server);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
app/Events/CloudflareTunnelConfigured.php
Normal file
34
app/Events/CloudflareTunnelConfigured.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?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 CloudflareTunnelConfigured implements ShouldBroadcast
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
|
public $teamId;
|
||||||
|
|
||||||
|
public function __construct($teamId = null)
|
||||||
|
{
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||||
|
}
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
throw new \Exception('Team id is null');
|
||||||
|
}
|
||||||
|
$this->teamId = $teamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastOn(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new PrivateChannel("team.{$this->teamId}"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -33,10 +33,11 @@ class SshMultiplexingHelper
|
|||||||
|
|
||||||
self::validateSshKey($sshKeyLocation);
|
self::validateSshKey($sshKeyLocation);
|
||||||
|
|
||||||
$checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
|
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
|
||||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||||
$checkCommand = 'cloudflared access ssh --hostname %h -O check -o ControlPath=' . $muxSocket . ' ' . $server->user . '@' . $server->ip;
|
$checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||||
}
|
}
|
||||||
|
$checkCommand .= "{$server->user}@{$server->ip}";
|
||||||
$process = Process::run($checkCommand);
|
$process = Process::run($checkCommand);
|
||||||
|
|
||||||
if ($process->exitCode() !== 0) {
|
if ($process->exitCode() !== 0) {
|
||||||
@@ -54,14 +55,15 @@ class SshMultiplexingHelper
|
|||||||
$serverInterval = config('constants.ssh.server_interval');
|
$serverInterval = config('constants.ssh.server_interval');
|
||||||
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||||
|
|
||||||
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "
|
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||||
.self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval)
|
|
||||||
."{$server->user}@{$server->ip}";
|
|
||||||
|
|
||||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||||
$establishCommand = 'cloudflared access ssh --hostname %h -fNM -o ControlMaster=auto -o ControlPath=' . $muxSocket . ' -o ControlPersist=' . $muxPersistTime . ' ' . self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval) . $server->user . '@' . $server->ip;
|
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
|
||||||
|
$establishCommand .= "{$server->user}@{$server->ip}";
|
||||||
|
|
||||||
$establishProcess = Process::run($establishCommand);
|
$establishProcess = Process::run($establishCommand);
|
||||||
|
|
||||||
if ($establishProcess->exitCode() !== 0) {
|
if ($establishProcess->exitCode() !== 0) {
|
||||||
@@ -74,10 +76,11 @@ class SshMultiplexingHelper
|
|||||||
$sshConfig = self::serverSshConfiguration($server);
|
$sshConfig = self::serverSshConfiguration($server);
|
||||||
$muxSocket = $sshConfig['muxFilename'];
|
$muxSocket = $sshConfig['muxFilename'];
|
||||||
|
|
||||||
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
|
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
|
||||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||||
$closeCommand = 'cloudflared access ssh --hostname %h -O exit -o ControlPath=' . $muxSocket . ' ' . $server->user . '@' . $server->ip;
|
$closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||||
}
|
}
|
||||||
|
$closeCommand .= "{$server->user}@{$server->ip}";
|
||||||
Process::run($closeCommand);
|
Process::run($closeCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +101,7 @@ class SshMultiplexingHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||||
$scp_command = 'timeout ' . $timeout . ' cloudflared access ssh --hostname %h -o ControlMaster=auto -o ControlPath=' . $muxSocket . ' -o ControlPersist=' . $muxPersistTime . ' ';
|
$scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||||
}
|
}
|
||||||
|
|
||||||
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
|
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
|
||||||
@@ -128,7 +131,7 @@ class SshMultiplexingHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||||
$ssh_command = 'timeout ' . $timeout . ' cloudflared access ssh --hostname %h -o ControlMaster=auto -o ControlPath=' . $muxSocket . ' -o ControlPersist=' . $muxPersistTime . ' ';
|
$ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
|
||||||
}
|
}
|
||||||
|
|
||||||
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
|
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
|
||||||
|
@@ -31,13 +31,12 @@ class ConfigureCloudflareTunnels extends Component
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
|
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
|
||||||
ConfigureCloudflared::run($server, $this->cloudflare_token);
|
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
|
||||||
$server->settings->is_cloudflare_tunnel = true;
|
$server->settings->is_cloudflare_tunnel = true;
|
||||||
$server->ip = $this->ssh_domain;
|
$server->ip = $this->ssh_domain;
|
||||||
$server->save();
|
$server->save();
|
||||||
$server->settings->save();
|
$server->settings->save();
|
||||||
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
|
$this->dispatch('warning', 'Cloudflare Tunnels configuration started.');
|
||||||
$this->dispatch('refreshServerShow');
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
@@ -24,11 +24,16 @@ class Form extends Component
|
|||||||
|
|
||||||
public $timezones;
|
public $timezones;
|
||||||
|
|
||||||
protected $listeners = [
|
public function getListeners()
|
||||||
'serverInstalled',
|
{
|
||||||
'refreshServerShow' => 'serverInstalled',
|
$teamId = auth()->user()->currentTeam()->id;
|
||||||
'revalidate' => '$refresh',
|
|
||||||
];
|
return [
|
||||||
|
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured',
|
||||||
|
'refreshServerShow' => 'serverInstalled',
|
||||||
|
'revalidate' => '$refresh',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'server.name' => 'required',
|
'server.name' => 'required',
|
||||||
@@ -96,6 +101,12 @@ class Form extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function cloudflareTunnelConfigured()
|
||||||
|
{
|
||||||
|
$this->serverInstalled();
|
||||||
|
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
public function serverInstalled()
|
public function serverInstalled()
|
||||||
{
|
{
|
||||||
$this->server->refresh();
|
$this->server->refresh();
|
||||||
|
@@ -134,6 +134,9 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
|
|||||||
return 'exited';
|
return 'exited';
|
||||||
}
|
}
|
||||||
$container = format_docker_command_output_to_json($container);
|
$container = format_docker_command_output_to_json($container);
|
||||||
|
if ($container->isEmpty()) {
|
||||||
|
return 'exited';
|
||||||
|
}
|
||||||
if ($all_data) {
|
if ($all_data) {
|
||||||
return $container[0];
|
return $container[0];
|
||||||
}
|
}
|
||||||
|
@@ -30,7 +30,7 @@
|
|||||||
"service.stop": "This service will be stopped.",
|
"service.stop": "This service will be stopped.",
|
||||||
"resource.docker_cleanup": "Run Docker Cleanup (remove unused images and builder cache).",
|
"resource.docker_cleanup": "Run Docker Cleanup (remove unused images and builder cache).",
|
||||||
"resource.non_persistent": "All non-persistent data will be deleted.",
|
"resource.non_persistent": "All non-persistent data will be deleted.",
|
||||||
"resource.delete_volumes": "All volumes associated with this resource will be permanently deleted.",
|
"resource.delete_volumes": "Permanently delete all volumes associated with this resource.",
|
||||||
"resource.delete_connected_networks": "All non-predefined networks associated with this resource will be permanently deleted.",
|
"resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.",
|
||||||
"resource.delete_configurations": "All configuration files will be permanently deleted from the server."
|
"resource.delete_configurations": "Permanently delete all configuration files from the server."
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming back!
|
<div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming back!
|
||||||
</div>
|
</div>
|
||||||
<x-modal-confirmation title="Confirm Resource Deletion?" buttonTitle="Delete" isErrorButton submitAction="delete"
|
<x-modal-confirmation title="Confirm Resource Deletion?" buttonTitle="Delete" isErrorButton submitAction="delete"
|
||||||
buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['All containers of this resource will be stopped and permanently deleted.']" confirmationText="{{ $resourceName }}"
|
buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['Permanently delete all containers of this resource.']" confirmationText="{{ $resourceName }}"
|
||||||
confirmationLabel="Please confirm the execution of the actions by entering the NAME of the resource below"
|
confirmationLabel="Please confirm the execution of the actions by entering the NAME of the resource below"
|
||||||
shortConfirmationLabel="Resource Name" step3ButtonText="Delete Permanently" />
|
shortConfirmationLabel="Resource Name" step3ButtonText="Permanently Delete" />
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<form wire:submit.prevent='submit' class="flex flex-col w-full gap-2">
|
<form wire:submit.prevent='submit' class="flex flex-col gap-2 w-full">
|
||||||
<x-forms.input id="cloudflare_token" required label="Cloudflare Token" />
|
<x-forms.input id="cloudflare_token" required label="Cloudflare Token" type="password" />
|
||||||
<x-forms.input id="ssh_domain" label="Configured SSH Domain" required
|
<x-forms.input id="ssh_domain" label="Configured SSH Domain" required
|
||||||
helper="The SSH Domain you configured in Cloudflare. Make sure there is no protocol like http(s):// so you provide a FQDN not a URL." />
|
helper="The SSH domain you configured in Cloudflare. Make sure there is no protocol like http(s):// so you provide a FQDN not a URL. <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels/#automated' target='_blank'>Documentation</a>" />
|
||||||
<x-forms.button type="submit" isHighlighted @click="modalOpen=false">Automated Configuration</x-forms.button>
|
<x-forms.button type="submit" isHighlighted @click="modalOpen=false">Continue</x-forms.button>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -117,37 +117,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-96">
|
<div class="{{ $server->isFunctional() ? 'w-96' : 'w-full' }}">
|
||||||
@if (!$server->isLocalhost())
|
@if (!$server->isLocalhost())
|
||||||
<x-forms.checkbox instantSave id="server.settings.is_build_server"
|
<x-forms.checkbox instantSave id="server.settings.is_build_server"
|
||||||
label="Use it as a build server?" />
|
label="Use it as a build server?" />
|
||||||
<div class="flex flex-col gap-2 pt-6">
|
<div class="flex flex-col gap-2 pt-6">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex gap-1 items-center">
|
||||||
<h3 class="text-lg font-semibold">Cloudflare Tunnels</h3>
|
<h3 class="text-lg font-semibold">Cloudflare Tunnels</h3>
|
||||||
<x-helper class="inline-flex"
|
<x-helper class="inline-flex"
|
||||||
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
|
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
|
||||||
</div>
|
</div>
|
||||||
@if ($server->settings->is_cloudflare_tunnel)
|
@if ($server->settings->is_cloudflare_tunnel)
|
||||||
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel" label="Enabled" />
|
<div class="w-64">
|
||||||
|
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel" label="Enabled" />
|
||||||
|
</div>
|
||||||
@elseif (!$server->isFunctional())
|
@elseif (!$server->isFunctional())
|
||||||
<div class="p-4 mb-4 text-sm text-yellow-800 bg-yellow-100 rounded-lg dark:bg-yellow-900 dark:text-yellow-300">
|
<div class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded dark:bg-yellow-900 dark:text-yellow-300">
|
||||||
<p>Please select manual cloudflare tunnel configuration (first then hit validate server) or validate the server first and then you can select automatic configuration.</p>
|
<x-slide-over closeWithX fullScreen>
|
||||||
<p class="mt-2">For more information, please read our <a href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank" class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">Documentation</a>.</p>
|
<x-slot:title>Validate & configure</x-slot:title>
|
||||||
|
<x-slot:content>
|
||||||
|
<livewire:server.validate-and-install :server="$server" />
|
||||||
|
</x-slot:content>
|
||||||
|
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please click
|
||||||
|
<span @click="slideOverOpen=true"
|
||||||
|
wire:click.prevent='validateServer' class="underline cursor-pointer">
|
||||||
|
here.</span> You will need a Cloudflare token and domain.
|
||||||
|
</x-slide-over>
|
||||||
|
<br/>
|
||||||
|
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please click <span wire:click="manualCloudflareConfig" class="underline cursor-pointer">here</span>, then you should validate the server.
|
||||||
|
<br/><br/>
|
||||||
|
For more information, please read our <a href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank" class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">documentation</a>.
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional())
|
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional())
|
||||||
<x-modal-input buttonTitle="Automatic Configuration" title="Cloudflare Tunnels" class="w-full">
|
<x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels" class="w-full">
|
||||||
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
|
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
|
||||||
</x-modal-input>
|
</x-modal-input>
|
||||||
@endif
|
@endif
|
||||||
@if (!$server->settings->is_cloudflare_tunnel)
|
@if ($server->isFunctional() &&!$server->settings->is_cloudflare_tunnel)
|
||||||
<x-forms.button wire:click="manualCloudflareConfig" class="w-full">
|
<div wire:click="manualCloudflareConfig" class="w-full underline cursor-pointer">
|
||||||
I have configured Cloudflare Tunnels manually
|
I have configured Cloudflare Tunnels manually
|
||||||
</x-forms.button>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@if (!$server->isBuildServer())
|
@if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
|
||||||
<h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span></h3>
|
<h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span></h3>
|
||||||
<div class="pb-4">Read the docs <a class='underline dark:text-white'
|
<div class="pb-4">Read the docs <a class='underline dark:text-white'
|
||||||
href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>.
|
href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>.
|
||||||
|
Reference in New Issue
Block a user