Compare commits

...

78 Commits

Author SHA1 Message Date
Andras Bacsai
d8639f58d7 Merge pull request #1625 from coollabsio/next
v4.0.0-beta.186
2024-01-11 11:56:00 +01:00
Andras Bacsai
cf9be9355f fix: upload limit on ui 2024-01-11 11:55:17 +01:00
Andras Bacsai
e36bb11ba8 fix: undead endpoint 2024-01-11 11:34:05 +01:00
Andras Bacsai
190beb3d3f fix: escape build envs properly for nixpacks + docker build 2024-01-11 11:32:32 +01:00
Andras Bacsai
890a6925d1 fix: email verification / forgot password 2024-01-11 08:52:30 +01:00
Andras Bacsai
d03b8420f8 Merge pull request #1627 from coollabsio/fix
fix: Email verification and forgot-password
2024-01-11 08:47:05 +01:00
Andras Bacsai
cbd3c880c3 Add email verification and password reset functionality 2024-01-11 07:16:28 +01:00
Andras Bacsai
6b24001876 feat: import backups 2024-01-10 15:42:54 +01:00
Andras Bacsai
6bb45430c9 fix: submit error on postgresql 2024-01-10 14:41:05 +01:00
Andras Bacsai
bc6b4ed850 Merge branch 'stooit-feat/db-import' into next 2024-01-10 14:37:30 +01:00
Andras Bacsai
8a63ef5da9 Merge branch 'feat/db-import' of github.com:stooit/coolify into stooit-feat/db-import 2024-01-10 14:36:59 +01:00
Andras Bacsai
e324866a27 Refactor database configuration layout 2024-01-10 14:34:04 +01:00
Andras Bacsai
0e5f733657 Add import tab to database configuration view 2024-01-10 14:33:21 +01:00
Andras Bacsai
c5932ed337 smaller ui fixes 2024-01-10 12:41:29 +01:00
Andras Bacsai
ef428f844f fix: do not include thegameplan.json into build image 2024-01-10 12:26:27 +01:00
Andras Bacsai
eb8b752a6e Merge pull request #1607 from stooit/feat/scheduled-tasks-cron
feat: Scheduled tasks (cron)
2024-01-10 12:08:47 +01:00
Andras Bacsai
ce0b38035c Merge branch 'next' into feat/scheduled-tasks-cron 2024-01-10 12:03:48 +01:00
Andras Bacsai
562a8f1fac Merge pull request #1614 from RayBB/fix-typos
fix typos
2024-01-10 11:56:29 +01:00
Andras Bacsai
68f1621757 Merge branch 'main' into fix-typos 2024-01-10 11:55:33 +01:00
Andras Bacsai
e3087573bb Merge pull request #1618 from Illyism/patch-1
Update README.md
2024-01-10 11:52:52 +01:00
Andras Bacsai
7869f223a3 Update version numbers to 4.0.0-beta.186 2024-01-10 11:52:39 +01:00
Andras Bacsai
0e99f27108 Merge pull request #1621 from coollabsio/next
v4.0.0-beta.185
2024-01-10 11:09:41 +01:00
Andras Bacsai
f445a8c312 fix: update navbar on build_pack change 2024-01-10 11:07:53 +01:00
Andras Bacsai
845fc191d4 Remove ray debug statement in updatedApplicationBuildPack method 2024-01-10 11:00:11 +01:00
Andras Bacsai
95d0d72e0d Fix application build pack logic 2024-01-10 11:00:06 +01:00
Andras Bacsai
76f695036c fix: static buildpack should set port 80 2024-01-10 10:58:31 +01:00
Andras Bacsai
225bf06736 Update version numbers and remove unused files 2024-01-10 10:50:07 +01:00
Andras Bacsai
3a287ae974 Update Dockerfile to use serversideup/php:8.2-fpm-nginx image 2024-01-10 09:25:04 +01:00
Andras Bacsai
e5c61b9f9f Merge pull request #1620 from coollabsio/next
fix: fix php-pgsql version to 8.2
2024-01-09 19:05:46 +01:00
Andras Bacsai
32bc876dfc fix: php pgsql to 8.2 2024-01-09 19:04:12 +01:00
Andras Bacsai
c9b3d2a43d Update PHP version in Dockerfiles 2024-01-09 15:23:37 +01:00
Andras Bacsai
f343210e7c hmm 2024-01-09 15:22:06 +01:00
Andras Bacsai
4a42bff0dc Update version numbers to 4.0.0-beta.185 2024-01-09 15:18:33 +01:00
Andras Bacsai
36931b5b18 Merge pull request #1612 from coollabsio/next
v4.0.0-beta.184
2024-01-09 14:58:01 +01:00
Andras Bacsai
3b080abada remove rays 2024-01-09 14:48:26 +01:00
Andras Bacsai
7feba4bbaa fix: remove traefik debug in dev mode 2024-01-09 14:27:55 +01:00
Andras Bacsai
eef8c756df fix: settings menu 2024-01-09 14:27:42 +01:00
Andras Bacsai
9ed30cb0dc Remove unnecessary proxy configurations 2024-01-09 13:13:55 +01:00
Andras Bacsai
6bc43bd999 fix: sort and rename (unique part) of labels 2024-01-09 13:00:07 +01:00
Andras Bacsai
e7683ee9a5 fix: service labels without ports (unknown ports) 2024-01-09 12:49:03 +01:00
Andras Bacsai
ee71aeaa36 fix: use ip for sslip in dev if remote server is used 2024-01-09 12:48:46 +01:00
Andras Bacsai
404c664500 fix: traefik labels 2024-01-09 12:29:45 +01:00
Andras Bacsai
aa80392b46 small fix 2024-01-09 12:25:50 +01:00
Andras Bacsai
c9509ef658 fix: show framework based notification in build logs 2024-01-09 12:19:39 +01:00
Andras Bacsai
31e08a24c9 fix: healthy status 2024-01-09 08:42:37 +01:00
Ilias Ism
14b32e30cd Update README.md 2024-01-08 23:47:11 +01:00
Raymond Berger
5aaad66fe5 fix data naming conflict 2024-01-08 21:59:26 +01:00
Andras Bacsai
b6745c691b fix: queue retry
fix: nixpacks builds
2024-01-08 16:33:34 +01:00
Ray Berger
5ee29c6072 fix typos 2024-01-07 22:32:54 +00:00
Andras Bacsai
b69584fe26 Merge branch 'main' into next 2024-01-07 16:25:13 +01:00
Andras Bacsai
4c3907c296 refactor routes 2024-01-07 16:23:41 +01:00
Andras Bacsai
bf44b4b949 version++ 2024-01-07 13:34:47 +01:00
Andras Bacsai
bee7a2357b Merge pull request #1608 from RayBB/patch-1
fix small typo
2024-01-07 13:34:11 +01:00
Andras Bacsai
98704fc3c2 Merge pull request #1605 from coollabsio/next
v4.0.0-beta.183
2024-01-06 11:39:51 +01:00
Andras Bacsai
e286e78ddc fix: database env variables 2024-01-06 11:38:12 +01:00
Stuart Rowlands
557e1407d0 Added database import feature. 2024-01-06 15:24:57 +10:00
Raymond Berger
3c99f24b5a fix small typo 2024-01-05 15:10:30 +01:00
Stuart Rowlands
512197021b Minor cleanup. 2024-01-05 15:26:51 +10:00
Stuart Rowlands
e233ec05b5 Merge branch 'main' into feat/scheduled-tasks-cron 2024-01-05 15:08:20 +10:00
Stuart Rowlands
d0e3a20a65 Merge branch 'main' of github.com:stooit/coolify 2024-01-05 15:08:06 +10:00
Stuart Rowlands
e2e6813632 Functional scheduled executions.
Display last executions.
Support for Services.
2024-01-05 15:06:36 +10:00
Andras Bacsai
963c519c38 feat: add www-non-www redirects to traefik 2024-01-04 14:14:40 +01:00
Andras Bacsai
d04513d817 Merge pull request #1603 from coollabsio/next
v4.0.0-beta.181
2024-01-04 13:08:18 +01:00
Andras Bacsai
64a7f27e37 fix: file storage save 2024-01-04 13:03:46 +01:00
Andras Bacsai
65652142b2 Merge pull request #1599 from coollabsio/next
v4.0.0-beta.181
2024-01-03 21:26:44 +01:00
Andras Bacsai
7691319c59 Refactor Dockerfile copying logic in ApplicationDeploymentJob.php 2024-01-03 21:15:58 +01:00
Andras Bacsai
206bd50d00 Update nixpacks build command in ApplicationDeploymentJob.php 2024-01-03 21:14:02 +01:00
Andras Bacsai
6159783a73 fix: nixpacks buildpack 2024-01-03 21:03:46 +01:00
Andras Bacsai
ed5f831c86 Merge pull request #1598 from coollabsio/next
v4.0.0-beta.180
2024-01-03 13:42:29 +01:00
Andras Bacsai
65be83e75d fix: only add restart policy if its empty (compose) 2024-01-03 13:37:14 +01:00
Andras Bacsai
25a471b045 Update Dockerfile with new versions of PACK and NIXPACKS 2024-01-03 13:29:23 +01:00
Andras Bacsai
60c7a29aa8 fix: nixpacks cache 2024-01-03 13:20:24 +01:00
Andras Bacsai
11ab6669a0 Merge pull request #1597 from coollabsio/next
v4.0.0-beta.179
2024-01-02 21:13:34 +01:00
Andras Bacsai
53965ab8ed fix: set deployment failed if new container is not healthy 2024-01-02 21:07:16 +01:00
Andras Bacsai
ea271ca079 fix: delete serviceApps & serviceDatabases
fix: cleanup trashed data
2024-01-02 21:03:51 +01:00
Stuart Rowlands
9bbe9567c7 Start scheduled task job execution. 2024-01-01 18:23:58 -08:00
Stuart Rowlands
7913a639b5 Complete add/edit/delete for scheduled tasks.
Refactor views.
2024-01-01 18:23:29 -08:00
Stuart Rowlands
adecf328fc WIP start of scheduled tasks. 2024-01-01 10:33:16 -08:00
190 changed files with 2868 additions and 1537 deletions

