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);
if ($found->count() == 0 && $server->id) {
StandaloneDocker::create([
StandaloneDocker::forceCreate([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,

View file

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

View file

@ -5,6 +5,7 @@
use App\Http\Controllers\Controller;
use App\Models\Project;
use App\Support\ValidationPatterns;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use OpenApi\Attributes as OA;
@ -234,7 +235,7 @@ public function create_project(Request $request)
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
if ($return instanceof JsonResponse) {
return $return;
}
$validator = Validator::make($request->all(), [
@ -257,7 +258,7 @@ public function create_project(Request $request)
], 422);
}
$project = Project::create([
$project = Project::forceCreate([
'name' => $request->name,
'description' => $request->description,
'team_id' => $teamId,
@ -347,7 +348,7 @@ public function update_project(Request $request)
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
if ($return instanceof JsonResponse) {
return $return;
}
$validator = Validator::make($request->all(), [
@ -600,7 +601,7 @@ public function create_environment(Request $request)
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
if ($return instanceof JsonResponse) {
return $return;
}
$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)) {
data_set($servicePayload, 'connect_to_docker_network', true);
}
$service = Service::create($servicePayload);
$service = Service::forceCreate($servicePayload);
$service->name = $request->name ?? "$oneClickServiceName-".$service->uuid;
$service->description = $request->description;
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();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::create([
$pr_app = ApplicationPreview::forceCreate([
'git_type' => 'bitbucket',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@ -128,7 +128,7 @@ public function manual(Request $request)
]);
$pr_app->generate_preview_fqdn_compose();
} else {
$pr_app = ApplicationPreview::create([
$pr_app = ApplicationPreview::forceCreate([
'git_type' => 'bitbucket',
'application_id' => $application->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();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::create([
$pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitea',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@ -153,7 +153,7 @@ public function manual(Request $request)
]);
$pr_app->generate_preview_fqdn_compose();
} else {
$pr_app = ApplicationPreview::create([
$pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitea',
'application_id' => $application->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();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::create([
$pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitlab',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@ -186,7 +186,7 @@ public function manual(Request $request)
]);
$pr_app->generate_preview_fqdn_compose();
} else {
$pr_app = ApplicationPreview::create([
$pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitlab',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,

View file

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

View file

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

View file

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

View file

@ -30,7 +30,7 @@ public function submit()
{
try {
$this->validate();
$project = Project::create([
$project = Project::forceCreate([
'name' => $this->name,
'description' => $this->description,
'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();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && ! is_null($pull_request_html_url)) {
$found = ApplicationPreview::create([
$found = ApplicationPreview::forceCreate([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'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();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && ! is_null($pull_request_html_url)) {
$found = ApplicationPreview::create([
$found = ApplicationPreview::forceCreate([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'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();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && ! is_null($pull_request_html_url)) {
ApplicationPreview::create([
ApplicationPreview::forceCreate([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,

View file

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

View file

@ -54,7 +54,7 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
$service = Service::create([
$service = Service::forceCreate([
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->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
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
$application = Application::create([
$application = Application::forceCreate([
'name' => 'docker-image-'.new Cuid2,
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',

View file

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

View file

@ -8,6 +8,7 @@
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch;
use App\Support\ValidationPatterns;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
@ -168,7 +169,7 @@ public function submit()
'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/',
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(),
'docker_compose_location' => ValidationPatterns::filePathRules(),
]);
if ($validator->fails()) {
@ -188,7 +189,7 @@ public function submit()
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_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),
'repository_project_id' => $this->selected_repository_id,
'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\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
use App\Support\ValidationPatterns;
use Illuminate\Support\Str;
use Livewire\Component;
use Spatie\Url\Url;
@ -66,7 +67,7 @@ protected function rules()
'is_static' => 'required|boolean',
'publish_directory' => 'nullable|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['base_directory'] = $this->base_directory;
}
$application = Application::create($application_init);
$application = Application::forceCreate($application_init);
$application->settings->is_static = $this->is_static;
$application->settings->save();

View file

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

View file

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

View file

@ -91,7 +91,7 @@ public function mount()
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
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->save();
if ($oneClickDotEnvs?->count() > 0) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,25 +22,25 @@
*
* @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
{
try {
$parsed = Yaml::parse($composeYaml);
} catch (\Exception $e) {
throw new \Exception('Invalid YAML format: '.$e->getMessage(), 0, $e);
} catch (Exception $e) {
throw new Exception('Invalid YAML format: '.$e->getMessage(), 0, $e);
}
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
foreach ($parsed['services'] as $serviceName => $serviceConfig) {
try {
validateShellSafePath($serviceName, 'service name');
} catch (\Exception $e) {
throw new \Exception(
} catch (Exception $e) {
throw new Exception(
'Invalid Docker Compose service name: '.$e->getMessage().
' Service names must not contain shell metacharacters.',
0,
@ -68,8 +68,8 @@ function validateDockerComposeForInjection(string $composeYaml): void
if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try {
validateShellSafePath($source, 'volume source');
} catch (\Exception $e) {
throw new \Exception(
} catch (Exception $e) {
throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.',
0,
@ -84,8 +84,8 @@ function validateDockerComposeForInjection(string $composeYaml): void
if (is_string($target)) {
try {
validateShellSafePath($target, 'volume target');
} catch (\Exception $e) {
throw new \Exception(
} catch (Exception $e) {
throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.',
0,
@ -105,7 +105,7 @@ function validateDockerComposeForInjection(string $composeYaml): void
*
* @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
{
@ -325,9 +325,9 @@ function parseDockerVolumeString(string $volumeString): array
if (! $isSimpleEnvVar && ! $isEnvVarWithPath) {
try {
validateShellSafePath($sourceStr, 'volume source');
} catch (\Exception $e) {
} catch (Exception $e) {
// Re-throw with more context about the volume string
throw new \Exception(
throw new Exception(
'Invalid Docker volume definition: '.$e->getMessage().
' Please use safe path names without shell metacharacters.'
);
@ -343,8 +343,8 @@ function parseDockerVolumeString(string $volumeString): array
// Still, defense in depth is important
try {
validateShellSafePath($targetStr, 'volume target');
} catch (\Exception $e) {
throw new \Exception(
} catch (Exception $e) {
throw new Exception(
'Invalid Docker volume definition: '.$e->getMessage().
' Please use safe path names without shell metacharacters.'
);
@ -375,7 +375,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
try {
$yaml = Yaml::parse($compose);
} catch (\Exception) {
} catch (Exception) {
return 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
try {
validateShellSafePath($serviceName, 'service name');
} catch (\Exception $e) {
throw new \Exception(
} catch (Exception $e) {
throw new Exception(
'Invalid Docker Compose service name: '.$e->getMessage().
' 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);
}
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();
if ($path !== '/') {
$fqdn = "$fqdn$path";
@ -738,8 +738,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try {
validateShellSafePath($sourceValue, 'volume source');
} catch (\Exception $e) {
throw new \Exception(
} catch (Exception $e) {
throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' 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())) {
try {
validateShellSafePath($target->value(), 'volume target');
} catch (\Exception $e) {
throw new \Exception(
} catch (Exception $e) {
throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' 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);
} catch (\Exception $e) {
} catch (Exception $e) {
// If parsing fails, keep the original docker_compose_raw unchanged
ray('Failed to update docker_compose_raw in applicationParser: '.$e->getMessage());
}
@ -1519,7 +1519,7 @@ function serviceParser(Service $resource): Collection
try {
$yaml = Yaml::parse($compose);
} catch (\Exception) {
} catch (Exception) {
return collect([]);
}
$services = data_get($yaml, 'services', collect([]));
@ -1566,8 +1566,8 @@ function serviceParser(Service $resource): Collection
// Validate service name for command injection
try {
validateShellSafePath($serviceName, 'service name');
} catch (\Exception $e) {
throw new \Exception(
} catch (Exception $e) {
throw new Exception(
'Invalid Docker Compose service name: '.$e->getMessage().
' Service names must not contain shell metacharacters.'
);
@ -1593,20 +1593,25 @@ function serviceParser(Service $resource): Collection
// Use image detection for non-migrated services
$isDatabase = isDatabaseImage($image, $service);
if ($isDatabase) {
$applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
if ($applicationFound) {
$savedService = $applicationFound;
$databaseFound = ServiceDatabase::where('name', $serviceName)->where('service_id', $resource->id)->first();
if ($databaseFound) {
$savedService = $databaseFound;
} else {
$savedService = ServiceDatabase::firstOrCreate([
$savedService = ServiceDatabase::forceCreate([
'name' => $serviceName,
'service_id' => $resource->id,
]);
}
} else {
$savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName,
'service_id' => $resource->id,
]);
$applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
if ($applicationFound) {
$savedService = $applicationFound;
} else {
$savedService = ServiceApplication::forceCreate([
'name' => $serviceName,
'service_id' => $resource->id,
]);
}
}
}
// Update image if it changed
@ -1772,7 +1777,7 @@ function serviceParser(Service $resource): Collection
// Strip scheme for environment variable values
$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();
if ($path !== '/') {
// 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) {
try {
validateShellSafePath($sourceValue, 'volume source');
} catch (\Exception $e) {
throw new \Exception(
} catch (Exception $e) {
throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.'
);
@ -2131,8 +2136,8 @@ function serviceParser(Service $resource): Collection
if ($target !== null && ! empty($target->value())) {
try {
validateShellSafePath($target->value(), 'volume target');
} catch (\Exception $e) {
throw new \Exception(
} catch (Exception $e) {
throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' 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);
} catch (\Exception $e) {
} catch (Exception $e) {
// If parsing fails, keep the original docker_compose_raw unchanged
ray('Failed to update docker_compose_raw in serviceParser: '.$e->getMessage());
}

View file

@ -2722,8 +2722,7 @@
},
"is_preserve_repository_enabled": {
"type": "boolean",
"default": false,
"description": "Preserve repository during deployment."
"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."
}
},
"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.'
is_preserve_repository_enabled:
type: boolean
default: false
description: 'Preserve repository during deployment.'
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.'
type: object
responses:
'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]);
StandaloneDocker::withoutEvents(function () {
$this->destination = StandaloneDocker::firstOrCreate(
['server_id' => $this->server->id, 'network' => 'coolify'],
$this->destination = $this->server->standaloneDockers()->firstOrCreate(
['network' => 'coolify'],
['uuid' => (string) new Cuid2, 'name' => 'test-docker']
);
});
$this->project = Project::create([
$this->project = Project::forceCreate([
'uuid' => (string) new Cuid2,
'name' => 'test-project',
'team_id' => $this->team->id,

View file

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

View file

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

View file

@ -33,7 +33,7 @@
describe('PATCH /api/v1/databases', function () {
test('updates public_port_timeout on a postgresql database', function () {
$database = StandalonePostgresql::create([
$database = StandalonePostgresql::forceCreate([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@ -57,7 +57,7 @@
});
test('updates public_port_timeout on a redis database', function () {
$database = StandaloneRedis::create([
$database = StandaloneRedis::forceCreate([
'name' => 'test-redis',
'image' => 'redis:7',
'redis_password' => 'password',
@ -79,7 +79,7 @@
});
test('rejects invalid public_port_timeout value', function () {
$database = StandalonePostgresql::create([
$database = StandalonePostgresql::forceCreate([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@ -101,7 +101,7 @@
});
test('accepts null public_port_timeout', function () {
$database = StandalonePostgresql::create([
$database = StandalonePostgresql::forceCreate([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'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\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
uses(RefreshDatabase::class);
it('returns the correct team through the service relationship chain', function () {
$team = Team::factory()->create();
$project = Project::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$project = Project::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'Test Project',
'team_id' => $team->id,
]);
$environment = Environment::create([
'name' => 'test-env-'.Illuminate\Support\Str::random(8),
$environment = Environment::forceCreate([
'name' => 'test-env-'.Str::random(8),
'project_id' => $project->id,
]);
$service = Service::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$service = Service::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'supabase',
'environment_id' => $environment->id,
'destination_id' => 1,
@ -33,8 +34,8 @@
'docker_compose_raw' => 'version: "3"',
]);
$serviceDatabase = ServiceDatabase::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$serviceDatabase = ServiceDatabase::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'supabase-db',
'service_id' => $service->id,
]);
@ -46,19 +47,19 @@
it('returns the correct team for ServiceApplication through the service relationship chain', function () {
$team = Team::factory()->create();
$project = Project::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$project = Project::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'Test Project',
'team_id' => $team->id,
]);
$environment = Environment::create([
'name' => 'test-env-'.Illuminate\Support\Str::random(8),
$environment = Environment::forceCreate([
'name' => 'test-env-'.Str::random(8),
'project_id' => $project->id,
]);
$service = Service::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$service = Service::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'supabase',
'environment_id' => $environment->id,
'destination_id' => 1,
@ -66,8 +67,8 @@
'docker_compose_raw' => 'version: "3"',
]);
$serviceApplication = ServiceApplication::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$serviceApplication = ServiceApplication::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'supabase-studio',
'service_id' => $service->id,
]);

View file

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

View file

@ -7,22 +7,24 @@
* 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.
*/
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
$parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php');
// Check that firstOrCreate is called with only name and service_id
// and NOT with image parameter in the ServiceApplication presave loop
// Check that trusted creation only uses name and service_id
// and does not include image in the creation payload
expect($parsersFile)
->toContain("firstOrCreate([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);")
->not->toContain("firstOrCreate([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);");
->toContain("\$databaseFound = ServiceDatabase::where('name', \$serviceName)->where('service_id', \$resource->id)->first();")
->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 () {
// Read the serviceParser function from 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)
->toContain('// Update image if it changed')
->toContain('if ($savedService->image !== $image) {')

View file

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