refactor: define explicit fillable attributes on all Eloquent models (#9282)

This commit is contained in:
Andras Bacsai 2026-03-30 13:11:58 +02:00 committed by GitHub
commit 3fddc795f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 688 additions and 168 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

@ -39,7 +39,32 @@
)] )]
class ApplicationDeploymentQueue extends Model class ApplicationDeploymentQueue extends Model
{ {
protected $guarded = []; protected $fillable = [
'application_id',
'deployment_uuid',
'pull_request_id',
'force_rebuild',
'commit',
'status',
'is_webhook',
'logs',
'current_process_id',
'restart_only',
'git_type',
'server_id',
'application_name',
'server_name',
'deployment_url',
'destination_id',
'only_this_server',
'rollback',
'commit_message',
'is_api',
'build_server_id',
'horizon_job_id',
'horizon_job_worker',
'finished_at',
];
protected $casts = [ protected $casts = [
'finished_at' => 'datetime', 'finished_at' => 'datetime',

View file

@ -10,7 +10,16 @@ class ApplicationPreview extends BaseModel
{ {
use SoftDeletes; use SoftDeletes;
protected $guarded = []; protected $fillable = [
'pull_request_id',
'pull_request_html_url',
'pull_request_issue_comment_id',
'fqdn',
'status',
'git_type',
'docker_compose_domains',
'last_online_at',
];
protected static function booted() protected static function booted()
{ {
@ -69,7 +78,7 @@ public function application()
public function persistentStorages() public function persistentStorages()
{ {
return $this->morphMany(\App\Models\LocalPersistentVolume::class, 'resource'); return $this->morphMany(LocalPersistentVolume::class, 'resource');
} }
public function generate_preview_fqdn() public function generate_preview_fqdn()

View file

@ -28,7 +28,42 @@ class ApplicationSetting extends Model
'docker_images_to_keep' => 'integer', 'docker_images_to_keep' => 'integer',
]; ];
protected $guarded = []; protected $fillable = [
'is_static',
'is_git_submodules_enabled',
'is_git_lfs_enabled',
'is_auto_deploy_enabled',
'is_force_https_enabled',
'is_debug_enabled',
'is_preview_deployments_enabled',
'is_log_drain_enabled',
'is_gpu_enabled',
'gpu_driver',
'gpu_count',
'gpu_device_ids',
'gpu_options',
'is_include_timestamps',
'is_swarm_only_worker_nodes',
'is_raw_compose_deployment_enabled',
'is_build_server_enabled',
'is_consistent_container_name_enabled',
'is_gzip_enabled',
'is_stripprefix_enabled',
'connect_to_docker_network',
'custom_internal_name',
'is_container_label_escape_enabled',
'is_env_sorting_enabled',
'is_container_label_readonly_enabled',
'is_preserve_repository_enabled',
'disable_build_cache',
'is_spa',
'is_git_shallow_clone_enabled',
'is_pr_deployments_public_enabled',
'use_build_secrets',
'inject_build_args_to_dockerfile',
'include_source_commit_in_build',
'docker_images_to_keep',
];
public function isStatic(): Attribute public function isStatic(): Attribute
{ {

View file

@ -4,7 +4,11 @@
class CloudProviderToken extends BaseModel class CloudProviderToken extends BaseModel
{ {
protected $guarded = []; protected $fillable = [
'provider',
'token',
'name',
];
protected $casts = [ protected $casts = [
'token' => 'encrypted', 'token' => 'encrypted',

View file

@ -24,7 +24,8 @@ class DiscordNotificationSettings extends Model
'backup_failure_discord_notifications', 'backup_failure_discord_notifications',
'scheduled_task_success_discord_notifications', 'scheduled_task_success_discord_notifications',
'scheduled_task_failure_discord_notifications', 'scheduled_task_failure_discord_notifications',
'docker_cleanup_discord_notifications', 'docker_cleanup_success_discord_notifications',
'docker_cleanup_failure_discord_notifications',
'server_disk_usage_discord_notifications', 'server_disk_usage_discord_notifications',
'server_reachable_discord_notifications', 'server_reachable_discord_notifications',
'server_unreachable_discord_notifications', 'server_unreachable_discord_notifications',

View file

@ -6,7 +6,12 @@
class DockerCleanupExecution extends BaseModel class DockerCleanupExecution extends BaseModel
{ {
protected $guarded = []; protected $fillable = [
'status',
'message',
'cleanup_log',
'finished_at',
];
public function server(): BelongsTo public function server(): BelongsTo
{ {

View file

@ -34,7 +34,11 @@ class EmailNotificationSettings extends Model
'backup_failure_email_notifications', 'backup_failure_email_notifications',
'scheduled_task_success_email_notifications', 'scheduled_task_success_email_notifications',
'scheduled_task_failure_email_notifications', 'scheduled_task_failure_email_notifications',
'docker_cleanup_success_email_notifications',
'docker_cleanup_failure_email_notifications',
'server_disk_usage_email_notifications', 'server_disk_usage_email_notifications',
'server_reachable_email_notifications',
'server_unreachable_email_notifications',
'server_patch_email_notifications', 'server_patch_email_notifications',
'traefik_outdated_email_notifications', 'traefik_outdated_email_notifications',
]; ];

View file

@ -25,7 +25,10 @@ class Environment extends BaseModel
use HasFactory; use HasFactory;
use HasSafeStringAttribute; use HasSafeStringAttribute;
protected $guarded = []; protected $fillable = [
'name',
'description',
];
protected static function booted() protected static function booted()
{ {

View file

@ -6,7 +6,25 @@
class GithubApp extends BaseModel class GithubApp extends BaseModel
{ {
protected $guarded = []; protected $fillable = [
'name',
'organization',
'api_url',
'html_url',
'custom_user',
'custom_port',
'app_id',
'installation_id',
'client_id',
'client_secret',
'webhook_secret',
'is_system_wide',
'is_public',
'contents',
'metadata',
'pull_requests',
'administration',
];
protected $appends = ['type']; protected $appends = ['type'];
@ -92,7 +110,7 @@ public function type(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: function () { get: function () {
if ($this->getMorphClass() === \App\Models\GithubApp::class) { if ($this->getMorphClass() === GithubApp::class) {
return 'github'; return 'github';
} }
}, },

View file

@ -4,6 +4,24 @@
class GitlabApp extends BaseModel class GitlabApp extends BaseModel
{ {
protected $fillable = [
'name',
'organization',
'api_url',
'html_url',
'custom_port',
'custom_user',
'is_system_wide',
'is_public',
'app_id',
'app_secret',
'oauth_id',
'group_name',
'public_key',
'webhook_token',
'deploy_key_id',
];
protected $hidden = [ protected $hidden = [
'webhook_token', 'webhook_token',
'app_secret', 'app_secret',

View file

@ -9,7 +9,43 @@
class InstanceSettings extends Model class InstanceSettings extends Model
{ {
protected $guarded = []; protected $fillable = [
'public_ipv4',
'public_ipv6',
'fqdn',
'public_port_min',
'public_port_max',
'do_not_track',
'is_auto_update_enabled',
'is_registration_enabled',
'next_channel',
'smtp_enabled',
'smtp_from_address',
'smtp_from_name',
'smtp_recipients',
'smtp_host',
'smtp_port',
'smtp_encryption',
'smtp_username',
'smtp_password',
'smtp_timeout',
'resend_enabled',
'resend_api_key',
'is_dns_validation_enabled',
'custom_dns_servers',
'instance_name',
'is_api_enabled',
'allowed_ips',
'auto_update_frequency',
'update_check_frequency',
'new_version_available',
'instance_timezone',
'helper_version',
'disable_two_step_confirmation',
'is_sponsorship_popup_enabled',
'dev_helper_version',
'is_wire_navigate_enabled',
];
protected $casts = [ protected $casts = [
'smtp_enabled' => 'boolean', 'smtp_enabled' => 'boolean',

View file

@ -20,7 +20,18 @@ class LocalFileVolume extends BaseModel
use HasFactory; use HasFactory;
protected $guarded = []; protected $fillable = [
'fs_path',
'mount_path',
'content',
'resource_type',
'resource_id',
'is_directory',
'chown',
'chmod',
'is_based_on_git',
'is_preview_suffix_enabled',
];
public $appends = ['is_binary']; public $appends = ['is_binary'];

View file

@ -7,7 +7,15 @@
class LocalPersistentVolume extends BaseModel class LocalPersistentVolume extends BaseModel
{ {
protected $guarded = []; protected $fillable = [
'name',
'mount_path',
'host_path',
'container_id',
'resource_type',
'resource_id',
'is_preview_suffix_enabled',
];
protected $casts = [ protected $casts = [
'is_preview_suffix_enabled' => 'boolean', 'is_preview_suffix_enabled' => 'boolean',

View file

@ -24,7 +24,10 @@ class Project extends BaseModel
use HasFactory; use HasFactory;
use HasSafeStringAttribute; use HasSafeStringAttribute;
protected $guarded = []; protected $fillable = [
'name',
'description',
];
/** /**
* Get query builder for projects owned by current team. * Get query builder for projects owned by current team.
@ -48,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

@ -6,7 +6,7 @@
class ProjectSetting extends Model class ProjectSetting extends Model
{ {
protected $guarded = []; protected $fillable = [];
public function project() public function project()
{ {

View file

@ -25,7 +25,8 @@ class PushoverNotificationSettings extends Model
'backup_failure_pushover_notifications', 'backup_failure_pushover_notifications',
'scheduled_task_success_pushover_notifications', 'scheduled_task_success_pushover_notifications',
'scheduled_task_failure_pushover_notifications', 'scheduled_task_failure_pushover_notifications',
'docker_cleanup_pushover_notifications', 'docker_cleanup_success_pushover_notifications',
'docker_cleanup_failure_pushover_notifications',
'server_disk_usage_pushover_notifications', 'server_disk_usage_pushover_notifications',
'server_reachable_pushover_notifications', 'server_reachable_pushover_notifications',
'server_unreachable_pushover_notifications', 'server_unreachable_pushover_notifications',

View file

@ -12,7 +12,17 @@ class S3Storage extends BaseModel
{ {
use HasFactory, HasSafeStringAttribute; use HasFactory, HasSafeStringAttribute;
protected $guarded = []; protected $fillable = [
'name',
'description',
'region',
'key',
'secret',
'bucket',
'endpoint',
'is_usable',
'unusable_email_sent',
];
protected $casts = [ protected $casts = [
'is_usable' => 'boolean', 'is_usable' => 'boolean',

View file

@ -8,7 +8,25 @@
class ScheduledDatabaseBackup extends BaseModel class ScheduledDatabaseBackup extends BaseModel
{ {
protected $guarded = []; protected $fillable = [
'description',
'enabled',
'save_s3',
'frequency',
'database_backup_retention_amount_locally',
'database_type',
'database_id',
's3_storage_id',
'databases_to_backup',
'dump_all',
'database_backup_retention_days_locally',
'database_backup_retention_max_storage_locally',
'database_backup_retention_amount_s3',
'database_backup_retention_days_s3',
'database_backup_retention_max_storage_s3',
'timeout',
'disable_local_backup',
];
public static function ownedByCurrentTeam() public static function ownedByCurrentTeam()
{ {

View file

@ -6,7 +6,17 @@
class ScheduledDatabaseBackupExecution extends BaseModel class ScheduledDatabaseBackupExecution extends BaseModel
{ {
protected $guarded = []; protected $fillable = [
'status',
'message',
'size',
'filename',
'database_name',
'finished_at',
'local_storage_deleted',
's3_storage_deleted',
's3_uploaded',
];
protected function casts(): array protected function casts(): array
{ {

View file

@ -29,7 +29,14 @@ class ScheduledTask extends BaseModel
use HasFactory; use HasFactory;
use HasSafeStringAttribute; use HasSafeStringAttribute;
protected $guarded = []; protected $fillable = [
'enabled',
'name',
'command',
'frequency',
'container',
'timeout',
];
public static function ownedByCurrentTeamAPI(int $teamId) public static function ownedByCurrentTeamAPI(int $teamId)
{ {

View file

@ -22,7 +22,15 @@
)] )]
class ScheduledTaskExecution extends BaseModel class ScheduledTaskExecution extends BaseModel
{ {
protected $guarded = []; protected $fillable = [
'status',
'message',
'finished_at',
'started_at',
'retry_count',
'duration',
'error_details',
];
protected function casts(): array protected function casts(): array
{ {

View file

@ -34,6 +34,7 @@
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Spatie\Url\Url; use Spatie\Url\Url;
use Stevebauman\Purify\Facades\Purify;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@ -142,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',
@ -163,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',
@ -270,7 +272,7 @@ public static function flushIdentityMap(): void
public function setValidationLogsAttribute($value): void public function setValidationLogsAttribute($value): void
{ {
$this->attributes['validation_logs'] = $value !== null $this->attributes['validation_logs'] = $value !== null
? \Stevebauman\Purify\Facades\Purify::config('validation_logs')->clean($value) ? Purify::config('validation_logs')->clean($value)
: null; : null;
} }

View file

@ -53,7 +53,50 @@
)] )]
class ServerSetting extends Model class ServerSetting extends Model
{ {
protected $guarded = []; protected $fillable = [
'is_swarm_manager',
'is_jump_server',
'is_build_server',
'is_reachable',
'is_usable',
'wildcard_domain',
'is_cloudflare_tunnel',
'is_logdrain_newrelic_enabled',
'logdrain_newrelic_license_key',
'logdrain_newrelic_base_uri',
'is_logdrain_highlight_enabled',
'logdrain_highlight_project_id',
'is_logdrain_axiom_enabled',
'logdrain_axiom_dataset_name',
'logdrain_axiom_api_key',
'is_swarm_worker',
'is_logdrain_custom_enabled',
'logdrain_custom_config',
'logdrain_custom_config_parser',
'concurrent_builds',
'dynamic_timeout',
'force_disabled',
'is_metrics_enabled',
'generate_exact_labels',
'force_docker_cleanup',
'docker_cleanup_frequency',
'docker_cleanup_threshold',
'server_timezone',
'delete_unused_volumes',
'delete_unused_networks',
'is_sentinel_enabled',
'sentinel_token',
'sentinel_metrics_refresh_rate_seconds',
'sentinel_metrics_history_days',
'sentinel_push_interval_seconds',
'sentinel_custom_url',
'server_disk_usage_notification_threshold',
'is_sentinel_debug_enabled',
'server_disk_usage_check_frequency',
'is_terminal_enabled',
'deployment_queue_limit',
'disable_application_image_retention',
];
protected $casts = [ protected $casts = [
'force_disabled' => 'boolean', 'force_disabled' => 'boolean',

View file

@ -57,6 +57,7 @@ class Service extends BaseModel
'service_type', 'service_type',
'config_hash', 'config_hash',
'compose_parsing_version', 'compose_parsing_version',
'is_container_label_escape_enabled',
]; ];
protected $appends = ['server_status', 'status']; protected $appends = ['server_status', 'status'];

View file

@ -5,12 +5,30 @@
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\Yaml\Yaml;
class ServiceApplication extends BaseModel class ServiceApplication extends BaseModel
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes;
protected $guarded = []; protected $fillable = [
'name',
'human_name',
'description',
'fqdn',
'ports',
'exposes',
'status',
'exclude_from_status',
'required_fqdn',
'image',
'is_log_drain_enabled',
'is_include_timestamps',
'is_gzip_enabled',
'is_stripprefix_enabled',
'last_online_at',
'is_migrated',
];
protected static function booted() protected static function booted()
{ {
@ -211,7 +229,7 @@ public function getRequiredPort(): ?int
return $this->service->getRequiredPort(); return $this->service->getRequiredPort();
} }
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw); $dockerCompose = Yaml::parse($dockerComposeRaw);
$serviceConfig = data_get($dockerCompose, "services.{$this->name}"); $serviceConfig = data_get($dockerCompose, "services.{$this->name}");
if (! $serviceConfig) { if (! $serviceConfig) {
return $this->service->getRequiredPort(); return $this->service->getRequiredPort();

View file

@ -9,7 +9,27 @@ class ServiceDatabase extends BaseModel
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes;
protected $guarded = []; protected $fillable = [
'name',
'human_name',
'description',
'fqdn',
'ports',
'exposes',
'status',
'exclude_from_status',
'image',
'public_port',
'is_public',
'is_log_drain_enabled',
'is_include_timestamps',
'is_gzip_enabled',
'is_stripprefix_enabled',
'last_online_at',
'is_migrated',
'custom_type',
'public_port_timeout',
];
protected $casts = [ protected $casts = [
'public_port_timeout' => 'integer', 'public_port_timeout' => 'integer',

View file

@ -22,6 +22,9 @@ class SharedEnvironmentVariable extends Model
'is_multiline', 'is_multiline',
'is_literal', 'is_literal',
'is_shown_once', 'is_shown_once',
// Metadata
'version',
]; ];
protected $casts = [ protected $casts = [

View file

@ -24,7 +24,8 @@ class SlackNotificationSettings extends Model
'backup_failure_slack_notifications', 'backup_failure_slack_notifications',
'scheduled_task_success_slack_notifications', 'scheduled_task_success_slack_notifications',
'scheduled_task_failure_slack_notifications', 'scheduled_task_failure_slack_notifications',
'docker_cleanup_slack_notifications', 'docker_cleanup_success_slack_notifications',
'docker_cleanup_failure_slack_notifications',
'server_disk_usage_slack_notifications', 'server_disk_usage_slack_notifications',
'server_reachable_slack_notifications', 'server_reachable_slack_notifications',
'server_unreachable_slack_notifications', 'server_unreachable_slack_notifications',

View file

@ -37,6 +37,9 @@ class StandaloneClickhouse extends BaseModel
'last_restart_at', 'last_restart_at',
'last_restart_type', 'last_restart_type',
'last_online_at', 'last_online_at',
'public_port_timeout',
'custom_docker_run_options',
'clickhouse_db',
]; ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -12,7 +12,10 @@ class StandaloneDocker extends BaseModel
use HasFactory; use HasFactory;
use HasSafeStringAttribute; use HasSafeStringAttribute;
protected $guarded = []; protected $fillable = [
'name',
'network',
];
protected static function boot() protected static function boot()
{ {

View file

@ -36,6 +36,9 @@ class StandaloneDragonfly extends BaseModel
'last_restart_at', 'last_restart_at',
'last_restart_type', 'last_restart_type',
'last_online_at', 'last_online_at',
'public_port_timeout',
'enable_ssl',
'custom_docker_run_options',
]; ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -37,6 +37,9 @@ class StandaloneKeydb extends BaseModel
'last_restart_at', 'last_restart_at',
'last_restart_type', 'last_restart_type',
'last_online_at', 'last_online_at',
'public_port_timeout',
'enable_ssl',
'custom_docker_run_options',
]; ];
protected $appends = ['internal_db_url', 'external_db_url', 'server_status']; protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];

View file

@ -39,6 +39,10 @@ class StandaloneMariadb extends BaseModel
'last_restart_at', 'last_restart_at',
'last_restart_type', 'last_restart_type',
'last_online_at', 'last_online_at',
'public_port_timeout',
'enable_ssl',
'is_log_drain_enabled',
'custom_docker_run_options',
]; ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -37,6 +37,12 @@ class StandaloneMongodb extends BaseModel
'last_restart_at', 'last_restart_at',
'last_restart_type', 'last_restart_type',
'last_online_at', 'last_online_at',
'public_port_timeout',
'enable_ssl',
'ssl_mode',
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
]; ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -38,6 +38,12 @@ class StandaloneMysql extends BaseModel
'last_restart_at', 'last_restart_at',
'last_restart_type', 'last_restart_type',
'last_online_at', 'last_online_at',
'public_port_timeout',
'enable_ssl',
'ssl_mode',
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
]; ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -40,6 +40,12 @@ class StandalonePostgresql extends BaseModel
'last_restart_at', 'last_restart_at',
'last_restart_type', 'last_restart_type',
'last_online_at', 'last_online_at',
'public_port_timeout',
'enable_ssl',
'ssl_mode',
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
]; ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -34,6 +34,11 @@ class StandaloneRedis extends BaseModel
'last_restart_at', 'last_restart_at',
'last_restart_type', 'last_restart_type',
'last_online_at', 'last_online_at',
'public_port_timeout',
'enable_ssl',
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
]; ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -6,7 +6,18 @@
class Subscription extends Model class Subscription extends Model
{ {
protected $guarded = []; protected $fillable = [
'stripe_invoice_paid',
'stripe_subscription_id',
'stripe_customer_id',
'stripe_cancel_at_period_end',
'stripe_plan_id',
'stripe_feedback',
'stripe_comment',
'stripe_trial_already_ended',
'stripe_past_due',
'stripe_refunded_at',
];
protected function casts(): array protected function casts(): array
{ {

View file

@ -6,7 +6,10 @@
class SwarmDocker extends BaseModel class SwarmDocker extends BaseModel
{ {
protected $guarded = []; protected $fillable = [
'name',
'network',
];
public function setNetworkAttribute(string $value): void public function setNetworkAttribute(string $value): void
{ {

View file

@ -8,7 +8,9 @@ class Tag extends BaseModel
{ {
use HasSafeStringAttribute; use HasSafeStringAttribute;
protected $guarded = []; protected $fillable = [
'name',
];
protected function customizeName($value) protected function customizeName($value)
{ {

View file

@ -25,7 +25,8 @@ class TelegramNotificationSettings extends Model
'backup_failure_telegram_notifications', 'backup_failure_telegram_notifications',
'scheduled_task_success_telegram_notifications', 'scheduled_task_success_telegram_notifications',
'scheduled_task_failure_telegram_notifications', 'scheduled_task_failure_telegram_notifications',
'docker_cleanup_telegram_notifications', 'docker_cleanup_success_telegram_notifications',
'docker_cleanup_failure_telegram_notifications',
'server_disk_usage_telegram_notifications', 'server_disk_usage_telegram_notifications',
'server_reachable_telegram_notifications', 'server_reachable_telegram_notifications',
'server_unreachable_telegram_notifications', 'server_unreachable_telegram_notifications',
@ -39,7 +40,8 @@ class TelegramNotificationSettings extends Model
'telegram_notifications_backup_failure_thread_id', 'telegram_notifications_backup_failure_thread_id',
'telegram_notifications_scheduled_task_success_thread_id', 'telegram_notifications_scheduled_task_success_thread_id',
'telegram_notifications_scheduled_task_failure_thread_id', 'telegram_notifications_scheduled_task_failure_thread_id',
'telegram_notifications_docker_cleanup_thread_id', 'telegram_notifications_docker_cleanup_success_thread_id',
'telegram_notifications_docker_cleanup_failure_thread_id',
'telegram_notifications_server_disk_usage_thread_id', 'telegram_notifications_server_disk_usage_thread_id',
'telegram_notifications_server_reachable_thread_id', 'telegram_notifications_server_reachable_thread_id',
'telegram_notifications_server_unreachable_thread_id', 'telegram_notifications_server_unreachable_thread_id',

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

@ -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

@ -168,6 +168,58 @@
expect($model->isFillable('mongo_initdb_root_username'))->toBeTrue(); expect($model->isFillable('mongo_initdb_root_username'))->toBeTrue();
}); });
test('standalone database models allow mass assignment of public_port_timeout', function () {
$models = [
StandalonePostgresql::class,
StandaloneRedis::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
StandaloneClickhouse::class,
];
foreach ($models as $modelClass) {
$model = new $modelClass;
expect($model->isFillable('public_port_timeout'))
->toBeTrue("{$modelClass} should allow mass assignment of 'public_port_timeout'");
}
});
test('standalone database models allow mass assignment of SSL fields where applicable', function () {
$sslModels = [
StandalonePostgresql::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneRedis::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
];
foreach ($sslModels as $modelClass) {
$model = new $modelClass;
expect($model->isFillable('enable_ssl'))
->toBeTrue("{$modelClass} should allow mass assignment of 'enable_ssl'");
}
// Clickhouse has no SSL columns
expect((new StandaloneClickhouse)->isFillable('enable_ssl'))->toBeFalse();
$sslModeModels = [
StandalonePostgresql::class,
StandaloneMysql::class,
StandaloneMongodb::class,
];
foreach ($sslModeModels as $modelClass) {
$model = new $modelClass;
expect($model->isFillable('ssl_mode'))
->toBeTrue("{$modelClass} should allow mass assignment of 'ssl_mode'");
}
});
test('Application fill ignores non-fillable fields', function () { test('Application fill ignores non-fillable fields', function () {
$application = new Application; $application = new Application;
$application->fill([ $application->fill([

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',