View File

@@ -31,7 +31,7 @@ Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and
<a href="https://github.com/whitesidest"><img src="https://avatars.githubusercontent.com/u/12365916?s=52&v=4" width="60px" alt="Tyler Whitesides" /></a>
<a href="https://github.com/aniftyco"><img src="https://github.com/aniftyco.png" width="60px" alt="NiftyCo" /></a>
<a href="https://github.com/iujlaki"><img src="https://github.com/iujlaki.png" width="60px" alt="Imre Ujlaki" /></a>
<a href="https://github.com/Illyism"><img src="https://github.com/Illyism.png" width="60px" alt="Ilias Ism" /></a>
<a href="https://il.ly"><img src="https://github.com/Illyism.png" width="60px" alt="Ilias Ism" /></a>
<a href="https://github.com/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a>
<a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a>

View File

@@ -21,14 +21,14 @@ class DeleteService
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
$application->delete();
$application->forceDelete();
}
foreach ($service->databases()->get() as $database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
$database->delete();
$database->forceDelete();
}
foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name";

View File

@@ -55,7 +55,8 @@ class Init extends Command
}
}
}
private function restore_coolify_db_backup() {
private function restore_coolify_db_backup()
{
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
@@ -73,9 +74,8 @@ class Init extends Command
'team_id' => 0,
]);
}
}
} catch(\Throwable $e) {
} catch (\Throwable $e) {
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
}
}
@@ -99,7 +99,7 @@ class Init extends Command
return;
}
try {
Http::get("https://get.coollabs.io/coolify/v4/alive?appId=$id&version=$version");
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
echo "I am alive!\n";
} catch (\Throwable $e) {
echo "Error in alive: {$e->getMessage()}\n";
@@ -138,6 +138,89 @@ class Init extends Command
}
private function cleanup_stucked_resources()
{
try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) {
echo "Deleting stuck application: {$application->name}\n";
$application->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($postgresqls as $postgresql) {
echo "Deleting stuck postgresql: {$postgresql->name}\n";
$postgresql->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n";
}
try {
$redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($redis as $redis) {
echo "Deleting stuck redis: {$redis->name}\n";
$redis->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
}
try {
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mongodbs as $mongodb) {
echo "Deleting stuck mongodb: {$mongodb->name}\n";
$mongodb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n";
}
try {
$mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mysqls as $mysql) {
echo "Deleting stuck mysql: {$mysql->name}\n";
$mysql->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck mysql: {$e->getMessage()}\n";
}
try {
$mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mariadbs as $mariadb) {
echo "Deleting stuck mariadb: {$mariadb->name}\n";
$mariadb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n";
}
try {
$services = Service::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($services as $service) {
echo "Deleting stuck service: {$service->name}\n";
$service->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck service: {$e->getMessage()}\n";
}
try {
$serviceApps = ServiceApplication::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($serviceApps as $serviceApp) {
echo "Deleting stuck serviceapp: {$serviceApp->name}\n";
$serviceApp->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
}
try {
$serviceDbs = ServiceDatabase::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($serviceDbs as $serviceDb) {
echo "Deleting stuck serviceapp: {$serviceDb->name}\n";
$serviceDb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
}
// Cleanup any resources that are not attached to any environment or destination or server
try {
$applications = Application::all();

View File

@@ -5,12 +5,14 @@ namespace App\Console;
use App\Jobs\CheckLogDrainContainerJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\ScheduledTaskJob;
use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ContainerStatusJob;
use App\Jobs\PullHelperImageJob;
use App\Jobs\ServerStatusJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Console\Scheduling\Schedule;
@@ -30,6 +32,7 @@ class Kernel extends ConsoleKernel
$this->check_resources($schedule);
$this->check_scheduled_backups($schedule);
$this->pull_helper_image($schedule);
$this->check_scheduled_tasks($schedule);
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
@@ -41,6 +44,7 @@ class Kernel extends ConsoleKernel
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->pull_helper_image($schedule);
$this->check_scheduled_tasks($schedule);
}
}
private function pull_helper_image($schedule)
@@ -107,6 +111,32 @@ class Kernel extends ConsoleKernel
}
}
private function check_scheduled_tasks($schedule) {
$scheduled_tasks = ScheduledTask::all();
if ($scheduled_tasks->isEmpty()) {
ray('no scheduled tasks');
return;
}
foreach ($scheduled_tasks as $scheduled_task) {
$service = $scheduled_task->service()->get();
$application = $scheduled_task->application()->get();
if (!$application && !$service) {
ray('application/service attached to scheduled task does not exist');
$scheduled_task->delete();
continue;
}
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
$schedule->job(new ScheduledTaskJob(
task: $scheduled_task
))->cron($scheduled_task->frequency)->onOneServer();
}
}
protected function commands(): void
{
$this->load(__DIR__ . '/Commands');

View File

@@ -2,23 +2,68 @@
namespace App\Http\Controllers;
use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\StandalonePostgresql;
use App\Events\TestEvent;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse;
use Laravel\Fortify\Contracts\SuccessfulPasswordResetLinkRequestResponse;
use Illuminate\Support\Facades\Password;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
public function realtime_test() {
if (auth()->user()?->currentTeam()->id !== 0) {
return redirect(RouteServiceProvider::HOME);
}
TestEvent::dispatch();
return 'Look at your other tab.';
}
public function verify() {
return view('auth.verify-email');
}
public function email_verify(EmailVerificationRequest $request) {
$request->fulfill();
$name = request()->user()?->name;
send_internal_notification("User {$name} verified their email address.");
return redirect(RouteServiceProvider::HOME);
}
public function forgot_password(Request $request) {
if (is_transactional_emails_active()) {
$arrayOfRequest = $request->only(Fortify::email());
$request->merge([
'email' => Str::lower($arrayOfRequest['email']),
]);
$type = set_transanctional_email_settings();
if (!$type) {
return response()->json(['message' => 'Transactional emails are not active'], 400);
}
$request->validate([Fortify::email() => 'required|email']);
$status = Password::broker(config('fortify.passwords'))->sendResetLink(
$request->only(Fortify::email())
);
if ($status == Password::RESET_LINK_SENT) {
return app(SuccessfulPasswordResetLinkRequestResponse::class, ['status' => $status]);
}
if ($status == Password::RESET_THROTTLED) {
return response('Already requested a password reset in the past minutes.', 400);
}
return app(FailedPasswordResetLinkRequestResponse::class, ['status' => $status]);
}
return response()->json(['message' => 'Transactional emails are not active'], 400);
}
public function link()
{
$token = request()->get('token');
@@ -51,90 +96,7 @@ class Controller extends BaseController
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
public function license()
{
if (!isCloud()) {
abort(404);
}
return view('settings.license', [
'settings' => InstanceSettings::get(),
]);
}
public function force_passoword_reset()
{
return view('auth.force-password-reset');
}
public function boarding()
{
if (currentTeam()->boarding || isDev()) {
return view('boarding');
} else {
return redirect()->route('dashboard');
}
}
public function settings()
{
if (isInstanceAdmin()) {
$settings = InstanceSettings::get();
$database = StandalonePostgresql::whereName('coolify-db')->first();
if ($database) {
if ($database->status !== 'running') {
$database->status = 'running';
$database->save();
}
$s3s = S3Storage::whereTeamId(0)->get();
}
return view('settings.configuration', [
'settings' => $settings,
'database' => $database,
's3s' => $s3s ?? [],
]);
} else {
return redirect()->route('dashboard');
}
}
public function team()
{
$invitations = [];
if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
return view('team.index', [
'invitations' => $invitations,
]);
}
public function storages()
{
$s3 = S3Storage::ownedByCurrentTeam()->get();
return view('team.storages.all', [
's3' => $s3,
]);
}
public function storages_show()
{
$storage = S3Storage::ownedByCurrentTeam()->whereUuid(request()->storage_uuid)->firstOrFail();
return view('team.storages.show', [
'storage' => $storage,
]);
}
public function members()
{
$invitations = [];
if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
return view('team.members', [
'invitations' => $invitations,
]);
}
public function acceptInvitation()
public function accept_invitation()
{
try {
$resetPassword = request()->query('reset-password');
@@ -169,7 +131,7 @@ class Controller extends BaseController
}
}
public function revokeInvitation()
public function revoke_invitation()
{
try {
$invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();

View File

@@ -1,84 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
class DatabaseController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function configuration()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
return view('project.database.configuration', ['database' => $database]);
}
public function executions()
{
$backup_uuid = request()->route('backup_uuid');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
$backup = $database->scheduledBackups->where('uuid', $backup_uuid)->first();
if (!$backup) {
return redirect()->route('dashboard');
}
$executions = collect($backup->executions)->sortByDesc('created_at');
return view('project.database.backups.executions', [
'database' => $database,
'backup' => $backup,
'executions' => $executions,
's3s' => currentTeam()->s3s,
]);
}
public function backups()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
// No backups for redis
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'database_uuid' => $database->uuid,
]);
}
return view('project.database.backups.all', [
'database' => $database,
's3s' => currentTeam()->s3s,
]);
}
}

View File

