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;
|
||||
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
|
@ -50,7 +49,9 @@ public function handle($manual_update = false)
|
|||
|
||||
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;
|
||||
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;
|
||||
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Url\Url;
|
||||
|
|
@ -35,14 +34,6 @@ class InstanceSettings extends Model
|
|||
protected static function booted(): void
|
||||
{
|
||||
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
|
||||
if ($settings->wasChanged('fqdn')) {
|
||||
\Cache::forget('instance_settings_fqdn_host');
|
||||
|
|
|
|||
|
|
@ -58,16 +58,35 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
|
|||
|
||||
$commands = $commands->map(function ($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('$(')) {
|
||||
$line = $line->replace('$(', '$(sudo ');
|
||||
}
|
||||
if (str($line)->contains('||')) {
|
||||
if (! $isComplexPipeCommand && str($line)->contains('||')) {
|
||||
$line = $line->replace('||', '|| sudo');
|
||||
}
|
||||
if (str($line)->contains('&&')) {
|
||||
if (! $isComplexPipeCommand && str($line)->contains('&&')) {
|
||||
$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 ');
|
||||
}
|
||||
|
||||
|
|
|
|||
72
composer.lock
generated
72
composer.lock
generated
|
|
@ -9514,16 +9514,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "v7.3.2",
|
||||
"version": "v7.3.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-foundation.git",
|
||||
"reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6"
|
||||
"reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6",
|
||||
"reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4",
|
||||
"reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -9573,7 +9573,7 @@
|
|||
"description": "Defines an object-oriented layer for the HTTP specification",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-foundation/tree/v7.3.2"
|
||||
"source": "https://github.com/symfony/http-foundation/tree/v7.3.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -9593,7 +9593,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-10T08:47:49+00:00"
|
||||
"time": "2025-11-08T16:41:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-kernel",
|
||||
|
|
@ -9799,16 +9799,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/mime",
|
||||
"version": "v7.3.2",
|
||||
"version": "v7.3.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mime.git",
|
||||
"reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1"
|
||||
"reference": "b1b828f69cbaf887fa835a091869e55df91d0e35"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1",
|
||||
"reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35",
|
||||
"reference": "b1b828f69cbaf887fa835a091869e55df91d0e35",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -9863,7 +9863,7 @@
|
|||
"mime-type"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mime/tree/v7.3.2"
|
||||
"source": "https://github.com/symfony/mime/tree/v7.3.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -9883,7 +9883,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-15T13:41:35+00:00"
|
||||
"time": "2025-09-16T08:38:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
|
|
@ -10195,7 +10195,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
"version": "v1.32.0",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||
|
|
@ -10258,7 +10258,7 @@
|
|||
"shim"
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
|
|
@ -10269,6 +10269,10 @@
|
|||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
|
|
@ -10278,7 +10282,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.32.0",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
|
|
@ -10339,7 +10343,7 @@
|
|||
"shim"
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
|
|
@ -10350,6 +10354,10 @@
|
|||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
|
|
@ -10359,7 +10367,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.32.0",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
|
|
@ -10420,7 +10428,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -10431,6 +10439,10 @@
|
|||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
|
|
@ -10440,7 +10452,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.32.0",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
|
|
@ -10500,7 +10512,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0"
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -10511,6 +10523,10 @@
|
|||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
|
|
@ -10520,16 +10536,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php83",
|
||||
"version": "v1.32.0",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
|
||||
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
|
||||
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
|
||||
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -10576,7 +10592,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0"
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -10587,12 +10603,16 @@
|
|||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
"time": "2025-07-08T02:45:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-uuid",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.443',
|
||||
'version' => '4.0.0-beta.444',
|
||||
'helper_version' => '1.0.12',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.443"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.444"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.445"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.11"
|
||||
"version": "1.0.12"
|
||||
},
|
||||
"realtime": {
|
||||
"version": "1.0.10"
|
||||
|
|
|
|||
|
|
@ -8,8 +8,26 @@
|
|||
'content' => null,
|
||||
'closeOutside' => true,
|
||||
'minWidth' => '36rem',
|
||||
'maxWidth' => '48rem',
|
||||
'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 }"
|
||||
x-init="$watch('modalOpen', value => { if (!value) { $wire.dispatch('modalClosed') } })"
|
||||
: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"
|
||||
@if ($closeOutside) @click="modalOpen=false" @endif
|
||||
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-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-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"
|
||||
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">
|
||||
<h3 class="text-2xl font-bold">{{ $title }}</h3>
|
||||
<button @click="modalOpen=false"
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
@if ($application->fqdn)
|
||||
<span class="flex gap-1 text-xs">{{ Str::limit($application->fqdn, 60) }}
|
||||
@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>
|
||||
<span class="cursor-pointer">
|
||||
<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": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.443"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.444"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.445"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.11"
|
||||
"version": "1.0.12"
|
||||
},
|
||||
"realtime": {
|
||||
"version": "1.0.10"
|
||||
|
|
|
|||
Loading…
Reference in a new issue