wip: non-root user for remote servers

This commit is contained in:
Andras Bacsai
2024-04-16 15:42:38 +02:00
parent 1aec16a240
commit a04c7831b1
11 changed files with 117 additions and 52 deletions

View File

@@ -16,11 +16,19 @@ class CheckConfiguration
return 'OK'; return 'OK';
} }
$proxy_path = $server->proxyPath(); $proxy_path = $server->proxyPath();
if ($server->isNonRoot()) {
$proxy_configuration = instant_remote_process([ $payload = [
"mkdir -p $proxy_path", "mkdir -p $proxy_path",
"cat $proxy_path/docker-compose.yml", "chown -R $server->user:$server->user $proxy_path",
], $server, false); "cat $proxy_path/docker-compose.yml",
];
} else {
$payload = [
"mkdir -p $proxy_path",
"cat $proxy_path/docker-compose.yml",
];
}
$proxy_configuration = instant_remote_process($payload, $server, false);
if ($reset || !$proxy_configuration || is_null($proxy_configuration)) { if ($reset || !$proxy_configuration || is_null($proxy_configuration)) {
$proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value; $proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value;

View File

@@ -13,8 +13,9 @@ class CheckProxy
if ($server->proxyType() === 'NONE') { if ($server->proxyType() === 'NONE') {
return false; return false;
} }
if (!$server->validateConnection()) { ['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
throw new \Exception("Server Connection Error"); if (!$uptime) {
throw new \Exception($error);
} }
if (!$server->isProxyShouldRun()) { if (!$server->isProxyShouldRun()) {
if ($fromUI) { if ($fromUI) {

View File

@@ -47,7 +47,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->server->isFunctional()) { if (!$this->server->isFunctional()) {
return 'Server is not ready.'; return 'Server is not ready.';
}; };
$applications = $this->server->applications(); $applications = $this->server->applications();
$skip_these_applications = collect([]); $skip_these_applications = collect([]);
foreach ($applications as $application) { foreach ($applications as $application) {
@@ -78,6 +77,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if (is_null($containers)) { if (is_null($containers)) {
return; return;
} }
$containers = format_docker_command_output_to_json($containers); $containers = format_docker_command_output_to_json($containers);
if ($containerReplicates) { if ($containerReplicates) {
$containerReplicates = format_docker_command_output_to_json($containerReplicates); $containerReplicates = format_docker_command_output_to_json($containerReplicates);
@@ -201,7 +201,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
// Notify user that this container should not be there. // Notify user that this container should not be there.
} }
} }
} }
if (data_get($container, 'Name') === '/coolify-db') { if (data_get($container, 'Name') === '/coolify-db') {
$foundDatabases[] = 0; $foundDatabases[] = 0;

View File

@@ -11,15 +11,17 @@ class NewActivityMonitor extends Component
public ?string $header = null; public ?string $header = null;
public $activityId; public $activityId;
public $eventToDispatch = 'activityFinished'; public $eventToDispatch = 'activityFinished';
public $eventData = null;
public $isPollingActive = false; public $isPollingActive = false;
protected $activity; protected $activity;
protected $listeners = ['newActivityMonitor' => 'newMonitorActivity']; protected $listeners = ['newActivityMonitor' => 'newMonitorActivity'];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished') public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null)
{ {
$this->activityId = $activityId; $this->activityId = $activityId;
$this->eventToDispatch = $eventToDispatch; $this->eventToDispatch = $eventToDispatch;
$this->eventData = $eventData;
$this->hydrateActivity(); $this->hydrateActivity();
@@ -55,8 +57,12 @@ class NewActivityMonitor extends Component
} }
return; return;
} }
$this->dispatch($this->eventToDispatch); if (!is_null($this->eventData)) {
ray('Dispatched event: ' . $this->eventToDispatch); $this->dispatch($this->eventToDispatch, $this->eventData);
} else {
$this->dispatch($this->eventToDispatch);
}
ray('Dispatched event: ' . $this->eventToDispatch . ' with data: ' . $this->eventData);
} }
} }
} }

View File