@@ -30,6 +30,7 @@ use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Throwable;
use Visus\Cuid2\Cuid2;
use Yosymfony\Toml\Toml;
class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
@@ -73,6 +74,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $env_args;
private $docker_compose;
private $docker_compose_base64;
private ?string $nixpacks_plan = null;
private ?string $nixpacks_type = null;
private string $dockerfile_location = '/Dockerfile';
private string $docker_compose_location = '/docker-compose.yml';
private ?string $docker_compose_custom_start_command = null;
@@ -164,11 +167,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
$allContainers = collect($allContainers)->sort()->values();
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
if (preg_match('/-(\d{12})/', $containerName)) {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
@@ -343,6 +350,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_image_names();
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->execute_remote_command([
"echo 'Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.'",
]);
$this->create_workdir();
$this->generate_compose_file();
$this->rolling_update();
@@ -575,7 +585,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_nixpacks_confs();
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
// $this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->rolling_update();
}
@@ -597,6 +607,24 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->rolling_update();
}
private function framework_based_notification()
{
// Laravel old env variables
if ($this->pull_request_id === 0) {
$nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
$nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
} else {
$nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
$nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
}
if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) {
$this->execute_remote_command(
[
"echo 'There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/frameworks/laravel#requirements'", 'type' => 'err'
],
);
}
}
private function rolling_update()
{
if ($this->server->isSwarm()) {
@@ -633,6 +661,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
}
}
$this->framework_based_notification();
}
private function health_check()
{
@@ -672,7 +701,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->execute_remote_command(
@@ -682,6 +711,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
break;
}
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
$this->newVersionIsHealthy = false;
break;
}
$counter++;
sleep($this->application->health_check_interval);
}
@@ -866,20 +899,30 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[
"echo -n 'Generating nixpacks configuration with: $nixpacks_command'",
],
[executeInDocker($this->deployment_uuid, $nixpacks_command)],
[executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")],
[executeInDocker($this->deployment_uuid, "rm -f {$this->workdir}/.nixpacks/Dockerfile")]
[executeInDocker($this->deployment_uuid, $nixpacks_command), "save" => "nixpacks_plan", "hidden" => true],
[executeInDocker($this->deployment_uuid, "nixpacks detect {$this->workdir}"), "save" => "nixpacks_type", "hidden" => true],
);
if ($this->saved_outputs->get('nixpacks_type')) {
$this->nixpacks_type = $this->saved_outputs->get('nixpacks_type');
}
if ($this->saved_outputs->get('nixpacks_plan')) {
$this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan');
if ($this->nixpacks_plan) {
$parsed = Toml::Parse($this->nixpacks_plan);
// Do any modifications here
$cmds = collect(data_get($parsed, 'phases.setup.cmds', []));
$this->generate_env_variables();
data_set($parsed, 'phases.setup.cmds', $cmds);
$merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', [])));
data_set($parsed, 'variables', $merged_envs->toArray());
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
}
}
}
private function nixpacks_build_cmd()
{
$this->generate_env_variables();
$cacheKey = $this->application->uuid;
if ($this->pull_request_id !== 0) {
$cacheKey = "{$this->application->uuid}-pr-{$this->pull_request_id}";
}
$nixpacks_command = "nixpacks build --cache-key '{$cacheKey}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
$nixpacks_command = "nixpacks plan -f toml";
if ($this->application->build_command) {
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
}
@@ -898,15 +941,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->env_args = collect([]);
if ($this->pull_request_id === 0) {
foreach ($this->application->nixpacks_environment_variables as $env) {
$this->env_args->push("--env {$env->key}={$env->value}");
$this->env_args->put($env->key, $env->value);
}
foreach ($this->application->build_environment_variables as $env) {
$this->env_args->put($env->key, $env->value);
}
} else {
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
$this->env_args->push("--env {$env->key}={$env->value}");
$this->env_args->put($env->key, $env->value);
}
foreach ($this->application->build_environment_variables_preview as $env) {
$this->env_args->put($env->key, $env->value);
}
}
$this->env_args = $this->env_args->implode(' ');
// $this->env_args = $this->env_args->implode(' ');
}
private function generate_compose_file()
@@ -1133,7 +1181,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
$environment_variables->push("PORT={$ports[0]}");
} if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('SOURCE_COMMIT'))->isEmpty()) {
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('SOURCE_COMMIT'))->isEmpty()) {
if (!is_null($this->commit)) {
$environment_variables->push("SOURCE_COMMIT={$this->commit}");
}
@@ -1196,6 +1245,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
if ($this->application->static_image) {
$this->pull_latest_image($this->application->static_image);
$this->execute_remote_command(
["echo -n 'Continue with the building process.'"],
);
}
if ($this->application->build_pack === 'static') {
$dockerfile = base64_encode("FROM {$this->application->static_image}
@@ -1222,9 +1274,36 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
}");
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]);
if ($this->application->build_pack === 'nixpacks') {
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d > /artifacts/thegameplan.json"), "hidden" => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), "hidden" => true
]);
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), "hidden" => true
]);
}
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "rm /artifacts/thegameplan.json"), "hidden" => true]);
} else {
if ($this->force_rebuild) {
$build_command = "docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}";
$base64_build_command = base64_encode($build_command);
} else {
$build_command = "docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}";
$base64_build_command = base64_encode($build_command);
}
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d > /artifacts/build.sh"), "hidden" => true
],
[
executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true
]
);
}
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/
@@ -1249,6 +1328,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
}");
}
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile")
@@ -1257,19 +1338,56 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
],
[
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d > /artifacts/build.sh"), "hidden" => true
],
[
executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true
]
);
} else {
// Pure Dockerfile based deployment
if ($this->application->dockerfile) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --pull $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
$build_command = "docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d > /artifacts/build.sh"), "hidden" => true
],
[
executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true
]
);
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
if ($this->application->build_pack === 'nixpacks') {
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d > /artifacts/thegameplan.json"), "hidden" => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), "hidden" => true
]);
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), "hidden" => true
]);
}
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "rm /artifacts/thegameplan.json"), "hidden" => true]);
} else {
if ($this->force_rebuild) {
$build_command = "docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
$base64_build_command = base64_encode($build_command);
} else {
$build_command = "docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
$base64_build_command = base64_encode($build_command);
}
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d > /artifacts/build.sh"), "hidden" => true
],
[
executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true
]
);
}
}
}
$this->execute_remote_command([
@@ -1295,6 +1413,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
});
} else {
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::FAILED->value,
]);
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
@@ -1345,11 +1466,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]);
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
$this->build_args->push("--build-arg {$env->key}=\"{$env->value}\"");
$value = escapeshellarg($env->value);
$this->build_args->push("--build-arg {$env->key}={$value}");
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
$this->build_args->push("--build-arg {$env->key}=\"{$env->value}\"");
$value = escapeshellarg($env->value);
$this->build_args->push("--build-arg {$env->key}={$value}");
}
}

View File

@@ -162,6 +162,10 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
// Notify user that this container should not be there.
}
}
if (data_get($container,'Name') === '/coolify-db') {
$foundDatabases[] = 0;
}
}
$serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) {
@@ -212,7 +216,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
}
$name = data_get($exitedService, 'name');
$fqdn = data_get($exitedService, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$containerName = $name ? "$name, available at $fqdn" : $fqdn;
$projectUuid = data_get($service, 'environment.project.uuid');
$serviceUuid = data_get($service, 'uuid');
$environmentName = data_get($service, 'environment.name');

View File

@@ -0,0 +1,115 @@
<?php
namespace App\Jobs;
use App\Models\ScheduledTask;
use App\Models\ScheduledTaskExecution;
use App\Models\Server;
use App\Models\Application;
use App\Models\Service;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Throwable;
class ScheduledTaskJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public ?Team $team = null;
public Server $server;
public ScheduledTask $task;
public Application|Service $resource;
public ?ScheduledTaskExecution $task_log = null;
public string $task_status = 'failed';
public ?string $task_output = null;
public array $containers = [];
public function __construct($task)
{
$this->task = $task;
if ($service = $task->service()->first()) {
$this->resource = $service;
} else if ($application = $task->application()->first()) {
$this->resource = $application;
}
$this->team = Team::find($task->team_id);
}
public function middleware(): array
{
return [new WithoutOverlapping($this->task->id)];
}
public function uniqueId(): int
{
return $this->task->id;
}
public function handle(): void
{
try {
$this->task_log = ScheduledTaskExecution::create([
'scheduled_task_id' => $this->task->id,
]);
$this->server = $this->resource->destination->server;
if ($this->resource->type() == 'application') {
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($containers->count() > 0) {
$containers->each(function ($container) {
$this->containers[] = str_replace('/', '', $container['Names']);
});
}
}
elseif ($this->resource->type() == 'service') {
$this->resource->applications()->get()->each(function ($application) {
if (str(data_get($application, 'status'))->contains('running')) {
$this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid');
}
});
}
if (count($this->containers) == 0) {
throw new \Exception('ScheduledTaskJob failed: No containers running.');
}
if (count($this->containers) > 1 && empty($this->task->container)) {
throw new \Exception('ScheduledTaskJob failed: More than one container exists but no container name was provided.');
}
foreach ($this->containers as $containerName) {
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
$cmd = 'sh -c "' . str_replace('"', '\"', $this->task->command) . '"';
$exec = "docker exec {$containerName} {$cmd}";
$this->task_output = instant_remote_process([$exec], $this->server, true);
$this->task_log->update([
'status' => 'success',
'message' => $this->task_output,
]);
return;
}
}
// No valid container was found.
throw new \Exception('ScheduledTaskJob failed: No valid container was found. Is the container name correct?');
} catch (\Throwable $e) {
if ($this->task_log) {
$this->task_log->update([
'status' => 'failed',
'message' => $this->task_output ?? $e->getMessage(),
]);
}
send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
}
}
}

View File

