Merge branch 'next' into add-opnform-template
This commit is contained in:
commit
0cb7881f37
12 changed files with 411 additions and 163 deletions
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace App\Actions\Server;
|
namespace App\Actions\Server;
|
||||||
|
|
||||||
use App\Jobs\PullHelperImageJob;
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Illuminate\Support\Sleep;
|
use Illuminate\Support\Sleep;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
@ -50,7 +49,9 @@ public function handle($manual_update = false)
|
||||||
|
|
||||||
private function update()
|
private function update()
|
||||||
{
|
{
|
||||||
PullHelperImageJob::dispatch($this->server);
|
$helperImage = config('constants.coolify.helper_image');
|
||||||
|
$latest_version = getHelperVersion();
|
||||||
|
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
|
||||||
|
|
||||||
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
|
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
|
||||||
instant_remote_process(["docker pull -q $image"], $this->server, false);
|
instant_remote_process(["docker pull -q $image"], $this->server, false);
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Jobs;
|
|
||||||
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
public $timeout = 1000;
|
|
||||||
|
|
||||||
public function __construct(public Server $server)
|
|
||||||
{
|
|
||||||
$this->onQueue('high');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle(): void
|
|
||||||
{
|
|
||||||
$helperImage = config('constants.coolify.helper_image');
|
|
||||||
$latest_version = getHelperVersion();
|
|
||||||
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Jobs\PullHelperImageJob;
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Spatie\Url\Url;
|
use Spatie\Url\Url;
|
||||||
|
|
@ -35,14 +34,6 @@ class InstanceSettings extends Model
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
{
|
{
|
||||||
static::updated(function ($settings) {
|
static::updated(function ($settings) {
|
||||||
if ($settings->wasChanged('helper_version')) {
|
|
||||||
Server::chunkById(100, function ($servers) {
|
|
||||||
foreach ($servers as $server) {
|
|
||||||
PullHelperImageJob::dispatch($server);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear trusted hosts cache when FQDN changes
|
// Clear trusted hosts cache when FQDN changes
|
||||||
if ($settings->wasChanged('fqdn')) {
|
if ($settings->wasChanged('fqdn')) {
|
||||||
\Cache::forget('instance_settings_fqdn_host');
|
\Cache::forget('instance_settings_fqdn_host');
|
||||||
|
|
|
||||||
|
|
@ -58,16 +58,35 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
|
||||||
|
|
||||||
$commands = $commands->map(function ($line) {
|
$commands = $commands->map(function ($line) {
|
||||||
$line = str($line);
|
$line = str($line);
|
||||||
|
|
||||||
|
// Detect complex piped commands that should be wrapped in bash -c
|
||||||
|
$isComplexPipeCommand = (
|
||||||
|
$line->contains(' | sh') ||
|
||||||
|
$line->contains(' | bash') ||
|
||||||
|
($line->contains(' | ') && ($line->contains('||') || $line->contains('&&')))
|
||||||
|
);
|
||||||
|
|
||||||
|
// If it's a complex pipe command and starts with sudo, wrap it in bash -c
|
||||||
|
if ($isComplexPipeCommand && $line->startsWith('sudo ')) {
|
||||||
|
$commandWithoutSudo = $line->after('sudo ')->value();
|
||||||
|
// Escape single quotes for bash -c by replacing ' with '\''
|
||||||
|
$escapedCommand = str_replace("'", "'\\''", $commandWithoutSudo);
|
||||||
|
|
||||||
|
return "sudo bash -c '$escapedCommand'";
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-complex commands, apply the original logic
|
||||||
if (str($line)->contains('$(')) {
|
if (str($line)->contains('$(')) {
|
||||||
$line = $line->replace('$(', '$(sudo ');
|
$line = $line->replace('$(', '$(sudo ');
|
||||||
}
|
}
|
||||||
if (str($line)->contains('||')) {
|
if (! $isComplexPipeCommand && str($line)->contains('||')) {
|
||||||
$line = $line->replace('||', '|| sudo');
|
$line = $line->replace('||', '|| sudo');
|
||||||
}
|
}
|
||||||
if (str($line)->contains('&&')) {
|
if (! $isComplexPipeCommand && str($line)->contains('&&')) {
|
||||||
$line = $line->replace('&&', '&& sudo');
|
$line = $line->replace('&&', '&& sudo');
|
||||||
}
|
}
|
||||||
if (str($line)->contains(' | ')) {
|
// Don't insert sudo into pipes for complex commands
|
||||||
|
if (! $isComplexPipeCommand && str($line)->contains(' | ')) {
|
||||||
$line = $line->replace(' | ', ' | sudo ');
|
$line = $line->replace(' | ', ' | sudo ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
72
composer.lock
generated
72
composer.lock
generated
|
|
@ -9514,16 +9514,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-foundation",
|
"name": "symfony/http-foundation",
|
||||||
"version": "v7.3.2",
|
"version": "v7.3.7",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/http-foundation.git",
|
"url": "https://github.com/symfony/http-foundation.git",
|
||||||
"reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6"
|
"reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6",
|
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4",
|
||||||
"reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6",
|
"reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -9573,7 +9573,7 @@
|
||||||
"description": "Defines an object-oriented layer for the HTTP specification",
|
"description": "Defines an object-oriented layer for the HTTP specification",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/http-foundation/tree/v7.3.2"
|
"source": "https://github.com/symfony/http-foundation/tree/v7.3.7"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -9593,7 +9593,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-07-10T08:47:49+00:00"
|
"time": "2025-11-08T16:41:12+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-kernel",
|
"name": "symfony/http-kernel",
|
||||||
|
|
@ -9799,16 +9799,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/mime",
|
"name": "symfony/mime",
|
||||||
"version": "v7.3.2",
|
"version": "v7.3.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/mime.git",
|
"url": "https://github.com/symfony/mime.git",
|
||||||
"reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1"
|
"reference": "b1b828f69cbaf887fa835a091869e55df91d0e35"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1",
|
"url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35",
|
||||||
"reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1",
|
"reference": "b1b828f69cbaf887fa835a091869e55df91d0e35",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -9863,7 +9863,7 @@
|
||||||
"mime-type"
|
"mime-type"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/mime/tree/v7.3.2"
|
"source": "https://github.com/symfony/mime/tree/v7.3.4"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -9883,7 +9883,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-07-15T13:41:35+00:00"
|
"time": "2025-09-16T08:38:17+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/options-resolver",
|
"name": "symfony/options-resolver",
|
||||||
|
|
@ -10195,7 +10195,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-idn",
|
"name": "symfony/polyfill-intl-idn",
|
||||||
"version": "v1.32.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||||
|
|
@ -10258,7 +10258,7 @@
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
|
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -10269,6 +10269,10 @@
|
||||||
"url": "https://github.com/fabpot",
|
"url": "https://github.com/fabpot",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
|
|
@ -10278,7 +10282,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-normalizer",
|
"name": "symfony/polyfill-intl-normalizer",
|
||||||
"version": "v1.32.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||||
|
|
@ -10339,7 +10343,7 @@
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
|
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -10350,6 +10354,10 @@
|
||||||
"url": "https://github.com/fabpot",
|
"url": "https://github.com/fabpot",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
|
|
@ -10359,7 +10367,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-mbstring",
|
"name": "symfony/polyfill-mbstring",
|
||||||
"version": "v1.32.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||||
|
|
@ -10420,7 +10428,7 @@
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
|
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -10431,6 +10439,10 @@
|
||||||
"url": "https://github.com/fabpot",
|
"url": "https://github.com/fabpot",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
|
|
@ -10440,7 +10452,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php80",
|
"name": "symfony/polyfill-php80",
|
||||||
"version": "v1.32.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||||
|
|
@ -10500,7 +10512,7 @@
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0"
|
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -10511,6 +10523,10 @@
|
||||||
"url": "https://github.com/fabpot",
|
"url": "https://github.com/fabpot",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
|
|
@ -10520,16 +10536,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php83",
|
"name": "symfony/polyfill-php83",
|
||||||
"version": "v1.32.0",
|
"version": "v1.33.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php83.git",
|
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||||
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
|
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
|
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
|
||||||
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
|
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -10576,7 +10592,7 @@
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0"
|
"source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -10587,12 +10603,16 @@
|
||||||
"url": "https://github.com/fabpot",
|
"url": "https://github.com/fabpot",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-09-09T11:45:10+00:00"
|
"time": "2025-07-08T02:45:35+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-uuid",
|
"name": "symfony/polyfill-uuid",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'coolify' => [
|
'coolify' => [
|
||||||
'version' => '4.0.0-beta.443',
|
'version' => '4.0.0-beta.444',
|
||||||
'helper_version' => '1.0.12',
|
'helper_version' => '1.0.12',
|
||||||
'realtime_version' => '1.0.10',
|
'realtime_version' => '1.0.10',
|
||||||
'self_hosted' => env('SELF_HOSTED', true),
|
'self_hosted' => env('SELF_HOSTED', true),
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"coolify": {
|
"coolify": {
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.443"
|
|
||||||
},
|
|
||||||
"nightly": {
|
|
||||||
"version": "4.0.0-beta.444"
|
"version": "4.0.0-beta.444"
|
||||||
},
|
},
|
||||||
|
"nightly": {
|
||||||
|
"version": "4.0.0-beta.445"
|
||||||
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"version": "1.0.11"
|
"version": "1.0.12"
|
||||||
},
|
},
|
||||||
"realtime": {
|
"realtime": {
|
||||||
"version": "1.0.10"
|
"version": "1.0.10"
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,26 @@
|
||||||
'content' => null,
|
'content' => null,
|
||||||
'closeOutside' => true,
|
'closeOutside' => true,
|
||||||
'minWidth' => '36rem',
|
'minWidth' => '36rem',
|
||||||
|
'maxWidth' => '48rem',
|
||||||
'isFullWidth' => false,
|
'isFullWidth' => false,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$modalId = 'modal-' . uniqid();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#{{ $modalId }} {
|
||||||
|
max-height: calc(100vh - 2rem);
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
#{{ $modalId }} {
|
||||||
|
min-width: {{ $minWidth }};
|
||||||
|
max-width: {{ $maxWidth }};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div x-data="{ modalOpen: false }"
|
<div x-data="{ modalOpen: false }"
|
||||||
x-init="$watch('modalOpen', value => { if (!value) { $wire.dispatch('modalClosed') } })"
|
x-init="$watch('modalOpen', value => { if (!value) { $wire.dispatch('modalClosed') } })"
|
||||||
:class="{ 'z-40': modalOpen }" @keydown.window.escape="modalOpen=false"
|
:class="{ 'z-40': modalOpen }" @keydown.window.escape="modalOpen=false"
|
||||||
|
|
@ -38,14 +56,14 @@ class="fixed top-0 left-0 z-99 flex items-center justify-center w-screen h-scree
|
||||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||||
@if ($closeOutside) @click="modalOpen=false" @endif
|
@if ($closeOutside) @click="modalOpen=false" @endif
|
||||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen"
|
<div id="{{ $modalId }}" x-show="modalOpen" x-trap.inert.noscroll="modalOpen"
|
||||||
x-transition:enter="ease-out duration-100"
|
x-transition:enter="ease-out duration-100"
|
||||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
x-transition:leave="ease-in duration-100"
|
x-transition:leave="ease-in duration-100"
|
||||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||||
class="relative w-full border rounded-sm drop-shadow-sm min-w-full lg:min-w-[{{ $minWidth }}] max-w-fit max-h-[calc(100vh-2rem)] bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">
|
class="relative w-full border rounded-sm drop-shadow-sm min-w-full bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">
|
||||||
<div class="flex items-center justify-between py-6 px-6 shrink-0">
|
<div class="flex items-center justify-between py-6 px-6 shrink-0">
|
||||||
<h3 class="text-2xl font-bold">{{ $title }}</h3>
|
<h3 class="text-2xl font-bold">{{ $title }}</h3>
|
||||||
<button @click="modalOpen=false"
|
<button @click="modalOpen=false"
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
@if ($application->fqdn)
|
@if ($application->fqdn)
|
||||||
<span class="flex gap-1 text-xs">{{ Str::limit($application->fqdn, 60) }}
|
<span class="flex gap-1 text-xs">{{ Str::limit($application->fqdn, 60) }}
|
||||||
@can('update', $service)
|
@can('update', $service)
|
||||||
<x-modal-input title="Edit Domains" :closeOutside="false">
|
<x-modal-input title="Edit Domains" :closeOutside="false" minWidth="32rem" maxWidth="40rem">
|
||||||
<x-slot:content>
|
<x-slot:content>
|
||||||
<span class="cursor-pointer">
|
<span class="cursor-pointer">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use App\Jobs\PullHelperImageJob;
|
|
||||||
use App\Models\InstanceSettings;
|
|
||||||
use App\Models\Server;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\Queue;
|
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
|
||||||
|
|
||||||
it('dispatches PullHelperImageJob when helper_version changes', function () {
|
|
||||||
Queue::fake();
|
|
||||||
|
|
||||||
// Create user and servers
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$team = $user->teams()->first();
|
|
||||||
Server::factory()->count(3)->create(['team_id' => $team->id]);
|
|
||||||
|
|
||||||
$settings = InstanceSettings::firstOrCreate([], ['helper_version' => 'v1.0.0']);
|
|
||||||
|
|
||||||
// Change helper_version
|
|
||||||
$settings->helper_version = 'v1.2.3';
|
|
||||||
$settings->save();
|
|
||||||
|
|
||||||
// Verify PullHelperImageJob was dispatched for all servers
|
|
||||||
Queue::assertPushed(PullHelperImageJob::class, 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not dispatch PullHelperImageJob when helper_version is unchanged', function () {
|
|
||||||
Queue::fake();
|
|
||||||
|
|
||||||
// Create user and servers
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$team = $user->teams()->first();
|
|
||||||
Server::factory()->count(3)->create(['team_id' => $team->id]);
|
|
||||||
|
|
||||||
$settings = InstanceSettings::firstOrCreate([], ['helper_version' => 'v1.0.0']);
|
|
||||||
$currentVersion = $settings->helper_version;
|
|
||||||
|
|
||||||
// Set to same value
|
|
||||||
$settings->helper_version = $currentVersion;
|
|
||||||
$settings->save();
|
|
||||||
|
|
||||||
// Verify no jobs were dispatched
|
|
||||||
Queue::assertNotPushed(PullHelperImageJob::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not dispatch PullHelperImageJob when other fields change', function () {
|
|
||||||
Queue::fake();
|
|
||||||
|
|
||||||
// Create user and servers
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$team = $user->teams()->first();
|
|
||||||
Server::factory()->count(3)->create(['team_id' => $team->id]);
|
|
||||||
|
|
||||||
$settings = InstanceSettings::firstOrCreate([], ['helper_version' => 'v1.0.0']);
|
|
||||||
|
|
||||||
// Change different field
|
|
||||||
$settings->is_auto_update_enabled = ! $settings->is_auto_update_enabled;
|
|
||||||
$settings->save();
|
|
||||||
|
|
||||||
// Verify no jobs were dispatched
|
|
||||||
Queue::assertNotPushed(PullHelperImageJob::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('detects helper_version changes with wasChanged', function () {
|
|
||||||
$changeDetected = false;
|
|
||||||
|
|
||||||
InstanceSettings::updated(function ($settings) use (&$changeDetected) {
|
|
||||||
if ($settings->wasChanged('helper_version')) {
|
|
||||||
$changeDetected = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$settings = InstanceSettings::firstOrCreate([], ['helper_version' => 'v1.0.0']);
|
|
||||||
$settings->helper_version = 'v2.0.0';
|
|
||||||
$settings->save();
|
|
||||||
|
|
||||||
expect($changeDetected)->toBeTrue();
|
|
||||||
});
|
|
||||||
310
tests/Unit/ParseCommandsByLineForSudoTest.php
Normal file
310
tests/Unit/ParseCommandsByLineForSudoTest.php
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// Create a mock server with non-root user
|
||||||
|
$this->server = Mockery::mock(Server::class)->makePartial();
|
||||||
|
$this->server->shouldReceive('getAttribute')->with('user')->andReturn('ubuntu');
|
||||||
|
$this->server->shouldReceive('setAttribute')->andReturnSelf();
|
||||||
|
$this->server->user = 'ubuntu';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
Mockery::close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('wraps complex Docker install command with pipes in bash -c', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'curl https://releases.rancher.com/install-docker/27.3.sh | sh || curl https://get.docker.com | sh',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe("sudo bash -c 'curl https://releases.rancher.com/install-docker/27.3.sh | sh || curl https://get.docker.com | sh'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('wraps complex Docker install command with multiple fallbacks', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'curl --max-time 300 https://releases.rancher.com/install-docker/27.3.sh | sh || curl https://get.docker.com | sh -s -- --version 27.3',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe("sudo bash -c 'curl --max-time 300 https://releases.rancher.com/install-docker/27.3.sh | sh || curl https://get.docker.com | sh -s -- --version 27.3'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('wraps command with pipe to bash in bash -c', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'curl https://example.com/script.sh | bash',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe("sudo bash -c 'curl https://example.com/script.sh | bash'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('wraps complex command with pipes and && operators', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'curl https://example.com | sh && echo "done"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe("sudo bash -c 'curl https://example.com | sh && echo \"done\"'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escapes single quotes in complex piped commands', function () {
|
||||||
|
$commands = collect([
|
||||||
|
"curl https://example.com | sh -c 'echo \"test\"'",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe("sudo bash -c 'curl https://example.com | sh -c '\\''echo \"test\"'\\'''");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles simple command without pipes or operators', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'apt-get update',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe('sudo apt-get update');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles command with double ampersand operator but no pipes', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'mkdir -p /foo && chown ubuntu /foo',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe('sudo mkdir -p /foo && sudo chown ubuntu /foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles command with double pipe operator but no pipes', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'command -v docker || echo "not found"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
// 'command' is exempted from sudo, but echo gets sudo after ||
|
||||||
|
expect($result[0])->toBe('command -v docker || sudo echo "not found"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles command with simple pipe but no operators', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'cat file | grep pattern',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe('sudo cat file | sudo grep pattern');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles command with subshell $(...)', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'echo $(whoami)',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
// 'echo' is exempted from sudo at the start
|
||||||
|
expect($result[0])->toBe('echo $(sudo whoami)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips sudo for cd commands', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'cd /var/www',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe('cd /var/www');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips sudo for echo commands', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'echo "test"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe('echo "test"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips sudo for command commands', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'command -v docker',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe('command -v docker');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips sudo for true commands', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'true',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles if statements by adding sudo to condition', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'if command -v docker',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe('if sudo command -v docker');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips sudo for fi statements', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'fi',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe('fi');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds ownership changes for Coolify data paths', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'mkdir -p /data/coolify/logs',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
// Note: The && operator adds another sudo, creating double sudo for chown/chmod
|
||||||
|
// This is existing behavior that may need refactoring but isn't part of this bug fix
|
||||||
|
expect($result[0])->toBe('sudo mkdir -p /data/coolify/logs && sudo sudo chown -R ubuntu:ubuntu /data/coolify/logs && sudo sudo chmod -R o-rwx /data/coolify/logs');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds ownership changes for Coolify tmp paths', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'mkdir -p /tmp/coolify/cache',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
// Note: The && operator adds another sudo, creating double sudo for chown/chmod
|
||||||
|
// This is existing behavior that may need refactoring but isn't part of this bug fix
|
||||||
|
expect($result[0])->toBe('sudo mkdir -p /tmp/coolify/cache && sudo sudo chown -R ubuntu:ubuntu /tmp/coolify/cache && sudo sudo chmod -R o-rwx /tmp/coolify/cache');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not add ownership changes for system paths', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'mkdir -p /var/log',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe('sudo mkdir -p /var/log');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles multiple commands in sequence', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'apt-get update',
|
||||||
|
'apt-get install -y docker',
|
||||||
|
'curl https://get.docker.com | sh',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result)->toHaveCount(3);
|
||||||
|
expect($result[0])->toBe('sudo apt-get update');
|
||||||
|
expect($result[1])->toBe('sudo apt-get install -y docker');
|
||||||
|
expect($result[2])->toBe("sudo bash -c 'curl https://get.docker.com | sh'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles empty command list', function () {
|
||||||
|
$commands = collect([]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result)->toBeArray();
|
||||||
|
expect($result)->toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles real-world Docker installation command from InstallDocker action', function () {
|
||||||
|
$version = '27.3';
|
||||||
|
$commands = collect([
|
||||||
|
"curl --max-time 300 --retry 3 https://releases.rancher.com/install-docker/{$version}.sh | sh || curl --max-time 300 --retry 3 https://get.docker.com | sh -s -- --version {$version}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toStartWith("sudo bash -c '");
|
||||||
|
expect($result[0])->toEndWith("'");
|
||||||
|
expect($result[0])->toContain('curl --max-time 300');
|
||||||
|
expect($result[0])->toContain('| sh');
|
||||||
|
expect($result[0])->toContain('||');
|
||||||
|
expect($result[0])->not->toContain('| sudo sh');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves command structure in wrapped bash -c', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'curl https://example.com | sh || curl https://backup.com | sh',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
// The command should be wrapped without breaking the pipe and fallback structure
|
||||||
|
expect($result[0])->toBe("sudo bash -c 'curl https://example.com | sh || curl https://backup.com | sh'");
|
||||||
|
|
||||||
|
// Verify it doesn't contain broken patterns like "| sudo sh"
|
||||||
|
expect($result[0])->not->toContain('| sudo sh');
|
||||||
|
expect($result[0])->not->toContain('|| sudo curl');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles command with mixed operators and subshells', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'docker ps || echo $(date)',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
// This should use the original logic since it's not a complex pipe command
|
||||||
|
expect($result[0])->toBe('sudo docker ps || sudo echo $(sudo date)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles whitespace-only commands gracefully', function () {
|
||||||
|
$commands = collect([
|
||||||
|
' ',
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result)->toHaveCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('detects pipe to sh with additional arguments', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'curl https://example.com | sh -s -- --arg1 --arg2',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toBe("sudo bash -c 'curl https://example.com | sh -s -- --arg1 --arg2'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles command chains with both && and || operators with pipes', function () {
|
||||||
|
$commands = collect([
|
||||||
|
'curl https://first.com | sh && echo "success" || curl https://backup.com | sh',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = parseCommandsByLineForSudo($commands, $this->server);
|
||||||
|
|
||||||
|
expect($result[0])->toStartWith("sudo bash -c '");
|
||||||
|
expect($result[0])->toEndWith("'");
|
||||||
|
expect($result[0])->not->toContain('| sudo');
|
||||||
|
});
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"coolify": {
|
"coolify": {
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.443"
|
|
||||||
},
|
|
||||||
"nightly": {
|
|
||||||
"version": "4.0.0-beta.444"
|
"version": "4.0.0-beta.444"
|
||||||
},
|
},
|
||||||
|
"nightly": {
|
||||||
|
"version": "4.0.0-beta.445"
|
||||||
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"version": "1.0.11"
|
"version": "1.0.12"
|
||||||
},
|
},
|
||||||
"realtime": {
|
"realtime": {
|
||||||
"version": "1.0.10"
|
"version": "1.0.10"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue