refactor: use forceCreate() for internal model creation

Replace create() with forceCreate() across internal model creation operations to bypass mass assignment protection. This is appropriate for internal code that constructs complete model state without user input.

Add InternalModelCreationMassAssignmentTest to ensure internal model creation behavior is properly tested. Optimize imports by using shortened Livewire attribute references and removing unused imports.
This commit is contained in:
Andras Bacsai 2026-03-30 13:04:11 +02:00
parent 71cde5a063
commit 1da1f32f0e
41 changed files with 265 additions and 145 deletions

View file

@ -49,7 +49,7 @@ public function handle(Server $server)
}'); }');
$found = StandaloneDocker::where('server_id', $server->id); $found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0 && $server->id) { if ($found->count() == 0 && $server->id) {
StandaloneDocker::create([ StandaloneDocker::forceCreate([
'name' => 'coolify', 'name' => 'coolify',
'network' => 'coolify', 'network' => 'coolify',
'server_id' => $server->id, 'server_id' => $server->id,

View file

@ -136,7 +136,7 @@ public function handle()
$application = Application::all()->first(); $application = Application::all()->first();
$preview = ApplicationPreview::all()->first(); $preview = ApplicationPreview::all()->first();
if (! $preview) { if (! $preview) {
$preview = ApplicationPreview::create([ $preview = ApplicationPreview::forceCreate([
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => 1, 'pull_request_id' => 1,
'pull_request_html_url' => 'http://example.com', 'pull_request_html_url' => 'http://example.com',

View file

@ -5,6 +5,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Project; use App\Models\Project;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@ -234,7 +235,7 @@ public function create_project(Request $request)
} }
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof JsonResponse) {
return $return; return $return;
} }
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
@ -257,7 +258,7 @@ public function create_project(Request $request)
], 422); ], 422);
} }
$project = Project::create([ $project = Project::forceCreate([
'name' => $request->name, 'name' => $request->name,
'description' => $request->description, 'description' => $request->description,
'team_id' => $teamId, 'team_id' => $teamId,
@ -347,7 +348,7 @@ public function update_project(Request $request)
} }
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof JsonResponse) {
return $return; return $return;
} }
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
@ -600,7 +601,7 @@ public function create_environment(Request $request)
} }
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof JsonResponse) {
return $return; return $return;
} }
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [

View file

@ -432,7 +432,7 @@ public function create_service(Request $request)
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) { if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
data_set($servicePayload, 'connect_to_docker_network', true); data_set($servicePayload, 'connect_to_docker_network', true);
} }
$service = Service::create($servicePayload); $service = Service::forceCreate($servicePayload);
$service->name = $request->name ?? "$oneClickServiceName-".$service->uuid; $service->name = $request->name ?? "$oneClickServiceName-".$service->uuid;
$service->description = $request->description; $service->description = $request->description;
if ($request->has('is_container_label_escape_enabled')) { if ($request->has('is_container_label_escape_enabled')) {

View file

@ -119,7 +119,7 @@ public function manual(Request $request)
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) { if (! $found) {
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::create([ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'bitbucket', 'git_type' => 'bitbucket',
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
@ -128,7 +128,7 @@ public function manual(Request $request)
]); ]);
$pr_app->generate_preview_fqdn_compose(); $pr_app->generate_preview_fqdn_compose();
} else { } else {
$pr_app = ApplicationPreview::create([ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'bitbucket', 'git_type' => 'bitbucket',
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,

View file

@ -144,7 +144,7 @@ public function manual(Request $request)
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) { if (! $found) {
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::create([ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitea', 'git_type' => 'gitea',
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
@ -153,7 +153,7 @@ public function manual(Request $request)
]); ]);
$pr_app->generate_preview_fqdn_compose(); $pr_app->generate_preview_fqdn_compose();
} else { } else {
$pr_app = ApplicationPreview::create([ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitea', 'git_type' => 'gitea',
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,

View file

@ -177,7 +177,7 @@ public function manual(Request $request)
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) { if (! $found) {
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::create([ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitlab', 'git_type' => 'gitlab',
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
@ -186,7 +186,7 @@ public function manual(Request $request)
]); ]);
$pr_app->generate_preview_fqdn_compose(); $pr_app->generate_preview_fqdn_compose();
} else { } else {
$pr_app = ApplicationPreview::create([ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitlab', 'git_type' => 'gitlab',
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,

View file

@ -118,7 +118,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp
if (! $found) { if (! $found) {
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {
$preview = ApplicationPreview::create([ $preview = ApplicationPreview::forceCreate([
'git_type' => 'github', 'git_type' => 'github',
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => $this->pullRequestId, 'pull_request_id' => $this->pullRequestId,
@ -127,7 +127,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp
]); ]);
$preview->generate_preview_fqdn_compose(); $preview->generate_preview_fqdn_compose();
} else { } else {
$preview = ApplicationPreview::create([ $preview = ApplicationPreview::forceCreate([
'git_type' => 'github', 'git_type' => 'github',
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => $this->pullRequestId, 'pull_request_id' => $this->pullRequestId,

View file

@ -9,6 +9,7 @@
use App\Models\Team; use App\Models\Team;
use App\Services\ConfigurationRepository; use App\Services\ConfigurationRepository;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@ -19,18 +20,18 @@ class Index extends Component
'prerequisitesInstalled' => 'handlePrerequisitesInstalled', 'prerequisitesInstalled' => 'handlePrerequisitesInstalled',
]; ];
#[\Livewire\Attributes\Url(as: 'step', history: true)] #[Url(as: 'step', history: true)]
public string $currentState = 'welcome'; public string $currentState = 'welcome';
#[\Livewire\Attributes\Url(keep: true)] #[Url(keep: true)]
public ?string $selectedServerType = null; public ?string $selectedServerType = null;
public ?Collection $privateKeys = null; public ?Collection $privateKeys = null;
#[\Livewire\Attributes\Url(keep: true)] #[Url(keep: true)]
public ?int $selectedExistingPrivateKey = null; public ?int $selectedExistingPrivateKey = null;
#[\Livewire\Attributes\Url(keep: true)] #[Url(keep: true)]
public ?string $privateKeyType = null; public ?string $privateKeyType = null;
public ?string $privateKey = null; public ?string $privateKey = null;
@ -45,7 +46,7 @@ class Index extends Component
public ?Collection $servers = null; public ?Collection $servers = null;
#[\Livewire\Attributes\Url(keep: true)] #[Url(keep: true)]
public ?int $selectedExistingServer = null; public ?int $selectedExistingServer = null;
public ?string $remoteServerName = null; public ?string $remoteServerName = null;
@ -66,7 +67,7 @@ class Index extends Component
public Collection $projects; public Collection $projects;
#[\Livewire\Attributes\Url(keep: true)] #[Url(keep: true)]
public ?int $selectedProject = null; public ?int $selectedProject = null;
public ?Project $createdProject = null; public ?Project $createdProject = null;
@ -440,7 +441,7 @@ public function selectExistingProject()
public function createNewProject() public function createNewProject()
{ {
$this->createdProject = Project::create([ $this->createdProject = Project::forceCreate([
'name' => 'My first project', 'name' => 'My first project',
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2, 'uuid' => (string) new Cuid2,

View file

@ -5,7 +5,6 @@
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
@ -78,7 +77,7 @@ public function submit()
if ($found) { if ($found) {
throw new \Exception('Network already added to this server.'); throw new \Exception('Network already added to this server.');
} else { } else {
$docker = SwarmDocker::create([ $docker = SwarmDocker::forceCreate([
'name' => $this->name, 'name' => $this->name,
'network' => $this->network, 'network' => $this->network,
'server_id' => $this->selectedServer->id, 'server_id' => $this->selectedServer->id,
@ -89,7 +88,7 @@ public function submit()
if ($found) { if ($found) {
throw new \Exception('Network already added to this server.'); throw new \Exception('Network already added to this server.');
} else { } else {
$docker = StandaloneDocker::create([ $docker = StandaloneDocker::forceCreate([
'name' => $this->name, 'name' => $this->name,
'network' => $this->network, 'network' => $this->network,
'server_id' => $this->selectedServer->id, 'server_id' => $this->selectedServer->id,

View file

@ -30,7 +30,7 @@ public function submit()
{ {
try { try {
$this->validate(); $this->validate();
$project = Project::create([ $project = Project::forceCreate([
'name' => $this->name, 'name' => $this->name,
'description' => $this->description, 'description' => $this->description,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,

View file

@ -182,7 +182,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
$this->setDeploymentUuid(); $this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && ! is_null($pull_request_html_url)) { if (! $found && ! is_null($pull_request_html_url)) {
$found = ApplicationPreview::create([ $found = ApplicationPreview::forceCreate([
'application_id' => $this->application->id, 'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url, 'pull_request_html_url' => $pull_request_html_url,
@ -196,7 +196,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
$this->setDeploymentUuid(); $this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && ! is_null($pull_request_html_url)) { if (! $found && ! is_null($pull_request_html_url)) {
$found = ApplicationPreview::create([ $found = ApplicationPreview::forceCreate([
'application_id' => $this->application->id, 'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url, 'pull_request_html_url' => $pull_request_html_url,
@ -236,7 +236,7 @@ public function deploy(int $pull_request_id, ?string $pull_request_html_url = nu
$this->setDeploymentUuid(); $this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && ! is_null($pull_request_html_url)) { if (! $found && ! is_null($pull_request_html_url)) {
ApplicationPreview::create([ ApplicationPreview::forceCreate([
'application_id' => $this->application->id, 'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url, 'pull_request_html_url' => $pull_request_html_url,

View file

@ -100,7 +100,7 @@ public function clone(string $type)
if ($foundProject) { if ($foundProject) {
throw new \Exception('Project with the same name already exists.'); throw new \Exception('Project with the same name already exists.');
} }
$project = Project::create([ $project = Project::forceCreate([
'name' => $this->newName, 'name' => $this->newName,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'description' => $this->project->description.' (clone)', 'description' => $this->project->description.' (clone)',

View file

@ -54,7 +54,7 @@ public function submit()
} }
$destination_class = $destination->getMorphClass(); $destination_class = $destination->getMorphClass();
$service = Service::create([ $service = Service::forceCreate([
'docker_compose_raw' => $this->dockerComposeRaw, 'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id, 'environment_id' => $environment->id,
'server_id' => (int) $server_id, 'server_id' => (int) $server_id,

View file

@ -133,7 +133,7 @@ public function submit()
// Determine the image tag based on whether it's a hash or regular tag // Determine the image tag based on whether it's a hash or regular tag
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag(); $imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
$application = Application::create([ $application = Application::forceCreate([
'name' => 'docker-image-'.new Cuid2, 'name' => 'docker-image-'.new Cuid2,
'repository_project_id' => 0, 'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify', 'git_repository' => 'coollabsio/coolify',

View file

@ -10,7 +10,7 @@ class EmptyProject extends Component
{ {
public function createEmptyProject() public function createEmptyProject()
{ {
$project = Project::create([ $project = Project::forceCreate([
'name' => generate_random_name(), 'name' => generate_random_name(),
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2, 'uuid' => (string) new Cuid2,

View file

@ -8,6 +8,7 @@
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch; use App\Rules\ValidGitBranch;
use App\Support\ValidationPatterns;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Livewire\Component; use Livewire\Component;
@ -168,7 +169,7 @@ public function submit()
'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/', 'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/',
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/', 'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
'selected_branch_name' => ['required', 'string', new ValidGitBranch], 'selected_branch_name' => ['required', 'string', new ValidGitBranch],
'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(), 'docker_compose_location' => ValidationPatterns::filePathRules(),
]); ]);
if ($validator->fails()) { if ($validator->fails()) {
@ -188,7 +189,7 @@ public function submit()
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail(); $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail(); $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$application = Application::create([ $application = Application::forceCreate([
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name), 'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
'repository_project_id' => $this->selected_repository_id, 'repository_project_id' => $this->selected_repository_id,
'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(), 'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(),

View file

@ -11,6 +11,7 @@
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch; use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl; use App\Rules\ValidGitRepositoryUrl;
use App\Support\ValidationPatterns;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
@ -66,7 +67,7 @@ protected function rules()
'is_static' => 'required|boolean', 'is_static' => 'required|boolean',
'publish_directory' => 'nullable|string', 'publish_directory' => 'nullable|string',
'build_pack' => 'required|string', 'build_pack' => 'required|string',
'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(), 'docker_compose_location' => ValidationPatterns::filePathRules(),
]; ];
} }
@ -182,7 +183,7 @@ public function submit()
$application_init['docker_compose_location'] = $this->docker_compose_location; $application_init['docker_compose_location'] = $this->docker_compose_location;
$application_init['base_directory'] = $this->base_directory; $application_init['base_directory'] = $this->base_directory;
} }
$application = Application::create($application_init); $application = Application::forceCreate($application_init);
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();

View file

@ -11,6 +11,7 @@
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch; use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl; use App\Rules\ValidGitRepositoryUrl;
use App\Support\ValidationPatterns;
use Carbon\Carbon; use Carbon\Carbon;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
@ -72,7 +73,7 @@ protected function rules()
'publish_directory' => 'nullable|string', 'publish_directory' => 'nullable|string',
'build_pack' => 'required|string', 'build_pack' => 'required|string',
'base_directory' => 'nullable|string', 'base_directory' => 'nullable|string',
'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(), 'docker_compose_location' => ValidationPatterns::filePathRules(),
'git_branch' => ['required', 'string', new ValidGitBranch], 'git_branch' => ['required', 'string', new ValidGitBranch],
]; ];
} }
@ -233,7 +234,7 @@ private function getBranch()
return; return;
} }
if ($this->git_source->getMorphClass() === \App\Models\GithubApp::class) { if ($this->git_source->getMorphClass() === GithubApp::class) {
['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}"); ['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}");
$this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s'); $this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s');
$this->branchFound = true; $this->branchFound = true;
@ -298,7 +299,7 @@ public function submit()
$new_service['source_id'] = $this->git_source->id; $new_service['source_id'] = $this->git_source->id;
$new_service['source_type'] = $this->git_source->getMorphClass(); $new_service['source_type'] = $this->git_source->getMorphClass();
} }
$service = Service::create($new_service); $service = Service::forceCreate($new_service);
return redirect()->route('project.service.configuration', [ return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid, 'service_uuid' => $service->uuid,
@ -345,7 +346,7 @@ public function submit()
$application_init['docker_compose_location'] = $this->docker_compose_location; $application_init['docker_compose_location'] = $this->docker_compose_location;
$application_init['base_directory'] = $this->base_directory; $application_init['base_directory'] = $this->base_directory;
} }
$application = Application::create($application_init); $application = Application::forceCreate($application_init);
$application->settings->is_static = $this->isStatic; $application->settings->is_static = $this->isStatic;
$application->settings->save(); $application->settings->save();

View file

@ -52,7 +52,7 @@ public function submit()
if (! $port) { if (! $port) {
$port = 80; $port = 80;
} }
$application = Application::create([ $application = Application::forceCreate([
'name' => 'dockerfile-'.new Cuid2, 'name' => 'dockerfile-'.new Cuid2,
'repository_project_id' => 0, 'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify', 'git_repository' => 'coollabsio/coolify',

View file

@ -91,7 +91,7 @@ public function mount()
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) { if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
data_set($service_payload, 'connect_to_docker_network', true); data_set($service_payload, 'connect_to_docker_network', true);
} }
$service = Service::create($service_payload); $service = Service::forceCreate($service_payload);
$service->name = "$oneClickServiceName-".$service->uuid; $service->name = "$oneClickServiceName-".$service->uuid;
$service->save(); $service->save();
if ($oneClickDotEnvs?->count() > 0) { if ($oneClickDotEnvs?->count() > 0) {

View file

@ -42,7 +42,7 @@ public function submit()
{ {
try { try {
$this->validate(); $this->validate();
$environment = Environment::create([ $environment = Environment::forceCreate([
'name' => $this->name, 'name' => $this->name,
'project_id' => $this->project->id, 'project_id' => $this->project->id,
'uuid' => (string) new Cuid2, 'uuid' => (string) new Cuid2,

View file

@ -43,7 +43,7 @@ public function add($name)
return; return;
} else { } else {
SwarmDocker::create([ SwarmDocker::forceCreate([
'name' => $this->server->name.'-'.$name, 'name' => $this->server->name.'-'.$name,
'network' => $this->name, 'network' => $this->name,
'server_id' => $this->server->id, 'server_id' => $this->server->id,
@ -57,7 +57,7 @@ public function add($name)
return; return;
} else { } else {
StandaloneDocker::create([ StandaloneDocker::forceCreate([
'name' => $this->server->name.'-'.$name, 'name' => $this->server->name.'-'.$name,
'network' => $name, 'network' => $name,
'server_id' => $this->server->id, 'server_id' => $this->server->id,

View file

@ -6,6 +6,7 @@
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
@ -82,7 +83,7 @@ public function addCoolifyDatabase()
$postgres_password = $envs['POSTGRES_PASSWORD']; $postgres_password = $envs['POSTGRES_PASSWORD'];
$postgres_user = $envs['POSTGRES_USER']; $postgres_user = $envs['POSTGRES_USER'];
$postgres_db = $envs['POSTGRES_DB']; $postgres_db = $envs['POSTGRES_DB'];
$this->database = StandalonePostgresql::create([ $this->database = StandalonePostgresql::forceCreate([
'id' => 0, 'id' => 0,
'name' => 'coolify-db', 'name' => 'coolify-db',
'description' => 'Coolify database', 'description' => 'Coolify database',
@ -90,7 +91,7 @@ public function addCoolifyDatabase()
'postgres_password' => $postgres_password, 'postgres_password' => $postgres_password,
'postgres_db' => $postgres_db, 'postgres_db' => $postgres_db,
'status' => 'running', 'status' => 'running',
'destination_type' => \App\Models\StandaloneDocker::class, 'destination_type' => StandaloneDocker::class,
'destination_id' => 0, 'destination_id' => 0,
]); ]);
$this->backup = ScheduledDatabaseBackup::create([ $this->backup = ScheduledDatabaseBackup::create([
@ -99,7 +100,7 @@ public function addCoolifyDatabase()
'save_s3' => false, 'save_s3' => false,
'frequency' => '0 0 * * *', 'frequency' => '0 0 * * *',
'database_id' => $this->database->id, 'database_id' => $this->database->id,
'database_type' => \App\Models\StandalonePostgresql::class, 'database_type' => StandalonePostgresql::class,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ]);
$this->database->refresh(); $this->database->refresh();

View file

@ -299,7 +299,7 @@ protected static function booted()
} }
}); });
static::created(function ($application) { static::created(function ($application) {
ApplicationSetting::create([ ApplicationSetting::forceCreate([
'application_id' => $application->id, 'application_id' => $application->id,
]); ]);
$application->compose_parsing_version = self::$parserVersion; $application->compose_parsing_version = self::$parserVersion;

View file

@ -51,10 +51,10 @@ public static function ownedByCurrentTeamCached()
protected static function booted() protected static function booted()
{ {
static::created(function ($project) { static::created(function ($project) {
ProjectSetting::create([ ProjectSetting::forceCreate([
'project_id' => $project->id, 'project_id' => $project->id,
]); ]);
Environment::create([ Environment::forceCreate([
'name' => 'production', 'name' => 'production',
'project_id' => $project->id, 'project_id' => $project->id,
'uuid' => (string) new Cuid2, 'uuid' => (string) new Cuid2,

View file

@ -143,19 +143,19 @@ protected static function booted()
} }
}); });
static::created(function ($server) { static::created(function ($server) {
ServerSetting::create([ ServerSetting::forceCreate([
'server_id' => $server->id, 'server_id' => $server->id,
]); ]);
if ($server->id === 0) { if ($server->id === 0) {
if ($server->isSwarm()) { if ($server->isSwarm()) {
SwarmDocker::create([ SwarmDocker::forceCreate([
'id' => 0, 'id' => 0,
'name' => 'coolify', 'name' => 'coolify',
'network' => 'coolify-overlay', 'network' => 'coolify-overlay',
'server_id' => $server->id, 'server_id' => $server->id,
]); ]);
} else { } else {
StandaloneDocker::create([ StandaloneDocker::forceCreate([
'id' => 0, 'id' => 0,
'name' => 'coolify', 'name' => 'coolify',
'network' => 'coolify', 'network' => 'coolify',
@ -164,13 +164,14 @@ protected static function booted()
} }
} else { } else {
if ($server->isSwarm()) { if ($server->isSwarm()) {
SwarmDocker::create([ SwarmDocker::forceCreate([
'name' => 'coolify-overlay', 'name' => 'coolify-overlay',
'network' => 'coolify-overlay', 'network' => 'coolify-overlay',
'server_id' => $server->id, 'server_id' => $server->id,
]); ]);
} else { } else {
$standaloneDocker = new StandaloneDocker([ $standaloneDocker = new StandaloneDocker;
$standaloneDocker->forceFill([
'name' => 'coolify', 'name' => 'coolify',
'uuid' => (string) new Cuid2, 'uuid' => (string) new Cuid2,
'network' => 'coolify', 'network' => 'coolify',

View file

@ -22,25 +22,25 @@
* *
* @param string $composeYaml The raw Docker Compose YAML content * @param string $composeYaml The raw Docker Compose YAML content
* *
* @throws \Exception If the compose file contains command injection attempts * @throws Exception If the compose file contains command injection attempts
*/ */
function validateDockerComposeForInjection(string $composeYaml): void function validateDockerComposeForInjection(string $composeYaml): void
{ {
try { try {
$parsed = Yaml::parse($composeYaml); $parsed = Yaml::parse($composeYaml);
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception('Invalid YAML format: '.$e->getMessage(), 0, $e); throw new Exception('Invalid YAML format: '.$e->getMessage(), 0, $e);
} }
if (! is_array($parsed) || ! isset($parsed['services']) || ! is_array($parsed['services'])) { if (! is_array($parsed) || ! isset($parsed['services']) || ! is_array($parsed['services'])) {
throw new \Exception('Docker Compose file must contain a "services" section'); throw new Exception('Docker Compose file must contain a "services" section');
} }
// Validate service names // Validate service names
foreach ($parsed['services'] as $serviceName => $serviceConfig) { foreach ($parsed['services'] as $serviceName => $serviceConfig) {
try { try {
validateShellSafePath($serviceName, 'service name'); validateShellSafePath($serviceName, 'service name');
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception( throw new Exception(
'Invalid Docker Compose service name: '.$e->getMessage(). 'Invalid Docker Compose service name: '.$e->getMessage().
' Service names must not contain shell metacharacters.', ' Service names must not contain shell metacharacters.',
0, 0,
@ -68,8 +68,8 @@ function validateDockerComposeForInjection(string $composeYaml): void
if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try { try {
validateShellSafePath($source, 'volume source'); validateShellSafePath($source, 'volume source');
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception( throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage(). 'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.', ' Please use safe path names without shell metacharacters.',
0, 0,
@ -84,8 +84,8 @@ function validateDockerComposeForInjection(string $composeYaml): void
if (is_string($target)) { if (is_string($target)) {
try { try {
validateShellSafePath($target, 'volume target'); validateShellSafePath($target, 'volume target');
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception( throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage(). 'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.', ' Please use safe path names without shell metacharacters.',
0, 0,
@ -105,7 +105,7 @@ function validateDockerComposeForInjection(string $composeYaml): void
* *
* @param string $volumeString The volume string to validate * @param string $volumeString The volume string to validate
* *
* @throws \Exception If the volume string contains command injection attempts * @throws Exception If the volume string contains command injection attempts
*/ */
function validateVolumeStringForInjection(string $volumeString): void function validateVolumeStringForInjection(string $volumeString): void
{ {
@ -325,9 +325,9 @@ function parseDockerVolumeString(string $volumeString): array
if (! $isSimpleEnvVar && ! $isEnvVarWithPath) { if (! $isSimpleEnvVar && ! $isEnvVarWithPath) {
try { try {
validateShellSafePath($sourceStr, 'volume source'); validateShellSafePath($sourceStr, 'volume source');
} catch (\Exception $e) { } catch (Exception $e) {
// Re-throw with more context about the volume string // Re-throw with more context about the volume string
throw new \Exception( throw new Exception(
'Invalid Docker volume definition: '.$e->getMessage(). 'Invalid Docker volume definition: '.$e->getMessage().
' Please use safe path names without shell metacharacters.' ' Please use safe path names without shell metacharacters.'
); );
@ -343,8 +343,8 @@ function parseDockerVolumeString(string $volumeString): array
// Still, defense in depth is important // Still, defense in depth is important
try { try {
validateShellSafePath($targetStr, 'volume target'); validateShellSafePath($targetStr, 'volume target');
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception( throw new Exception(
'Invalid Docker volume definition: '.$e->getMessage(). 'Invalid Docker volume definition: '.$e->getMessage().
' Please use safe path names without shell metacharacters.' ' Please use safe path names without shell metacharacters.'
); );
@ -375,7 +375,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
try { try {
$yaml = Yaml::parse($compose); $yaml = Yaml::parse($compose);
} catch (\Exception) { } catch (Exception) {
return collect([]); return collect([]);
} }
$services = data_get($yaml, 'services', collect([])); $services = data_get($yaml, 'services', collect([]));
@ -409,8 +409,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
// Validate service name for command injection // Validate service name for command injection
try { try {
validateShellSafePath($serviceName, 'service name'); validateShellSafePath($serviceName, 'service name');
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception( throw new Exception(
'Invalid Docker Compose service name: '.$e->getMessage(). 'Invalid Docker Compose service name: '.$e->getMessage().
' Service names must not contain shell metacharacters.' ' Service names must not contain shell metacharacters.'
); );
@ -465,7 +465,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$fqdn = generateFqdn(server: $server, random: "$uuid", parserVersion: $resource->compose_parsing_version); $fqdn = generateFqdn(server: $server, random: "$uuid", parserVersion: $resource->compose_parsing_version);
} }
if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { if ($value && get_class($value) === Illuminate\Support\Stringable::class && $value->startsWith('/')) {
$path = $value->value(); $path = $value->value();
if ($path !== '/') { if ($path !== '/') {
$fqdn = "$fqdn$path"; $fqdn = "$fqdn$path";
@ -738,8 +738,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try { try {
validateShellSafePath($sourceValue, 'volume source'); validateShellSafePath($sourceValue, 'volume source');
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception( throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage(). 'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.' ' Please use safe path names without shell metacharacters.'
); );
@ -749,8 +749,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
if ($target !== null && ! empty($target->value())) { if ($target !== null && ! empty($target->value())) {
try { try {
validateShellSafePath($target->value(), 'volume target'); validateShellSafePath($target->value(), 'volume target');
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception( throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage(). 'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.' ' Please use safe path names without shell metacharacters.'
); );
@ -1489,7 +1489,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
} }
} }
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2); $resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
} catch (\Exception $e) { } catch (Exception $e) {
// If parsing fails, keep the original docker_compose_raw unchanged // If parsing fails, keep the original docker_compose_raw unchanged
ray('Failed to update docker_compose_raw in applicationParser: '.$e->getMessage()); ray('Failed to update docker_compose_raw in applicationParser: '.$e->getMessage());
} }
@ -1519,7 +1519,7 @@ function serviceParser(Service $resource): Collection
try { try {
$yaml = Yaml::parse($compose); $yaml = Yaml::parse($compose);
} catch (\Exception) { } catch (Exception) {
return collect([]); return collect([]);
} }
$services = data_get($yaml, 'services', collect([])); $services = data_get($yaml, 'services', collect([]));
@ -1566,8 +1566,8 @@ function serviceParser(Service $resource): Collection
// Validate service name for command injection // Validate service name for command injection
try { try {
validateShellSafePath($serviceName, 'service name'); validateShellSafePath($serviceName, 'service name');
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception( throw new Exception(
'Invalid Docker Compose service name: '.$e->getMessage(). 'Invalid Docker Compose service name: '.$e->getMessage().
' Service names must not contain shell metacharacters.' ' Service names must not contain shell metacharacters.'
); );
@ -1593,20 +1593,25 @@ function serviceParser(Service $resource): Collection
// Use image detection for non-migrated services // Use image detection for non-migrated services
$isDatabase = isDatabaseImage($image, $service); $isDatabase = isDatabaseImage($image, $service);
if ($isDatabase) { if ($isDatabase) {
$applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); $databaseFound = ServiceDatabase::where('name', $serviceName)->where('service_id', $resource->id)->first();
if ($applicationFound) { if ($databaseFound) {
$savedService = $applicationFound; $savedService = $databaseFound;
} else { } else {
$savedService = ServiceDatabase::firstOrCreate([ $savedService = ServiceDatabase::forceCreate([
'name' => $serviceName, 'name' => $serviceName,
'service_id' => $resource->id, 'service_id' => $resource->id,
]); ]);
} }
} else { } else {
$savedService = ServiceApplication::firstOrCreate([ $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
'name' => $serviceName, if ($applicationFound) {
'service_id' => $resource->id, $savedService = $applicationFound;
]); } else {
$savedService = ServiceApplication::forceCreate([
'name' => $serviceName,
'service_id' => $resource->id,
]);
}
} }
} }
// Update image if it changed // Update image if it changed
@ -1772,7 +1777,7 @@ function serviceParser(Service $resource): Collection
// Strip scheme for environment variable values // Strip scheme for environment variable values
$fqdnValueForEnv = str($fqdn)->after('://')->value(); $fqdnValueForEnv = str($fqdn)->after('://')->value();
if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { if ($value && get_class($value) === Illuminate\Support\Stringable::class && $value->startsWith('/')) {
$path = $value->value(); $path = $value->value();
if ($path !== '/') { if ($path !== '/') {
// Only add path if it's not already present (prevents duplication on subsequent parse() calls) // Only add path if it's not already present (prevents duplication on subsequent parse() calls)
@ -2120,8 +2125,8 @@ function serviceParser(Service $resource): Collection
if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try { try {
validateShellSafePath($sourceValue, 'volume source'); validateShellSafePath($sourceValue, 'volume source');
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception( throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage(). 'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.' ' Please use safe path names without shell metacharacters.'
); );
@ -2131,8 +2136,8 @@ function serviceParser(Service $resource): Collection
if ($target !== null && ! empty($target->value())) { if ($target !== null && ! empty($target->value())) {
try { try {
validateShellSafePath($target->value(), 'volume target'); validateShellSafePath($target->value(), 'volume target');
} catch (\Exception $e) { } catch (Exception $e) {
throw new \Exception( throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage(). 'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.' ' Please use safe path names without shell metacharacters.'
); );
@ -2741,7 +2746,7 @@ function serviceParser(Service $resource): Collection
} }
} }
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2); $resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
} catch (\Exception $e) { } catch (Exception $e) {
// If parsing fails, keep the original docker_compose_raw unchanged // If parsing fails, keep the original docker_compose_raw unchanged
ray('Failed to update docker_compose_raw in serviceParser: '.$e->getMessage()); ray('Failed to update docker_compose_raw in serviceParser: '.$e->getMessage());
} }

View file

@ -2722,8 +2722,7 @@
}, },
"is_preserve_repository_enabled": { "is_preserve_repository_enabled": {
"type": "boolean", "type": "boolean",
"default": false, "description": "Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false."
"description": "Preserve repository during deployment."
} }
}, },
"type": "object" "type": "object"

View file

@ -1755,8 +1755,7 @@ paths:
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.' description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
is_preserve_repository_enabled: is_preserve_repository_enabled:
type: boolean type: boolean
default: false description: 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'
description: 'Preserve repository during deployment.'
type: object type: object
responses: responses:
'200': '200':

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -25,13 +25,13 @@
$this->server = Server::factory()->create(['team_id' => $this->team->id]); $this->server = Server::factory()->create(['team_id' => $this->team->id]);
StandaloneDocker::withoutEvents(function () { StandaloneDocker::withoutEvents(function () {
$this->destination = StandaloneDocker::firstOrCreate( $this->destination = $this->server->standaloneDockers()->firstOrCreate(
['server_id' => $this->server->id, 'network' => 'coolify'], ['network' => 'coolify'],
['uuid' => (string) new Cuid2, 'name' => 'test-docker'] ['uuid' => (string) new Cuid2, 'name' => 'test-docker']
); );
}); });
$this->project = Project::create([ $this->project = Project::forceCreate([
'uuid' => (string) new Cuid2, 'uuid' => (string) new Cuid2,
'name' => 'test-project', 'name' => 'test-project',
'team_id' => $this->team->id, 'team_id' => $this->team->id,

View file

@ -14,9 +14,10 @@
]), ]),
]); ]);
$preview = ApplicationPreview::create([ $preview = ApplicationPreview::forceCreate([
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => 42, 'pull_request_id' => 42,
'pull_request_html_url' => 'https://github.com/example/repo/pull/42',
'docker_compose_domains' => $application->docker_compose_domains, 'docker_compose_domains' => $application->docker_compose_domains,
]); ]);
@ -38,9 +39,10 @@
]), ]),
]); ]);
$preview = ApplicationPreview::create([ $preview = ApplicationPreview::forceCreate([
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => 7, 'pull_request_id' => 7,
'pull_request_html_url' => 'https://github.com/example/repo/pull/7',
'docker_compose_domains' => $application->docker_compose_domains, 'docker_compose_domains' => $application->docker_compose_domains,
]); ]);
@ -63,9 +65,10 @@
]), ]),
]); ]);
$preview = ApplicationPreview::create([ $preview = ApplicationPreview::forceCreate([
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => 99, 'pull_request_id' => 99,
'pull_request_html_url' => 'https://github.com/example/repo/pull/99',
'docker_compose_domains' => $application->docker_compose_domains, 'docker_compose_domains' => $application->docker_compose_domains,
]); ]);

View file

@ -33,7 +33,7 @@
function createDatabase($context): StandalonePostgresql function createDatabase($context): StandalonePostgresql
{ {
return StandalonePostgresql::create([ return StandalonePostgresql::forceCreate([
'name' => 'test-postgres', 'name' => 'test-postgres',
'image' => 'postgres:15-alpine', 'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres', 'postgres_user' => 'postgres',

View file

@ -33,7 +33,7 @@
describe('PATCH /api/v1/databases', function () { describe('PATCH /api/v1/databases', function () {
test('updates public_port_timeout on a postgresql database', function () { test('updates public_port_timeout on a postgresql database', function () {
$database = StandalonePostgresql::create([ $database = StandalonePostgresql::forceCreate([
'name' => 'test-postgres', 'name' => 'test-postgres',
'image' => 'postgres:15-alpine', 'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres', 'postgres_user' => 'postgres',
@ -57,7 +57,7 @@
}); });
test('updates public_port_timeout on a redis database', function () { test('updates public_port_timeout on a redis database', function () {
$database = StandaloneRedis::create([ $database = StandaloneRedis::forceCreate([
'name' => 'test-redis', 'name' => 'test-redis',
'image' => 'redis:7', 'image' => 'redis:7',
'redis_password' => 'password', 'redis_password' => 'password',
@ -79,7 +79,7 @@
}); });
test('rejects invalid public_port_timeout value', function () { test('rejects invalid public_port_timeout value', function () {
$database = StandalonePostgresql::create([ $database = StandalonePostgresql::forceCreate([
'name' => 'test-postgres', 'name' => 'test-postgres',
'image' => 'postgres:15-alpine', 'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres', 'postgres_user' => 'postgres',
@ -101,7 +101,7 @@
}); });
test('accepts null public_port_timeout', function () { test('accepts null public_port_timeout', function () {
$database = StandalonePostgresql::create([ $database = StandalonePostgresql::forceCreate([
'name' => 'test-postgres', 'name' => 'test-postgres',
'image' => 'postgres:15-alpine', 'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres', 'postgres_user' => 'postgres',

View file

@ -0,0 +1,73 @@
<?php
use App\Models\Application;
use App\Models\ApplicationSetting;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('creates application settings for internally created applications', function () {
$team = Team::factory()->create();
$project = Project::factory()->create([
'team_id' => $team->id,
]);
$environment = Environment::factory()->create([
'project_id' => $project->id,
]);
$server = Server::factory()->create([
'team_id' => $team->id,
]);
$destination = $server->standaloneDockers()->firstOrFail();
$application = Application::forceCreate([
'name' => 'internal-app',
'git_repository' => 'https://github.com/coollabsio/coolify',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
$setting = ApplicationSetting::query()
->where('application_id', $application->id)
->first();
expect($application->environment_id)->toBe($environment->id);
expect($setting)->not->toBeNull();
expect($setting?->application_id)->toBe($application->id);
});
it('creates services with protected relationship ids in trusted internal paths', function () {
$team = Team::factory()->create();
$project = Project::factory()->create([
'team_id' => $team->id,
]);
$environment = Environment::factory()->create([
'project_id' => $project->id,
]);
$server = Server::factory()->create([
'team_id' => $team->id,
]);
$destination = $server->standaloneDockers()->firstOrFail();
$service = Service::forceCreate([
'docker_compose_raw' => 'services: {}',
'environment_id' => $environment->id,
'server_id' => $server->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
'service_type' => 'test-service',
]);
expect($service->environment_id)->toBe($environment->id);
expect($service->server_id)->toBe($server->id);
expect($service->destination_id)->toBe($destination->id);
expect($service->destination_type)->toBe($destination->getMorphClass());
});

View file

@ -7,25 +7,26 @@
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use App\Models\Team; use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
it('returns the correct team through the service relationship chain', function () { it('returns the correct team through the service relationship chain', function () {
$team = Team::factory()->create(); $team = Team::factory()->create();
$project = Project::create([ $project = Project::forceCreate([
'uuid' => (string) Illuminate\Support\Str::uuid(), 'uuid' => (string) Str::uuid(),
'name' => 'Test Project', 'name' => 'Test Project',
'team_id' => $team->id, 'team_id' => $team->id,
]); ]);
$environment = Environment::create([ $environment = Environment::forceCreate([
'name' => 'test-env-'.Illuminate\Support\Str::random(8), 'name' => 'test-env-'.Str::random(8),
'project_id' => $project->id, 'project_id' => $project->id,
]); ]);
$service = Service::create([ $service = Service::forceCreate([
'uuid' => (string) Illuminate\Support\Str::uuid(), 'uuid' => (string) Str::uuid(),
'name' => 'supabase', 'name' => 'supabase',
'environment_id' => $environment->id, 'environment_id' => $environment->id,
'destination_id' => 1, 'destination_id' => 1,
@ -33,8 +34,8 @@
'docker_compose_raw' => 'version: "3"', 'docker_compose_raw' => 'version: "3"',
]); ]);
$serviceDatabase = ServiceDatabase::create([ $serviceDatabase = ServiceDatabase::forceCreate([
'uuid' => (string) Illuminate\Support\Str::uuid(), 'uuid' => (string) Str::uuid(),
'name' => 'supabase-db', 'name' => 'supabase-db',
'service_id' => $service->id, 'service_id' => $service->id,
]); ]);
@ -46,19 +47,19 @@
it('returns the correct team for ServiceApplication through the service relationship chain', function () { it('returns the correct team for ServiceApplication through the service relationship chain', function () {
$team = Team::factory()->create(); $team = Team::factory()->create();
$project = Project::create([ $project = Project::forceCreate([
'uuid' => (string) Illuminate\Support\Str::uuid(), 'uuid' => (string) Str::uuid(),
'name' => 'Test Project', 'name' => 'Test Project',
'team_id' => $team->id, 'team_id' => $team->id,
]); ]);
$environment = Environment::create([ $environment = Environment::forceCreate([
'name' => 'test-env-'.Illuminate\Support\Str::random(8), 'name' => 'test-env-'.Str::random(8),
'project_id' => $project->id, 'project_id' => $project->id,
]); ]);
$service = Service::create([ $service = Service::forceCreate([
'uuid' => (string) Illuminate\Support\Str::uuid(), 'uuid' => (string) Str::uuid(),
'name' => 'supabase', 'name' => 'supabase',
'environment_id' => $environment->id, 'environment_id' => $environment->id,
'destination_id' => 1, 'destination_id' => 1,
@ -66,8 +67,8 @@
'docker_compose_raw' => 'version: "3"', 'docker_compose_raw' => 'version: "3"',
]); ]);
$serviceApplication = ServiceApplication::create([ $serviceApplication = ServiceApplication::forceCreate([
'uuid' => (string) Illuminate\Support\Str::uuid(), 'uuid' => (string) Str::uuid(),
'name' => 'supabase-studio', 'name' => 'supabase-studio',
'service_id' => $service->id, 'service_id' => $service->id,
]); ]);

View file

@ -49,7 +49,7 @@ function createTestApplication($context): Application
function createTestDatabase($context): StandalonePostgresql function createTestDatabase($context): StandalonePostgresql
{ {
return StandalonePostgresql::create([ return StandalonePostgresql::forceCreate([
'name' => 'test-postgres', 'name' => 'test-postgres',
'image' => 'postgres:15-alpine', 'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres', 'postgres_user' => 'postgres',

View file

@ -7,22 +7,24 @@
* These tests verify the fix for the issue where changing an image in a * These tests verify the fix for the issue where changing an image in a
* docker-compose file would create a new service instead of updating the existing one. * docker-compose file would create a new service instead of updating the existing one.
*/ */
it('ensures service parser does not include image in firstOrCreate query', function () { it('ensures service parser does not include image in trusted service creation query', function () {
// Read the serviceParser function from parsers.php // Read the serviceParser function from parsers.php
$parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php'); $parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php');
// Check that firstOrCreate is called with only name and service_id // Check that trusted creation only uses name and service_id
// and NOT with image parameter in the ServiceApplication presave loop // and does not include image in the creation payload
expect($parsersFile) expect($parsersFile)
->toContain("firstOrCreate([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);") ->toContain("\$databaseFound = ServiceDatabase::where('name', \$serviceName)->where('service_id', \$resource->id)->first();")
->not->toContain("firstOrCreate([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);"); ->toContain("\$applicationFound = ServiceApplication::where('name', \$serviceName)->where('service_id', \$resource->id)->first();")
->toContain("forceCreate([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);")
->not->toContain("forceCreate([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);");
}); });
it('ensures service parser updates image after finding or creating service', function () { it('ensures service parser updates image after finding or creating service', function () {
// Read the serviceParser function from parsers.php // Read the serviceParser function from parsers.php
$parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php'); $parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php');
// Check that image update logic exists after firstOrCreate // Check that image update logic exists after the trusted create/find branch
expect($parsersFile) expect($parsersFile)
->toContain('// Update image if it changed') ->toContain('// Update image if it changed')
->toContain('if ($savedService->image !== $image) {') ->toContain('if ($savedService->image !== $image) {')

View file

@ -77,21 +77,21 @@
], ],
]); ]);
Project::create([ Project::forceCreate([
'uuid' => 'project-1', 'uuid' => 'project-1',
'name' => 'My first project', 'name' => 'My first project',
'description' => 'This is a test project in development', 'description' => 'This is a test project in development',
'team_id' => 0, 'team_id' => 0,
]); ]);
Project::create([ Project::forceCreate([
'uuid' => 'project-2', 'uuid' => 'project-2',
'name' => 'Production API', 'name' => 'Production API',
'description' => 'Backend services for production', 'description' => 'Backend services for production',
'team_id' => 0, 'team_id' => 0,
]); ]);
Project::create([ Project::forceCreate([
'uuid' => 'project-3', 'uuid' => 'project-3',
'name' => 'Staging Environment', 'name' => 'Staging Environment',
'description' => 'Staging and QA testing', 'description' => 'Staging and QA testing',