@@ -274,7 +274,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{
$this->skipBoarding();
return redirect()->route(
'project.resources.new',
'project.resource.create',
[
'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production',

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Livewire\CommandCenter;
use App\Models\Server;
use Livewire\Component;
class Index extends Component
{
public $servers = [];
public function mount() {
$this->servers = Server::isReachable()->get();
}
public function render()
{
return view('livewire.command-center.index');
}
}

View File

@@ -22,6 +22,10 @@ class ForcePasswordReset extends Component
{
$this->email = auth()->user()->email;
}
public function render()
{
return view('livewire.force-password-reset')->layout('layouts.simple');
}
public function submit()
{
try {

View File

@@ -5,21 +5,19 @@ namespace App\Livewire\Profile;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Form extends Component
class Index extends Component
{
public int $userId;
public string $email;
#[Validate('required')]
public string $name;
public function mount()
{
$this->userId = auth()->user()->id;
$this->name = auth()->user()->name;
$this->email = auth()->user()->email;
}
public function submit()
{
@@ -34,4 +32,8 @@ class Form extends Component
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.profile.index');
}
}

View File

@@ -27,7 +27,7 @@ class AddEnvironment extends Component
'project_id' => $this->project->id,
]);
return redirect()->route('project.resources', [
return redirect()->route('project.resource.index', [
'project_uuid' => $this->project->uuid,
'environment_name' => $environment->name,
]);

View File

@@ -4,13 +4,14 @@ namespace App\Livewire\Project\Application;
use App\Models\Application;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Livewire\Component;
class Configuration extends Component
{
public Application $application;
public $servers;
protected $listeners = ['build_pack_updated' => '$refresh'];
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();

View File

@@ -1,15 +1,15 @@
<?php
namespace App\Livewire\Project\Application;
namespace App\Livewire\Project\Application\Deployment;
use App\Models\Application;
use Illuminate\Support\Collection;
use Livewire\Component;
class Deployments extends Component
class Index extends Component
{
public Application $application;
public Array|Collection $deployments = [];
public array|Collection $deployments = [];
public int $deployments_count = 0;
public string $current_url;
public int $skip = 0;
@@ -19,11 +19,28 @@ class Deployments extends Component
protected $queryString = ['pull_request_id'];
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return redirect()->route('dashboard');
}
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 40);
$this->application = $application;
$this->deployments = $deployments;
$this->deployments_count = $count;
$this->current_url = url()->current();
$this->show_pull_request_only();
$this->show_more();
}
private function show_pull_request_only() {
private function show_pull_request_only()
{
if ($this->pull_request_id) {
$this->deployments = $this->deployments->where('pull_request_id', $this->pull_request_id);
}
@@ -57,4 +74,8 @@ class Deployments extends Component
$this->show_pull_request_only();
$this->show_more();
}
public function render()
{
return view('livewire.project.application.deployment.index');
}
}

View File

@@ -1,35 +1,20 @@
<?php
namespace App\Http\Controllers;
namespace App\Livewire\Project\Application\Deployment;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Livewire\Component;
class ApplicationController extends Controller
class Show extends Component
{
use AuthorizesRequests, ValidatesRequests;
public Application $application;
public ApplicationDeploymentQueue $application_deployment_queue;
public string $deployment_uuid;
public $isKeepAliveOn = true;
protected $listeners = ['refreshQueue'];
public function deployments()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return redirect()->route('dashboard');
}
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 40);
return view('project.application.deployments', ['application' => $application, 'deployments' => $deployments, 'deployments_count' => $count]);
}
public function deployment()
{
public function mount() {
$deploymentUuid = request()->route('deployment_uuid');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
@@ -46,7 +31,7 @@ class ApplicationController extends Controller
}
// $activity = Activity::where('properties->type_uuid', '=', $deploymentUuid)->first();
// if (!$activity) {
// return redirect()->route('project.application.deployments', [
// return redirect()->route('project.application.deployment.index', [
// 'project_uuid' => $project->uuid,
// 'environment_name' => $environment->name,
// 'application_uuid' => $application->uuid,
@@ -54,17 +39,32 @@ class ApplicationController extends Controller
// }
$application_deployment_queue = ApplicationDeploymentQueue::where('deployment_uuid', $deploymentUuid)->first();
if (!$application_deployment_queue) {
return redirect()->route('project.application.deployments', [
return redirect()->route('project.application.deployment.index', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'application_uuid' => $application->uuid,
]);
}
return view('project.application.deployment', [
'application' => $application,
// 'activity' => $activity,
'application_deployment_queue' => $application_deployment_queue,
'deployment_uuid' => $deploymentUuid,
]);
$this->application = $application;
$this->application_deployment_queue = $application_deployment_queue;
$this->deployment_uuid = $deploymentUuid;
}
public function refreshQueue()
{
$this->application_deployment_queue->refresh();
}
public function polling()
{
$this->dispatch('deploymentFinished');
$this->application_deployment_queue->refresh();
if (data_get($this->application_deployment_queue, 'status') == 'finished' || data_get($this->application_deployment_queue, 'status') == 'failed') {
$this->isKeepAliveOn = false;
}
}
public function render()
{
return view('livewire.project.application.deployment.show');
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace App\Livewire\Project\Application;
use App\Models\ApplicationDeploymentQueue;
use Livewire\Component;
class DeploymentLogs extends Component
{
public ApplicationDeploymentQueue $application_deployment_queue;
public $isKeepAliveOn = true;
protected $listeners = ['refreshQueue'];
public function refreshQueue()
{
$this->application_deployment_queue->refresh();
}
public function polling()
{
$this->dispatch('deploymentFinished');
$this->application_deployment_queue->refresh();
if (data_get($this->application_deployment_queue, 'status') == 'finished' || data_get($this->application_deployment_queue, 'status') == 'failed') {
$this->isKeepAliveOn = false;
}
}
}

View File

@@ -165,12 +165,20 @@ class General extends Component
if ($this->application->build_pack !== 'nixpacks') {
$this->application->settings->is_static = false;
$this->application->settings->save();
} else {
$this->application->ports_exposes = $this->ports_exposes = 3000;
$this->resetDefaultLabels(false);
}
if ($this->application->build_pack === 'dockercompose') {
$this->application->fqdn = null;
$this->application->settings->save();
}
if ($this->application->build_pack === 'static') {
$this->application->ports_exposes = $this->ports_exposes = 80;
$this->resetDefaultLabels(false);
}
$this->submit();
$this->dispatch('build_pack_updated');
}
public function checkLabelUpdates()
{
@@ -201,7 +209,7 @@ class General extends Component
public function updatedApplicationFqdn()
{
$this->resetDefaultLabels(false);
$this->dispatch('success', 'Labels reseted to default!');
$this->dispatch('success', 'Labels reset to default!');
}
public function submit($showToaster = true)
{

View File

@@ -39,7 +39,7 @@ class Heading extends Component
} else {
dispatch(new ServerStatusJob($this->application->destination->server));
}
if ($showNotification) $this->dispatch('success', 'Application status updated.');
if ($showNotification) $this->dispatch('success', "Application ({$this->application->name}) status updated.");
}
public function force_deploy_without_cache()
@@ -60,7 +60,7 @@ class Heading extends Component
force_rebuild: false,
is_new_deployment: true,
);
return redirect()->route('project.application.deployment', [
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
@@ -83,7 +83,7 @@ class Heading extends Component
deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild,
);
return redirect()->route('project.application.deployment', [
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
@@ -113,7 +113,7 @@ class Heading extends Component
restart_only: true,
is_new_deployment: true,
);
return redirect()->route('project.application.deployment', [
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
@@ -128,7 +128,7 @@ class Heading extends Component
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);
return redirect()->route('project.application.deployment', [
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,

View File

@@ -52,7 +52,7 @@ class Previews extends Component
force_rebuild: true,
pull_request_id: $pull_request_id,
);
return redirect()->route('project.application.deployment', [
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deployment_uuid,

View File

@@ -29,7 +29,7 @@ class Rollback extends Component
commit: $commit,
force_rebuild: false,
);
return redirect()->route('project.application.deployment', [
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $deployment_uuid,

View File

@@ -8,7 +8,7 @@ use App\Models\Server;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class CloneProject extends Component
class CloneMe extends Component
{
public string $project_uuid;
public string $environment_name;
@@ -41,7 +41,7 @@ class CloneProject extends Component
public function render()
{
return view('livewire.project.clone-project');
return view('livewire.project.clone-me');
}
public function selectServer($server_id, $destination_id)
@@ -152,7 +152,7 @@ class CloneProject extends Component
}
$newService->parse();
}
return redirect()->route('project.resources', [
return redirect()->route('project.resource.index', [
'project_uuid' => $newProject->uuid,
'environment_name' => $newEnvironment->name,
]);

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Livewire\Project\Database\Backup;
use Livewire\Component;
class Execution extends Component
{
public $database;
public $backup;
public $executions;
public $s3s;
public function mount() {
$backup_uuid = request()->route('backup_uuid');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
$backup = $database->scheduledBackups->where('uuid', $backup_uuid)->first();
if (!$backup) {
return redirect()->route('dashboard');
}
$executions = collect($backup->executions)->sortByDesc('created_at');
$this->database = $database;
$this->backup = $backup;
$this->executions = $executions;
$this->s3s = currentTeam()->s3s;
}
public function render()
{
return view('livewire.project.database.backup.execution');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Livewire\Project\Database\Backup;
use Livewire\Component;
class Index extends Component
{
public $database;
public $s3s;
public function mount() {
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
// No backups for redis
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'database_uuid' => $database->uuid,
]);
}
$this->database = $database;
$this->s3s = currentTeam()->s3s;
}
public function render()
{
return view('livewire.project.database.backup.index');
}
}

View File

@@ -52,7 +52,7 @@ class BackupEdit extends Component
$url = $url->getPath() . "#{$url->getFragment()}";
return redirect($url);
} else {
return redirect()->route('project.database.backups.all', $this->parameters);
return redirect()->route('project.database.backup.index', $this->parameters);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Livewire\Project\Database;
use Livewire\Component;
class Configuration extends Component
{
public $database;
public function mount() {
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
$this->database = $database;
}
public function render()
{
return view('livewire.project.database.configuration');
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Livewire\Project\Database;
use Exception;
use Livewire\Component;
use Livewire\WithFileUploads;
use App\Models\Server;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Storage;
class Import extends Component
{
use WithFileUploads;
public $file;
public $resource;
public $parameters;
public $containers;
public bool $validated = true;
public bool $scpInProgress = false;
public bool $importRunning = false;
public string $validationMsg = '';
public Server $server;
public string $container;
public array $importCommands = [];
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p $MYSQL_PASSWORD $MYSQL_DATABASE';
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p $MARIADB_PASSWORD $MARIADB_DATABASE';
public function getListeners()
{
$userId = auth()->user()->id;
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
];
}
public function mount()
{
$this->parameters = get_route_parameters();
$this->getContainers();
}
public function getContainers()
{
$this->containers = collect();
if (!data_get($this->parameters, 'database_uuid')) {
abort(404);
}
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
}
}
$this->resource = $resource;
$this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid;
if (str(data_get($this, 'resource.status'))->startsWith('running')) {
$this->containers->push($this->container);
}
if ($this->containers->count() > 1) {
$this->validated = false;
$this->validationMsg = 'The database service has more than one container running. Cannot import.';
}
if (
$this->resource->getMorphClass() == 'App\Models\StandaloneRedis'
|| $this->resource->getMorphClass() == 'App\Models\StandaloneMongodb'
) {
$this->validated = false;
$this->validationMsg = 'This database type is not currently supported.';
}
}
public function runImport()
{
$this->validate([
'file' => 'required|file|max:102400'
]);
$this->importRunning = true;
$this->scpInProgress = true;
try {
$uploadedFilename = $this->file->store('backup-import');
$path = Storage::path($uploadedFilename);
$tmpPath = '/tmp/' . basename($uploadedFilename);
// SCP the backup file to the server.
instant_scp($path, $tmpPath, $this->server);
$this->scpInProgress = false;
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
switch ($this->resource->getMorphClass()) {
case 'App\Models\StandaloneMariadb':
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mariadbRestoreCommand} < {$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
break;
case 'App\Models\StandaloneMysql':
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mysqlRestoreCommand} < {$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
break;
case 'App\Models\StandalonePostgresql':
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
break;
}
$this->importCommands[] = "docker exec {$this->container} sh -c 'rm {$tmpPath}'";
$this->importCommands[] = "docker exec {$this->container} sh -c 'echo \"Import finished with exit code $?\"'";
if (!empty($this->importCommands)) {
$activity = remote_process($this->importCommands, $this->server, ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id);
}
} catch (\Throwable $e) {
$this->validated = false;
$this->validationMsg = $e->getMessage();
}
}
}

View File

@@ -25,6 +25,6 @@ class DeleteProject extends Component
return $this->dispatch('error', 'Project has resources defined, please delete them first.');
}
$project->delete();
return redirect()->route('projects');
return redirect()->route('project.index');
}
}

View File

@@ -12,6 +12,15 @@ class Edit extends Component
'project.name' => 'required|min:3|max:255',
'project.description' => 'nullable|string|max:255',
];
public function mount() {
$projectUuid = request()->route('project_uuid');
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
if (!$project) {
return redirect()->route('dashboard');
}
$this->project = $project;
}
public function submit()
{

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Livewire\Project;
use App\Models\Project;
use App\Models\Server;
use Livewire\Component;
class Index extends Component
{
public $projects;
public $servers;
public function mount() {
$this->projects = Project::ownedByCurrentTeam()->get();
$this->servers = Server::ownedByCurrentTeam()->count();
}
public function render()
{
return view('livewire.project.index');
}
}

View File

@@ -55,7 +55,7 @@ class Select extends Component
public function updatedSelectedEnvironment()
{
return redirect()->route('project.resources.new', [
return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->selectedEnvironment,
]);
@@ -157,7 +157,7 @@ class Select extends Component
public function setDestination(string $destination_uuid)
{
$this->destination_uuid = $destination_uuid;
return redirect()->route('project.resources.new', [
return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'],
'type' => $this->type,

View File

@@ -1,52 +1,18 @@
<?php
namespace App\Http\Controllers;
namespace App\Livewire\Project\Resource;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneDocker;
use Illuminate\Support\Str;
use Livewire\Component;
class ProjectController extends Controller
class Create extends Component
{
public function all()
{
return view('projects', [
'projects' => Project::ownedByCurrentTeam()->get(),
'servers' => Server::ownedByCurrentTeam()->count(),
]);
}
public function edit()
{
$projectUuid = request()->route('project_uuid');
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
if (!$project) {
return redirect()->route('dashboard');
}
return view('project.edit', ['project' => $project]);
}
public function show()
{
$projectUuid = request()->route('project_uuid');
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
if (!$project) {
return redirect()->route('dashboard');
}
$project->load(['environments']);
return view('project.show', ['project' => $project]);
}
public function new()
{
public $type;
public function mount() {
$services = getServiceTemplates();
$type = Str::of(request()->query('type'));
$type = str(request()->query('type'));
$destination_uuid = request()->query('destination');
$server_id = request()->query('server_id');
@@ -81,14 +47,14 @@ class ProjectController extends Controller
$oneClickService = data_get($services, "$oneClickServiceName.compose");
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
$oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
return !empty($value);
});
}
if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
$service = Service::create([
'name' => "$oneClickServiceName-" . Str::random(10),
'name' => "$oneClickServiceName-" . str()->random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
@@ -99,8 +65,8 @@ class ProjectController extends Controller
$service->save();
if ($oneClickDotEnvs?->count() > 0) {
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = Str::before($value, '=');
$value = Str::of(Str::after($value, '='));
$key = str()->before($value, '=');
$value = str(str()->after($value, '='));
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
@@ -123,24 +89,10 @@ class ProjectController extends Controller
]);
}
}
return view('project.new', [
'type' => $type->value()
]);
$this->type = $type->value();
}
public function resources()
public function render()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first();
if (!$environment) {
return redirect()->route('dashboard');
}
return view('project.resources', [
'project' => $project,
'environment' => $environment
]);
return view('livewire.project.resource.create');
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Livewire\Project\Resource;
use App\Models\Environment;
use App\Models\Project;
use Livewire\Component;
class Index extends Component
{
public Project $project;
public Environment $environment;
public function mount () {
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first();
if (!$environment) {
return redirect()->route('dashboard');
}
$this->project = $project;
$this->environment = $environment;
}
public function render()
{
return view('livewire.project.resource.index');
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Livewire\Project\Service;
use App\Jobs\ContainerStatusJob;
use App\Models\Service;
use Livewire\Component;
class Configuration extends Component
{
public Service $service;
public $applications;
public $databases;
public array $parameters;
public array $query;
public function getListeners()
{
$userId = auth()->user()->id;
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'checkStatus',
"refreshStacks",
"checkStatus",
];
}
public function render()
{
return view('livewire.project.service.configuration');
}
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
}
public function checkStatus()
{
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->refreshStacks();
$this->dispatch('serviceStatusChanged');
}
public function refreshStacks()
{
$this->applications = $this->service->applications->sort();
$this->applications->each(function ($application) {
$application->refresh();
});
$this->databases = $this->service->databases->sort();
$this->databases->each(function ($database) {
$database->refresh();
});
}
}

View File

@@ -2,53 +2,51 @@
namespace App\Livewire\Project\Service;
use App\Jobs\ContainerStatusJob;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Collection;
use Livewire\Component;
class Index extends Component
{
public Service $service;
public $applications;
public $databases;
public ?ServiceApplication $serviceApplication = null;
public ?ServiceDatabase $serviceDatabase = null;
public array $parameters;
public array $query;
public function getListeners()
public Collection $services;
public $s3s;
protected $listeners = ['generateDockerCompose'];
public function mount()
{
$userId = auth()->user()->id;
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'checkStatus',
"refreshStacks",
"checkStatus",
];
try {
$this->services = collect([]);
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$service = $this->service->applications()->whereName($this->parameters['service_name'])->first();
if ($service) {
$this->serviceApplication = $service;
$this->serviceApplication->getFilesFromServer();
} else {
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
$this->serviceDatabase->getFilesFromServer();
}
$this->s3s = currentTeam()->s3s;
} catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function generateDockerCompose()
{
$this->service->parse();
}
public function render()
{
return view('livewire.project.service.index');
}
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
}
public function checkStatus()
{
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->refreshStacks();
$this->dispatch('serviceStatusChanged');
}
public function refreshStacks()
{
$this->applications = $this->service->applications->sort();
$this->applications->each(function ($application) {
$application->refresh();
});
$this->databases = $this->service->databases->sort();
$this->databases->each(function ($database) {
$database->refresh();
});
}
}

View File

@@ -1,52 +0,0 @@
<?php
namespace App\Livewire\Project\Service;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component
{
public Service $service;
public ?ServiceApplication $serviceApplication = null;
public ?ServiceDatabase $serviceDatabase = null;
public array $parameters;
public array $query;
public Collection $services;
public $s3s;
protected $listeners = ['generateDockerCompose'];
public function mount()
{
try {
$this->services = collect([]);
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$service = $this->service->applications()->whereName($this->parameters['service_name'])->first();
if ($service) {
$this->serviceApplication = $service;
$this->serviceApplication->getFilesFromServer();
} else {
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
$this->serviceDatabase->getFilesFromServer();
}
$this->s3s = currentTeam()->s3s;
} catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function generateDockerCompose()
{
$this->service->parse();
}
public function render()
{
return view('livewire.project.service.show');
}
}

View File

@@ -25,7 +25,7 @@ class Danger extends Component
{
try {
DeleteResourceJob::dispatchSync($this->resource);
return redirect()->route('project.resources', [
return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName
]);

View File

@@ -139,6 +139,18 @@ class All extends Component
case 'standalone-postgresql':
$environment->standalone_postgresql_id = $this->resource->id;
break;
case 'standalone-redis':
$environment->standalone_redis_id = $this->resource->id;
break;
case 'standalone-mongodb':
$environment->standalone_mongodb_id = $this->resource->id;
break;
case 'standalone-mysql':
$environment->standalone_mysql_id = $this->resource->id;
break;
case 'standalone-mariadb':
$environment->standalone_mariadb_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;

View File

@@ -10,7 +10,6 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Sleep;
use Livewire\Component;
class ExecuteContainerCommand extends Component

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Livewire\Project\Shared\ScheduledTask;
use Livewire\Component;
class Add extends Component
{
public $parameters;
public string $name;
public string $command;
public string $frequency;
public ?string $container = '';
protected $listeners = ['clearScheduledTask' => 'clear'];
protected $rules = [
'name' => 'required|string',
'command' => 'required|string',
'frequency' => 'required|string',
'container' => 'nullable|string',
];
protected $validationAttributes = [
'name' => 'name',
'command' => 'command',
'frequency' => 'frequency',
'container' => 'container',
];
public function mount()
{
$this->parameters = get_route_parameters();
}
public function submit()
{
$this->validate();
$isValid = validate_cron_expression($this->frequency);
if (!$isValid) {
$this->dispatch('error', 'Invalid Cron / Human expression.');
return;
}
$this->dispatch('saveScheduledTask', [
'name' => $this->name,
'command' => $this->command,
'frequency' => $this->frequency,
'container' => $this->container,
]);
$this->clear();
}
public function clear()
{
$this->name = '';
$this->command = '';
$this->frequency = '';
$this->container = '';
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class All extends Component
{
public $resource;
public string|null $modalId = null;
public ?string $variables = null;
public array $parameters;
protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit'];
public function mount()
{
$this->parameters = get_route_parameters();
$this->modalId = new Cuid2(7);
}
public function refreshTasks()
{
$this->resource->refresh();
}
public function submit($data)
{
try {
$task = new ScheduledTask();
$task->name = $data['name'];
$task->command = $data['command'];
$task->frequency = $data['frequency'];
$task->container = $data['container'];
$task->team_id = currentTeam()->id;
switch ($this->resource->type()) {
case 'application':
$task->application_id = $this->resource->id;
break;
case 'standalone-postgresql':
$task->standalone_postgresql_id = $this->resource->id;
break;
case 'service':
$task->service_id = $this->resource->id;
break;
}
$task->save();
$this->refreshTasks();
$this->dispatch('success', 'Scheduled task added successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Livewire\Project\Shared\ScheduledTask;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
class Executions extends Component
{
public $executions = [];
public $selectedKey;
public function getListeners()
{
return [
"selectTask",
];
}
public function selectTask($key): void
{
if ($key == $this->selectedKey) {
$this->selectedKey = null;
return;
}
$this->selectedKey = $key;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask as ModelsScheduledTask;
use Livewire\Component;
use App\Models\Application;
use App\Models\Service;
use Visus\Cuid2\Cuid2;
class Show extends Component
{
public $parameters;
public Application|Service $resource;
public ModelsScheduledTask $task;
public ?string $modalId = null;
public string $type;
protected $rules = [
'task.name' => 'required|string',
'task.command' => 'required|string',
'task.frequency' => 'required|string',
'task.container' => 'nullable|string',
];
protected $validationAttributes = [
'name' => 'name',
'command' => 'command',
'frequency' => 'frequency',
'container' => 'container',
];
public function mount()
{
$this->parameters = get_route_parameters();
if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application';
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
} else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
}
$this->modalId = new Cuid2(7);
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
}
public function submit()
{
$this->validate();
$this->task->save();
$this->dispatch('success', 'Scheduled task updated successfully.');
$this->dispatch('refreshTasks');
}
public function delete()
{
try {
$this->task->delete();
if ($this->type == 'application') {
return redirect()->route('project.application.configuration', $this->parameters);
}
else {
return redirect()->route('project.service.configuration', $this->parameters);
}
} catch (\Exception $e) {
return handleError($e);
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Livewire\Project;
use App\Models\Project;
use Livewire\Component;
class Show extends Component
{
public Project $project;
public function mount() {
$projectUuid = request()->route('project_uuid');
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
if (!$project) {
return redirect()->route('dashboard');
}
$project->load(['environments']);
$this->project = $project;
}
public function render()
{
return view('livewire.project.show');
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Livewire\PrivateKey;
namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;

View File

@@ -1,11 +1,11 @@
<?php
namespace App\Livewire\PrivateKey;
namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey;
use Livewire\Component;
class Change extends Component
class Show extends Component
{
public PrivateKey $private_key;
public $public_key;
@@ -24,6 +24,7 @@ class Change extends Component
public function mount()
{
try {
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
$this->public_key = $this->private_key->publicKey();
}catch(\Throwable $e) {
return handleError($e, $this);

View File

@@ -19,7 +19,7 @@ class Delete extends Component
return;
}
$this->server->delete();
return redirect()->route('server.all');
return redirect()->route('server.index');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -15,7 +15,7 @@ class Show extends Component
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
return redirect()->route('server.index');
}
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -6,7 +6,7 @@ use App\Models\Server;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
class All extends Component
class Index extends Component
{
public ?Collection $servers = null;
@@ -15,6 +15,6 @@ class All extends Component
}
public function render()
{
return view('livewire.server.all');
return view('livewire.server.index');
}
}

View File

@@ -43,7 +43,7 @@ class LogDrains extends Component
try {
$server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($server)) {
return redirect()->route('server.all');
return redirect()->route('server.index');
}
$this->server = $server;
} catch (\Throwable $e) {

View File

@@ -17,7 +17,7 @@ class Show extends Component
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
return redirect()->route('server.index');
}
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false);
} catch (\Throwable $e) {

View File

@@ -15,7 +15,7 @@ class Logs extends Component
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
return redirect()->route('server.index');
}
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -20,7 +20,7 @@ class Show extends Component
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
return redirect()->route('server.index');
}
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -17,7 +17,7 @@ class Show extends Component
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
return redirect()->route('server.index');
}
} catch (\Throwable $e) {

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Livewire\Settings;
use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\StandalonePostgresql;
use Livewire\Component;
class Index extends Component
{
public InstanceSettings $settings;
public StandalonePostgresql $database;
public $s3s;
public function mount()
{
if (isInstanceAdmin()) {
$settings = InstanceSettings::get();
$database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($database) {
if ($database->status !== 'running') {
$database->status = 'running';
$database->save();
}
$this->database = $database;
}
$this->settings = $settings;
$this->s3s = $s3s;
} else {
return redirect()->route('dashboard');
}
}
public function render()
{
return view('livewire.settings.index');
}
}

View File

@@ -1,15 +1,16 @@
<?php
namespace App\Livewire;
namespace App\Livewire\Settings;
use App\Actions\License\CheckResaleLicense;
use App\Models\InstanceSettings;
use Livewire\Component;
class CheckLicense extends Component
class License extends Component
{
public InstanceSettings|null $settings = null;
public InstanceSettings $settings;
public string|null $instance_id = null;
protected $rules = [
'settings.resale_license' => 'nullable',
'settings.is_resale_license_active' => 'nullable',
@@ -20,12 +21,17 @@ class CheckLicense extends Component
'settings.is_resale_license_active' => 'Is License Active',
];
public function mount()
{
public function mount () {
if (!isCloud()) {
abort(404);
}
$this->instance_id = config('app.id');
$this->settings = InstanceSettings::get();
}
public function render()
{
return view('livewire.settings.license')->layout('layouts.subscription');
}
public function submit()
{
$this->validate();

View File

@@ -6,7 +6,7 @@ use App\Models\InstanceSettings;
use App\Providers\RouteServiceProvider;
use Livewire\Component;
class Show extends Component
class Index extends Component
{
public InstanceSettings $settings;
public bool $alreadySubscribed = false;
@@ -26,6 +26,6 @@ class Show extends Component
}
public function render()
{
return view('livewire.subscription.show')->layout('layouts.subscription');
return view('livewire.subscription.index')->layout('layouts.subscription');
}
}

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Livewire\Team;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
class Delete extends Component
{
public function delete()
{
$currentTeam = currentTeam();
$currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) {
if ($user->id === auth()->user()->id) {
return;
}
$user->teams()->detach($currentTeam);
$session = DB::table('sessions')->where('user_id', $user->id)->first();
if ($session) {
DB::table('sessions')->where('id', $session->id)->delete();
}
});
refreshSession();
return redirect()->route('team.index');
}
}

View File

@@ -1,35 +0,0 @@
<?php
namespace App\Livewire\Team;
use App\Models\Team;
use Livewire\Component;
class Form extends Component
{
public Team $team;
protected $rules = [
'team.name' => 'required|min:3|max:255',
'team.description' => 'nullable|min:3|max:255',
];
protected $validationAttributes = [
'team.name' => 'name',
'team.description' => 'description',
];
public function mount()
{
$this->team = currentTeam();
}
public function submit()
{
$this->validate();
try {
$this->team->save();
refreshSession();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\TeamInvitation;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
class Index extends Component
{
public $invitations = [];
public Team $team;
protected $rules = [
'team.name' => 'required|min:3|max:255',
'team.description' => 'nullable|min:3|max:255',
];
protected $validationAttributes = [
'team.name' => 'name',
'team.description' => 'description',
];
public function mount() {
$this->team = currentTeam();
if (auth()->user()->isAdminFromSession()) {
$this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
}
public function render()
{
return view('livewire.team.index');
}
public function submit()
{
$this->validate();
try {
$this->team->save();
refreshSession();
$this->dispatch('success', 'Team updated successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function delete()
{
$currentTeam = currentTeam();
$currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) {
if ($user->id === auth()->user()->id) {
return;
}
$user->teams()->detach($currentTeam);
$session = DB::table('sessions')->where('user_id', $user->id)->first();
if ($session) {
DB::table('sessions')->where('id', $session->id)->delete();
}
});
refreshSession();
return redirect()->route('team.index');
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Livewire\Team\Member;
use App\Models\TeamInvitation;
use Livewire\Component;
class Index extends Component
{
public $invitations = [];
public function mount() {
if (auth()->user()->isAdminFromSession()) {
$this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
}
public function render()
{
return view('livewire.team.member.index');
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Livewire\Team\Notification;
use Livewire\Component;
class Index extends Component
{
public function render()
{
return view('livewire.team.notification.index');
}
}

View File

@@ -65,7 +65,7 @@ class Create extends Component
$this->storage->team_id = currentTeam()->id;
$this->storage->testConnection();
$this->storage->save();
return redirect()->route('team.storages.show', $this->storage->uuid);
return redirect()->route('team.storage.show', $this->storage->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -43,7 +43,7 @@ class Form extends Component
{
try {
$this->storage->delete();
return redirect()->route('team.storages.all');
return redirect()->route('team.storage.index');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Livewire\Team\Storage;
use App\Models\S3Storage;
use Livewire\Component;
class Index extends Component
{
public $s3;
public function mount() {
$this->s3 = S3Storage::ownedByCurrentTeam()->get();
}
public function render()
{
return view('livewire.team.storage.index');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Livewire\Team\Storage;
use App\Models\S3Storage;
use Livewire\Component;
class Show extends Component
{
public $storage = null;
public function mount()
{
$this->storage = S3Storage::ownedByCurrentTeam()->whereUuid(request()->storage_uuid)->first();
if (!$this->storage) {
abort(404);
}
}
public function render()
{
return view('livewire.team.storage.show');
}
}

View File

@@ -315,6 +315,11 @@ class Application extends BaseModel
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('key', 'like', 'NIXPACKS_%');
}
public function scheduled_tasks(): HasMany
{
return $this->hasMany(ScheduledTask::class)->orderBy('name', 'asc');
}
public function private_key()
{
return $this->belongsTo(PrivateKey::class);
@@ -425,7 +430,7 @@ class Application extends BaseModel
public function isConfigurationChanged($save = false)
{
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
if ($this->pull_request_id === 0) {
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables->all());
} else {
$newConfigHash .= json_encode($this->environment_variables_preview->all());
@@ -755,7 +760,6 @@ class Application extends BaseModel
// if (count($this->ports_mappings_array) > 0) {
// $deployment->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
$containers = getCurrentApplicationContainerStatus($server, $this->id, $pullRequestId);
ray($containers);
// if ($pullRequestId === 0) {
// $containers = $containers->filter(function ($container) use ($containerName) {
// return data_get($container, 'Names') !== $containerName;
@@ -867,7 +871,6 @@ class Application extends BaseModel
} else {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base);
}
ray($git_clone_command);
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, "mkdir -p /root/.ssh"),

View File

@@ -48,7 +48,7 @@ class S3Storage extends BaseModel
if ($this->unusable_email_sent === false && is_transactional_emails_active()) {
$mail = new MailMessage();
$mail->subject('Coolify: S3 Storage Connection Error');
$mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('team.storages.show', ['storage_uuid' => $this->uuid])]);
$mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('team.storage.show', ['storage_uuid' => $this->uuid])]);
$users = collect([]);
$members = $this->team->members()->get();
foreach ($members as $user) {

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
class ScheduledTask extends BaseModel
{
protected $guarded = [];
public function service()
{
return $this->belongsTo(Service::class);
}
public function application()
{
return $this->belongsTo(Application::class);
}
public function latest_log(): HasOne
{
return $this->hasOne(ScheduledTaskExecution::class)->latest();
}
public function executions(): HasMany
{
return $this->hasMany(ScheduledTaskExecution::class);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ScheduledTaskExecution extends BaseModel
{
protected $guarded = [];
public function scheduledTask(): BelongsTo
{
return $this->belongsTo(ScheduledTask::class);
}
}

View File

@@ -358,10 +358,10 @@ class Server extends BaseModel
public function validateOS(): bool | Stringable
{
$os_release = instant_remote_process(['cat /etc/os-release'], $this);
$datas = collect(explode("\n", $os_release));
$releaseLines = collect(explode("\n", $os_release));
$collectedData = collect([]);
foreach ($datas as $data) {
$item = Str::of($data)->trim();
foreach ($releaseLines as $line) {
$item = Str::of($line)->trim();
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
}
$ID = data_get($collectedData, 'ID');

View File

@@ -396,6 +396,10 @@ class Service extends BaseModel
}
return null;
}
public function scheduled_tasks(): HasMany
{
return $this->hasMany(ScheduledTask::class)->orderBy('name', 'asc');
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');

View File

@@ -14,8 +14,8 @@ class EmailChannel
{
try {
$this->bootConfigs($notifiable);
$recepients = $notifiable->getRecepients($notification);
if (count($recepients) === 0) {
$recipients = $notifiable->getRecepients($notification);
if (count($recipients) === 0) {
throw new Exception('No email recipients found');
}
@@ -24,7 +24,7 @@ class EmailChannel
[],
[],
fn (Message $message) => $message
->to($recepients)
->to($recipients)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
@@ -35,8 +35,8 @@ class EmailChannel
}
ray($e->getMessage());
$message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
if (isset($recepients)) {
$message .= implode(', ', $recepients);
if (isset($recipients)) {
$message .= implode(', ', $recipients);
}
if (isset($mailMessage)) {
$message .= " with subject: {$mailMessage->subject}";

View File

@@ -27,7 +27,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: A service ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->subject("Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->view('emails.container-restarted', [
'containerName' => $this->name,
'serverName' => $this->server->name,
@@ -38,12 +38,12 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: A service ({$this->name}) has been restarted automatically on {$this->server->name}";
$message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = "Coolify: A service ({$this->name}) has been restarted automatically on {$this->server->name}";
$message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}";
$payload = [
"message" => $message,
];

View File

@@ -26,7 +26,7 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: A service ({$this->name}) has been stopped on {$this->server->name}");
$mail->subject("Coolify: A resource has been stopped unexpectedly on {$this->server->name}");
$mail->view('emails.container-stopped', [
'containerName' => $this->name,
'serverName' => $this->server->name,
@@ -37,12 +37,12 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: A service ({$this->name}) has been stopped on {$this->server->name}";
$message = "Coolify: A resource has been stopped unexpectedly on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = "Coolify: A service ($this->name} has been stopped on {$this->server->name}";
$message = "Coolify: A resource has been stopped unexpectedly on {$this->server->name}";
$payload = [
"message" => $message,
];

View File

@@ -31,7 +31,7 @@ class FortifyServiceProvider extends ServiceProvider
{
// First user (root) will be redirected to /settings instead of / on registration.
if ($request->user()->currentTeam->id === 0) {
return redirect()->route('settings.configuration');
return redirect()->route('settings.index');
}
return redirect(RouteServiceProvider::HOME);
}

View File

@@ -226,8 +226,8 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if (is_null($port) && !is_null($onlyPort)) {
$port = $onlyPort;
}
$http_label = "{$uuid}-{$loop}-http";
$https_label = "{$uuid}-{$loop}-https";
$http_label = "http-{$loop}-{$uuid}";
$https_label = "https-{$loop}-{$uuid}";
if ($schema === 'https') {
// Set labels for https
@@ -249,6 +249,10 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
// Set labels for http (redirect to https)
$labels->push("traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
$labels->push("traefik.http.routers.{$http_label}.entryPoints=http");
if ($port) {
$labels->push("traefik.http.services.{$http_label}.loadbalancer.server.port=$port");
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}");
}
if ($is_force_https_enabled) {
$labels->push("traefik.http.routers.{$http_label}.middlewares=redirect-to-https");
}
@@ -258,27 +262,26 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.routers.{$http_label}.entryPoints=http");
$labels->push("traefik.http.routers.{$http_label}.middlewares=gzip");
if ($port) {
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}");
$labels->push("traefik.http.services.{$http_label}.loadbalancer.server.port=$port");
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}");
}
if ($path !== '/') {
$labels->push("traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix");
$labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
}
}
} catch(\Throwable $e) {
} catch (\Throwable $e) {
continue;
}
}
return $labels;
return $labels->sort();
}
function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array
{
$ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
$onlyPort = null;
if (count($ports) === 1) {
if (count($ports) > 0) {
$onlyPort = $ports[0];
}
$pull_request_id = data_get($preview, 'pull_request_id', 0);

View File

@@ -101,7 +101,6 @@ function generate_default_proxy_configuration(Server $server)
$labels = [
"traefik.enable=true",
"traefik.http.routers.traefik.entrypoints=http",
"traefik.http.routers.traefik.middlewares=traefik-basic-auth@file",
"traefik.http.routers.traefik.service=api@internal",
"traefik.http.services.traefik.loadbalancer.server.port=8080",
// Global Middlewares
@@ -156,7 +155,7 @@ function generate_default_proxy_configuration(Server $server)
],
];
if (isDev()) {
$config['services']['traefik']['command'][] = "--log.level=debug";
// $config['services']['traefik']['command'][] = "--log.level=debug";
$config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log";
$config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100";
}

View File

@@ -67,6 +67,47 @@ function savePrivateKeyToFs(Server $server)
return $location;
}
function generateScpCommand(Server $server, string $source, string $dest)
{
$user = $server->user;
$port = $server->port;
$privateKeyLocation = savePrivateKeyToFs($server);
$timeout = config('constants.ssh.command_timeout');
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$scp_command = "timeout $timeout scp ";
$scp_command .= "-i {$privateKeyLocation} "
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
. '-o PasswordAuthentication=no '
. "-o ConnectTimeout=$connectionTimeout "
. "-o ServerAliveInterval=$serverInterval "
. '-o RequestTTY=no '
. '-o LogLevel=ERROR '
. "-P {$port} "
. "{$source} "
. "{$user}@{$server->ip}:{$dest}";
return $scp_command;
}
function instant_scp(string $source, string $dest, Server $server, $throwError = true)
{
$timeout = config('constants.ssh.command_timeout');
$scp_command = generateScpCommand($server, $source, $dest);
$process = Process::timeout($timeout)->run($scp_command);
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
if (!$throwError) {
return null;
}
return excludeCertainErrors($process->errorOutput(), $exitCode);
}
if ($output === 'null') {
$output = null;
}
return $output;
}
function generateSshCommand(Server $server, string $command, bool $isMux = true)
{
$user = $server->user;

View File

@@ -408,7 +408,7 @@ function generateFqdn(Server $server, string $random)
}
function sslip(Server $server)
{
if (isDev()) {
if (isDev() && $server->id === 0) {
return "http://127.0.0.1.sslip.io";
}
if ($server->ip === 'host.docker.internal') {
@@ -1035,7 +1035,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE);
if (!data_get($service, 'restart')) {
data_set($service, 'restart', RESTART_MODE);
}
data_set($service, 'container_name', $containerName);
data_forget($service, 'volumes.*.content');
data_forget($service, 'volumes.*.isDirectory');
@@ -1473,7 +1475,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE);
if (!data_get($service, 'restart')) {
data_set($service, 'restart', RESTART_MODE);
}
data_set($service, 'container_name', $containerName);
data_forget($service, 'volumes.*.content');
data_forget($service, 'volumes.*.isDirectory');

View File

@@ -53,7 +53,9 @@ return [
'temporary_file_upload' => [
'disk' => null, // Example: 'local', 's3' | Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'rules' => [ // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'file', 'max:256000'
],
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...

View File

@@ -65,7 +65,7 @@ return [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 300,
'retry_after' => 3600,
'block_for' => null,
'after_commit' => true,
],

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.178',
'release' => '4.0.0-beta.186',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.178';
return '4.0.0-beta.186';

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('scheduled_tasks', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->boolean('enabled')->default(true);
$table->string('name');
$table->string('command');
$table->string('frequency');
$table->string('container')->nullable();
$table->timestamps();
$table->foreignId('application_id')->nullable();
$table->foreignId('service_id')->nullable();
$table->foreignId('team_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('scheduled_tasks');
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('scheduled_task_executions', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->enum('status', ['success', 'failed', 'running'])->default('running');
$table->longText('message')->nullable();
$table->foreignId('scheduled_task_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('scheduled_task_executions');
}
};

View File

@@ -8,9 +8,9 @@ ARG DOCKER_COMPOSE_VERSION=2.21.0
# https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.11.2
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.31.0
ARG PACK_VERSION=0.32.1
# https://github.com/railwayapp/nixpacks/releases
ARG NIXPACKS_VERSION=1.18.0
ARG NIXPACKS_VERSION=1.20.0
USER root
WORKDIR /artifacts

View File

@@ -16,7 +16,7 @@ RUN apt-get update
RUN apt-get install postgresql-client-$POSTGRES_VERSION -y
# Coolify requirements
RUN apt-get install -y php-pgsql openssh-client git git-lfs jq lsof
RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof
RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY --chmod=755 docker/dev-ssu/etc/s6-overlay/ /etc/s6-overlay/
@@ -37,3 +37,7 @@ RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
RUN { \
echo 'upload_max_filesize=256M'; \
echo 'post_max_size=256M'; \
} > /etc/php/current_version/cli/conf.d/upload-limits.ini

View File

@@ -31,7 +31,7 @@ RUN apt-get update
RUN apt-get install postgresql-client-$POSTGRES_VERSION -y
# Coolify requirements
RUN apt-get install -y php-pgsql openssh-client git git-lfs jq lsof
RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof
RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY docker/prod-ssu/nginx.conf /etc/nginx/conf.d/custom.conf
@@ -62,3 +62,8 @@ RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
RUN { \
echo 'upload_max_filesize=256M'; \
echo 'post_max_size=256M'; \
} > /etc/php/current_version/cli/conf.d/upload-limits.ini

View File

@@ -1,3 +0,0 @@
<x-layout-simple>
<livewire:force-password-reset />
</x-layout-simple>

View File

@@ -13,8 +13,8 @@
href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button>
</a>
<a class="{{ request()->routeIs('project.application.deployments') ? 'text-white' : '' }}"
href="{{ route('project.application.deployments', $parameters) }}">
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'text-white' : '' }}"
href="{{ route('project.application.deployment.index', $parameters) }}">
<button>Deployments</button>
</a>
<x-applications.links :application="$application" />

View File

@@ -1,12 +0,0 @@
@isset($title, $action)
<div tabindex="0" x-data="{ open: false }"
class="transition border rounded cursor-pointer collapse collapse-arrow border-coolgray-200"
:class="open ? 'collapse-open' : 'collapse-close'">
<div class="flex flex-col justify-center text-sm select-text collapse-title" x-on:click="open = !open">
{{ $title }}
</div>
<div class="collapse-content">
{{ $action }}
</div>
</div>
@endisset

View File

@@ -16,8 +16,8 @@
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$database->getMorphClass() === 'App\Models\StandaloneMysql' ||
$database->getMorphClass() === 'App\Models\StandaloneMariadb')
<a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}"
href="{{ route('project.database.backups.all', $parameters) }}">
<a class="{{ request()->routeIs('project.database.backup.index') ? 'text-white' : '' }}"
href="{{ route('project.database.backup.index', $parameters) }}">
<button>Backups</button>
</a>
@endif

View File

@@ -302,7 +302,7 @@
</div>
<div class="mt-1 text-base leading-7 text-gray-300">
Once you connected your server, Coolify will start managing it and do a
lot of adminstrative tasks for you. You can also write your own scripts to
lot of administrative tasks for you. You can also write your own scripts to
automate your server<span class="text-warning">*</span>.
</div>
</div>
@@ -384,7 +384,7 @@
<div class="text-2xl font-semibold text-white">Powerful API</div>
</div>
<div class="mt-1 text-base leading-7 text-gray-300">
Programatically deploy, query, and manage your servers & resources.
Programmatically deploy, query, and manage your servers & resources.
Integrate to your CI/CD pipelines, or build your own custom integrations. <span
class="text-warning">*</span>
</div>

View File

@@ -1,7 +1,7 @@
<nav class="flex pt-2 pb-10">
<ol class="flex items-center">
<li class="inline-flex items-center">
<a wire:nagivate class="text-xs truncate lg:text-sm"
<a wire:navigate class="text-xs truncate lg:text-sm"
href="{{ route('project.show', ['project_uuid' => $this->parameters['project_uuid']]) }}">
{{ data_get($resource, 'environment.project.name', 'Undefined Name') }}</a>
</li>
@@ -14,7 +14,7 @@
clip-rule="evenodd"></path>
</svg>
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.resources', ['environment_name' => $this->parameters['environment_name'], 'project_uuid' => $this->parameters['project_uuid']]) }}">{{ $this->parameters['environment_name'] }}</a>
href="{{ route('project.resource.index', ['environment_name' => $this->parameters['environment_name'], 'project_uuid' => $this->parameters['project_uuid']]) }}">{{ $this->parameters['environment_name'] }}</a>
</div>
</li>
<li>

View File

@@ -2,8 +2,8 @@
<h1>Settings</h1>
<div class="subtitle">Instance wide settings for Coolify.</div>
<nav class="navbar-main">
<a class="{{ request()->routeIs('settings.configuration') ? 'text-white' : '' }}"
href="{{ route('settings.configuration') }}">
<a class="{{ request()->routeIs('settings.index') ? 'text-white' : '' }}"
href="{{ route('settings.index') }}">
<button>Configuration</button>
</a>
@if (isCloud())

View File

@@ -17,15 +17,15 @@
<a class="{{ request()->routeIs('team.index') ? 'text-white' : '' }}" href="{{ route('team.index') }}">
<button>General</button>
</a>
<a class="{{ request()->routeIs('team.members') ? 'text-white' : '' }}" href="{{ route('team.members') }}">
<a class="{{ request()->routeIs('team.member.index') ? 'text-white' : '' }}" href="{{ route('team.member.index') }}">
<button>Members</button>
</a>
<a class="{{ request()->routeIs('team.storages.all') ? 'text-white' : '' }}"
href="{{ route('team.storages.all') }}">
<a class="{{ request()->routeIs('team.storage.index') ? 'text-white' : '' }}"
href="{{ route('team.storage.index') }}">
<button>S3 Storages</button>
</a>
<a class="{{ request()->routeIs('team.notifications') ? 'text-white' : '' }}"
href="{{ route('team.notifications') }}">
<a class="{{ request()->routeIs('team.notification.index') ? 'text-white' : '' }}"
href="{{ route('team.notification.index') }}">
<button>Notifications</button>
</a>
<div class="flex-1"></div>

View File

@@ -1,11 +1,10 @@
<x-emails.layout>
@if ($pull_request_id === 0)
Failed to deploy a new version of {{ $name }} at [{{ $fqdn }}]({{ $fqdn }}) .
@else
Failed to deploy a pull request #{{ $pull_request_id }} of {{ $name }} at
[{{ $fqdn }}]({{ $fqdn }}).
@endif
[View Deployment Logs]({{ $deployment_url }})
@if ($pull_request_id === 0)
Failed to deploy a new version of {{ $name }} at [{{ $fqdn }}]({{ $fqdn }}) .
@else
Failed to deploy a pull request #{{ $pull_request_id }} of {{ $name }} at
[{{ $fqdn }}]({{ $fqdn }}).
@endif
[View Deployment Logs]({{ $deployment_url }})
</x-emails.layout>

View File

@@ -1,11 +1,11 @@
<x-emails.layout>
@if ($pull_request_id === 0)
A new version of {{ $name }} is available at [{{ $fqdn }}]({{ $fqdn }}) .
@else
Pull request #{{ $pull_request_id }} of {{ $name }} deployed successfully
[{{ $fqdn }}]({{ $fqdn }}).
@endif
@if ($pull_request_id === 0)
A new version of {{ $name }} is available at [{{ $fqdn }}]({{ $fqdn }}) .
@else
Pull request #{{ $pull_request_id }} of {{ $name }} deployed successfully
[{{ $fqdn }}]({{ $fqdn }}).
@endif
[View Deployment Logs]({{ $deployment_url }})
[View Deployment Logs]({{ $deployment_url }})
</x-emails.layout>

Some files were not shown because too many files have changed in this diff Show More