@@ -76,14 +76,14 @@ class Form extends Component
public function checkLocalhostConnection() public function checkLocalhostConnection()
{ {
$this->submit(); $this->submit();
$uptime = $this->server->validateConnection(); ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if ($uptime) { if ($uptime) {
$this->dispatch('success', 'Server is reachable.'); $this->dispatch('success', 'Server is reachable.');
$this->server->settings->is_reachable = true; $this->server->settings->is_reachable = true;
$this->server->settings->is_usable = true; $this->server->settings->is_usable = true;
$this->server->settings->save(); $this->server->settings->save();
} else { } 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.'); $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);
return; return;
} }
} }

View File

@@ -35,10 +35,11 @@ class ShowPrivateKey extends Component
public function checkConnection() public function checkConnection()
{ {
try { try {
$uptime = $this->server->validateConnection(); ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if ($uptime) { if ($uptime) {
$this->dispatch('success', 'Server is reachable.'); $this->dispatch('success', 'Server is reachable.');
} else { } else {
ray($error);
$this->dispatch('error', 'Server is not reachable.<br>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.'); $this->dispatch('error', 'Server is not reachable.<br>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.');
return; return;
} }

View File

@@ -11,7 +11,7 @@ class ValidateAndInstall extends Component
{ {
public Server $server; public Server $server;
public int $number_of_tries = 0; public int $number_of_tries = 0;
public int $max_tries = 1; public int $max_tries = 3;
public bool $install = true; public bool $install = true;
public $uptime = null; public $uptime = null;
public $supported_os_type = null; public $supported_os_type = null;
@@ -32,9 +32,8 @@ class ValidateAndInstall extends Component
'refresh' => '$refresh', 'refresh' => '$refresh',
]; ];
public function init(bool $install = true) public function init(int $data = 0)
{ {
$this->install = $install;
$this->uptime = null; $this->uptime = null;
$this->supported_os_type = null; $this->supported_os_type = null;
$this->docker_installed = null; $this->docker_installed = null;
@@ -42,7 +41,7 @@ class ValidateAndInstall extends Component
$this->docker_compose_installed = null; $this->docker_compose_installed = null;
$this->proxy_started = null; $this->proxy_started = null;
$this->error = null; $this->error = null;
$this->number_of_tries = 0; $this->number_of_tries = $data;
if (!$this->ask) { if (!$this->ask) {
$this->dispatch('validateConnection'); $this->dispatch('validateConnection');
} }
@@ -66,16 +65,15 @@ class ValidateAndInstall extends Component
} else { } else {
$this->proxy_started = true; $this->proxy_started = true;
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function validateConnection() public function validateConnection()
{ {
$this->uptime = $this->server->validateConnection(); ['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection();
if (!$this->uptime) { if (!$this->uptime) {
$this->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.'; $this->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;
return; return;
} }
$this->dispatch('validateOS'); $this->dispatch('validateOS');
@@ -99,10 +97,10 @@ class ValidateAndInstall extends Component
$this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.'; $this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return; return;
} else { } else {
if ($this->number_of_tries == 0) { if ($this->number_of_tries <= $this->max_tries) {
$activity = $this->server->installDocker(); $activity = $this->server->installDocker();
$this->number_of_tries++; $this->number_of_tries++;
$this->dispatch('newActivityMonitor', $activity->id, 'init'); $this->dispatch('newActivityMonitor', $activity->id, 'init', $this->number_of_tries);
} }
return; return;
} }

View File

@@ -481,8 +481,8 @@ $schema://$host {
// ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber); // ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber);
// ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax); // ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax);
$result = $this->validateConnection(); ['uptime' => $uptime] = $this->validateConnection();
if ($result) { if ($uptime) {
if ($this->unreachable_notification_sent === true) { if ($this->unreachable_notification_sent === true) {
$this->update(['unreachable_notification_sent' => false]); $this->update(['unreachable_notification_sent' => false]);
} }
@@ -551,7 +551,7 @@ $schema://$host {
public function loadUnmanagedContainers() public function loadUnmanagedContainers()
{ {
if ($this->isFunctional()) { if ($this->isFunctional()) {
$containers = instant_remote_process(["docker ps -a --format '{{json .}}' "], $this); $containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this);
$containers = format_docker_command_output_to_json($containers); $containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) { $containers = $containers->map(function ($container) {
$labels = data_get($container, 'Labels'); $labels = data_get($container, 'Labels');
@@ -748,34 +748,31 @@ $schema://$host {
$server = Server::find($this->id); $server = Server::find($this->id);
if (!$server) { if (!$server) {
return false; return ['uptime' => false, 'error' => 'Server not found.'];
} }
if ($server->skipServer()) { if ($server->skipServer()) {
return false; return ['uptime' => false, 'error' => 'Server skipped.'];
} }
// EC2 does not have `uptime` command, lol try {
// EC2 does not have `uptime` command, lol
$uptime = instant_remote_process(['ls /'], $server, false); instant_remote_process(['ls /'], $server);
if (!$uptime) {
$server->settings()->update([
'is_reachable' => false,
]);
return false;
} else {
$server->settings()->update([ $server->settings()->update([
'is_reachable' => true, 'is_reachable' => true,
]); ]);
$server->update([ $server->update([
'unreachable_count' => 0, 'unreachable_count' => 0,
]); ]);
if (data_get($server, 'unreachable_notification_sent') === true) {
$server->team?->notify(new Revived($server));
$server->update(['unreachable_notification_sent' => false]);
}
return ['uptime' => true, 'error' => null];
} catch (\Throwable $e) {
$server->settings()->update([
'is_reachable' => false,
]);
return ['uptime' => false, 'error' => $e->getMessage()];
} }
if (data_get($server, 'unreachable_notification_sent') === true) {
$server->team?->notify(new Revived($server));
$server->update(['unreachable_notification_sent' => false]);
}
return true;
} }
public function installDocker() public function installDocker()
{ {
@@ -784,7 +781,7 @@ $schema://$host {
} }
public function validateDockerEngine($throwError = false) public function validateDockerEngine($throwError = false)
{ {
$dockerBinary = instant_remote_process(["command -v docker"], $this, false); $dockerBinary = instant_remote_process(["command -v docker"], $this, false, no_sudo: true);
if (is_null($dockerBinary)) { if (is_null($dockerBinary)) {
$this->settings->is_usable = false; $this->settings->is_usable = false;
$this->settings->save(); $this->settings->save();
@@ -861,4 +858,8 @@ $schema://$host {
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
} }
} }
public function isNonRoot()
{
return $this->user !== 'root';
}
} }

View File

@@ -32,6 +32,33 @@ function remote_process(
if ($command instanceof Collection) { if ($command instanceof Collection) {
$command = $command->toArray(); $command = $command->toArray();
} }
if ($server->isNonRoot()) {
$command = collect($command)->map(function ($line) {
if (!str($line)->startSwith('cd')) {
return "sudo $line";
}
return $line;
})->toArray();
$command = collect($command)->map(function ($line) use ($server) {
if (Str::startsWith($line, 'sudo mkdir -p')) {
return "$line && sudo chown -R $server->user:$server->user " . Str::after($line, 'sudo mkdir -p') . ' && sudo chmod -R o-rwx ' . Str::after($line, 'sudo mkdir -p');
}
return $line;
})->toArray();
$command = collect($command)->map(function ($line) {
if (str($line)->contains('$(') || str($line)->contains('`')) {
return str($line)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value();
}
if (str($line)->contains('||')) {
return str($line)->replace('||', '|| sudo ')->value();
}
if (str($line)->contains('&&')) {
return str($line)->replace('&&', '&& sudo ')->value();
}
return $line;
})->toArray();
}
ray($command);
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
if (auth()->user()) { if (auth()->user()) {
$teams = auth()->user()->teams->pluck('id'); $teams = auth()->user()->teams->pluck('id');
@@ -144,7 +171,6 @@ function generateSshCommand(Server $server, string $command)
$ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; $ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
} }
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command"; $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command); $delimiter = Hash::make($command);
$command = str_replace($delimiter, '', $command); $command = str_replace($delimiter, '', $command);
$ssh_command .= "-i {$privateKeyLocation} " $ssh_command .= "-i {$privateKeyLocation} "
@@ -160,17 +186,42 @@ function generateSshCommand(Server $server, string $command)
. $command . PHP_EOL . $command . PHP_EOL
. $delimiter; . $delimiter;
// ray($ssh_command); // ray($ssh_command);
// ray($delimiter);
return $ssh_command; return $ssh_command;
} }
function instant_remote_process(Collection|array $command, Server $server, $throwError = true) function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false)
{ {
$timeout = config('constants.ssh.command_timeout'); $timeout = config('constants.ssh.command_timeout');
if ($command instanceof Collection) { if ($command instanceof Collection) {
$command = $command->toArray(); $command = $command->toArray();
} }
if ($server->isNonRoot() && !$no_sudo) {
$command = collect($command)->map(function ($line) {
if (!str($line)->startSwith('cd')) {
return "sudo $line";
}
return $line;
})->toArray();
$command = collect($command)->map(function ($line) use ($server) {
if (Str::startsWith($line, 'sudo mkdir -p')) {
return "$line && sudo chown -R $server->user:$server->user " . Str::after($line, 'sudo mkdir -p') . ' && sudo chmod -R o-rwx ' . Str::after($line, 'sudo mkdir -p');
}
return $line;
})->toArray();
$command = collect($command)->map(function ($line) {
if (str($line)->contains('$(') || str($line)->contains('`')) {
return str($line)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value();
}
if (str($line)->contains('||')) {
return str($line)->replace('||', '|| sudo ')->value();
}
if (str($line)->contains('&&')) {
return str($line)->replace('&&', '&& sudo ')->value();
}
return $line;
})->toArray();
}
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
$ssh_command = generateSshCommand($server, $command_string); $ssh_command = generateSshCommand($server, $command_string, $no_sudo);
$process = Process::timeout($timeout)->run($ssh_command); $process = Process::timeout($timeout)->run($ssh_command);
$output = trim($process->output()); $output = trim($process->output());
$exitCode = $process->exitCode(); $exitCode = $process->exitCode();

View File

@@ -10,7 +10,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input id="ip" label="IP Address/Domain" required <x-forms.input id="ip" label="IP Address/Domain" required
helper="An IP Address (127.0.0.1) or domain (example.com)." /> helper="An IP Address (127.0.0.1) or domain (example.com)." />
<x-forms.input id="user" label ="User" required /> {{-- <x-forms.input id="user" label="User" required /> --}}
<x-forms.input type="number" id="port" label="Port" required /> <x-forms.input type="number" id="port" label="Port" required />
</div> </div>
<x-forms.select label="Private Key" id="private_key_id"> <x-forms.select label="Private Key" id="private_key_id">

View File

@@ -618,7 +618,7 @@
"n8n": { "n8n": {
"documentation": "https:\/\/n8n.io", "documentation": "https:\/\/n8n.io",
"slogan": "n8n is an extendable workflow automation tool.", "slogan": "n8n is an extendable workflow automation tool.",
"compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdXRUJIT09LX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gJ0dFTkVSSUNfVElNRVpPTkU9IkV1cm9wZS9CZXJsaW4iJwogICAgICAtICdUWj0iRXVyb3BlL0JlcmxpbiInCiAgICB2b2x1bWVzOgogICAgICAtICduOG4tZGF0YTovaG9tZS9ub2RlLy5uOG4nCg==", "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdXRUJIT09LX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gJ0dFTkVSSUNfVElNRVpPTkU9IkV1cm9wZS9CZXJsaW4iJwogICAgICAtICdUWj0iRXVyb3BlL0JlcmxpbiInCiAgICB2b2x1bWVzOgogICAgICAtICduOG4tZGF0YTovaG9tZS9ub2RlLy5uOG4nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vbG9jYWxob3N0OjU2NzgvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
"tags": [ "tags": [
"n8n", "n8n",
"workflow", "workflow",