mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
A non-root SSH user cannot open redirects, expand globs, or cd into /data/coolify (9999:root 0700). Several remote commands relied on that and failed on non-root servers. - Write README files with `tee` instead of shell redirects in database starts and application deployments - Clear Postgres init scripts with `find` instead of a glob - Split the proxy config backfill if/else so each branch gets sudo - Start the log drain with `docker compose -f` instead of `cd` - Wrap the preserved-repository compose start in `sh -c` for non-root - Stage clone archives outside /data/coolify via StagesCloneArchives - Extend the sudo parser and add non-root command tests
78 lines
2.6 KiB
PHP
78 lines
2.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Enums\ProcessStatus;
|
|
use App\Helpers\SshMultiplexingHelper;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\Process;
|
|
use Spatie\Activitylog\Models\Activity;
|
|
|
|
class DatabaseStartCommandExecutor
|
|
{
|
|
public function execute(array $commands, Model $database, Activity $activity): Activity
|
|
{
|
|
$server = $database->destination->server;
|
|
if ($server->isNonRoot()) {
|
|
$commands = parseCommandsByLineForSudo(collect($commands), $server);
|
|
}
|
|
|
|
$secrets = method_exists($database, 'resolvedSecretManagerValuesForRedaction')
|
|
? $database->resolvedSecretManagerValuesForRedaction()
|
|
: [];
|
|
$remoteCommand = SshMultiplexingHelper::generateSshCommand($server, implode("\n", $commands));
|
|
|
|
$activity->properties = $activity->properties->merge(['status' => ProcessStatus::IN_PROGRESS->value]);
|
|
$activity->save();
|
|
|
|
$process = Process::timeout(config('constants.ssh.command_timeout'))
|
|
->idleTimeout(3600)
|
|
->start($remoteCommand, function (string $type, string $output) use ($activity, $secrets): void {
|
|
$this->appendOutput($activity, $type, $this->redact($output, $secrets));
|
|
});
|
|
|
|
$result = $process->wait();
|
|
$status = $result->successful() ? ProcessStatus::FINISHED : ProcessStatus::ERROR;
|
|
$activity->properties = $activity->properties->merge([
|
|
'status' => $status->value,
|
|
'exitCode' => $result->exitCode(),
|
|
]);
|
|
$activity->save();
|
|
|
|
if (! $result->successful()) {
|
|
throw new \RuntimeException($this->redact($result->errorOutput(), $secrets), $result->exitCode());
|
|
}
|
|
|
|
return $activity;
|
|
}
|
|
|
|
private function redact(string $value, array $secrets): string
|
|
{
|
|
foreach ($secrets as $secret) {
|
|
if (is_string($secret) && $secret !== '') {
|
|
$value = str_replace($secret, REDACTED, $value);
|
|
}
|
|
}
|
|
|
|
return sanitize_utf8_text(remove_iip($value));
|
|
}
|
|
|
|
private function appendOutput(Activity $activity, string $type, string $output): void
|
|
{
|
|
if ($output === '') {
|
|
return;
|
|
}
|
|
|
|
$entries = json_decode($activity->description ?: '[]', true, flags: JSON_THROW_ON_ERROR);
|
|
$entries[] = [
|
|
'type' => $type,
|
|
'output' => $output,
|
|
'timestamp' => hrtime(true),
|
|
'batch' => 1,
|
|
'order' => count($entries) + 1,
|
|
];
|
|
$activity->description = json_encode($entries, flags: JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
|
$activity->save();
|
|
}
|
|
}
|