Compare commits

...

70 Commits

Author SHA1 Message Date
Andras Bacsai
af11d8cf3d Merge pull request #1650 from coollabsio/next
v4.0.0-beta.191
2024-01-15 11:06:15 +01:00
Andras Bacsai
48990db699 Add link to documentation for further help 2024-01-15 11:00:09 +01:00
Andras Bacsai
da71353bfa Update application and proxy configuration 2024-01-15 10:49:39 +01:00
Andras Bacsai
0f5559bc61 fix: service stack view 2024-01-15 10:19:37 +01:00
Andras Bacsai
1afb509c33 add domain validation + custom dns servers
add new guides / docs
2024-01-15 10:03:15 +01:00
Andras Bacsai
bccca6e874 add docs to server validation 2024-01-15 09:03:46 +01:00
Andras Bacsai
083dc15053 Update version and add OpenSSH server detection and PermitRootLogin check 2024-01-15 08:40:46 +01:00
Andras Bacsai
1b6d376472 refactor: compose file and install script 2024-01-12 21:26:51 +01:00
Andras Bacsai
891deee05a Update version numbers to 4.0.0-beta.191 2024-01-12 15:42:05 +01:00
Andras Bacsai
5ffbba908b Refactor healthcheck test in StartPostgresql.php 2024-01-12 15:41:49 +01:00
Andras Bacsai
f762959c9f Refactor select.blade.php file 2024-01-12 15:41:46 +01:00
Andras Bacsai
90a5a23fd9 Fix initial username placeholder in PostgreSQL database view 2024-01-12 15:41:43 +01:00
Andras Bacsai
94e87141ff Merge pull request #1639 from coollabsio/next
Fix resource limits for CPU set
2024-01-12 14:37:34 +01:00
Andras Bacsai
fceaf3e94b Fix resource limits for CPU set 2024-01-12 14:30:25 +01:00
Andras Bacsai
3be554cb55 Merge pull request #1638 from coollabsio/next
v4.0.0-beta.190
2024-01-12 14:20:13 +01:00
Andras Bacsai
27b18fbedf Refactor database and application start scripts 2024-01-12 14:15:15 +01:00
Andras Bacsai
5e7c6906b3 fix: cpuset limits was determined in a way that apps only used 1 CPU max, ehh, sorry. 2024-01-12 13:47:01 +01:00
Andras Bacsai
d05ffe32a3 Merge pull request #1630 from coollabsio/next
v4.0.0-beta.189
2024-01-12 12:51:00 +01:00
Andras Bacsai
f1298d1db4 do not send notification of scheduled task failed 2024-01-12 12:44:08 +01:00
Andras Bacsai
408738e08d Add service status to Index.php and update resource display in index.blade.php 2024-01-12 11:31:51 +01:00
Andras Bacsai
8d04fbdb74 feat: search between resources 2024-01-12 11:25:20 +01:00
Andras Bacsai
dccb31d17e fix: service deletion command 2024-01-12 09:11:36 +01:00
Andras Bacsai
f61210287e Update release version to 4.0.0-beta.189 2024-01-12 08:56:59 +01:00
Andras Bacsai
18ad7220f0 10 mins server check -> 5 mins 2024-01-12 08:49:03 +01:00
Andras Bacsai
79e0df1d43 fix: cleanup docker stuffs before upgrading 2024-01-12 08:45:24 +01:00
Andras Bacsai
a2f53085e5 fix: preview deployments with nixpacks 2024-01-12 08:38:08 +01:00
Andras Bacsai
c5782252ea Merge branch 'fix' into next 2024-01-11 19:00:13 +01:00
Andras Bacsai
bf3d88facd Merge pull request #1632 from coollabsio/fix
v4.0.0-beta.188
2024-01-11 18:59:23 +01:00
Andras Bacsai
e45b0bf715 Update version numbers to 4.0.0-beta.188 2024-01-11 18:55:20 +01:00
Andras Bacsai
9db6c12eea fix missing a end tag 2024-01-11 18:54:55 +01:00
Andras Bacsai
3a391b69e8 fix: restart should not update config hash 2024-01-11 14:34:48 +01:00
Andras Bacsai
cc1fb83c79 cleanup 2024-01-11 14:25:55 +01:00
Andras Bacsai
efa5dd28f1 fix: load profile and set envs on remote cmd 2024-01-11 14:25:42 +01:00
Andras Bacsai
f1eddae379 cleanup 2024-01-11 14:24:54 +01:00
Andras Bacsai
34febe670d fix: load profile on remote commands 2024-01-11 14:13:43 +01:00
Andras Bacsai
3137131a1a Merge pull request #1628 from coollabsio/next
v4.0.0-beta.187
2024-01-11 12:57:01 +01:00
Andras Bacsai
1b6546d26c fix: nixpacks envs
fix: append logs
2024-01-11 12:56:02 +01:00
Andras Bacsai
b9f820cef4 fix: save cmd output propely (merge) 2024-01-11 12:33:28 +01:00
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
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
Ray Berger
5ee29c6072 fix typos 2024-01-07 22:32:54 +00:00
Stuart Rowlands
557e1407d0 Added database import feature. 2024-01-06 15:24:57 +10: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
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
85 changed files with 1709 additions and 319 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

@@ -57,7 +57,6 @@ class StartMariadb
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
@@ -69,6 +68,9 @@ class StartMariadb
]
]
];
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',

View File

@@ -64,7 +64,6 @@ class StartMongodb
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
@@ -76,6 +75,9 @@ class StartMongodb
]
]
];
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
@@ -121,7 +123,7 @@ class StartMongodb
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server,callEventOnFinish: 'DatabaseStatusChanged');
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
}
private function generate_local_persistent_volumes()

View File

@@ -57,7 +57,6 @@ class StartMysql
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
@@ -69,6 +68,9 @@ class StartMysql
]
]
];
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',

View File

@@ -50,12 +50,8 @@ class StartPostgresql
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'pg_isready',
'-d',
$this->database->postgres_db,
'-U',
$this->database->postgres_user,
"CMD-SHELL",
"psql -U {$this->database->postgres_user} -d {$this->database->postgres_db} -c 'SELECT 1' || exit 1"
],
'interval' => '5s',
'timeout' => '5s',
@@ -67,7 +63,6 @@ class StartPostgresql
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
@@ -79,6 +74,9 @@ class StartPostgresql
]
]
];
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
ray('Log Drain Enabled');
$docker_compose['services'][$container_name]['logging'] = [

View File

@@ -66,7 +66,6 @@ class StartRedis
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
@@ -78,6 +77,9 @@ class StartRedis
]
]
];
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Actions\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Server;
class CleanupDocker
{
use AsAction;
public function handle(Server $server, bool $force = true)
{
if ($force) {
instant_remote_process(['docker image prune -af'], $server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
instant_remote_process(['docker builder prune -af'], $server, false);
} else {
instant_remote_process(['docker image prune -f'], $server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
instant_remote_process(['docker builder prune -f'], $server, false);
}
}
}

View File

@@ -22,6 +22,7 @@ class UpdateCoolify
if (!$this->server) {
return;
}
CleanupDocker::run($this->server, false);
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
ray('latest version:' . $this->latestVersion . " current version: " . $this->currentVersion . ' force: ' . $force);

View File

@@ -99,7 +99,7 @@ class Init extends Command
return;
}
try {
Http::get("https://undead.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";
@@ -142,83 +142,83 @@ class Init extends Command
try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) {
echo "Deleting stucked application: {$application->name}\n";
echo "Deleting stuck application: {$application->name}\n";
$application->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked application: {$e->getMessage()}\n";
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($postgresqls as $postgresql) {
echo "Deleting stucked postgresql: {$postgresql->name}\n";
echo "Deleting stuck postgresql: {$postgresql->name}\n";
$postgresql->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked postgresql: {$e->getMessage()}\n";
echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n";
}
try {
$redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($redis as $redis) {
echo "Deleting stucked redis: {$redis->name}\n";
echo "Deleting stuck redis: {$redis->name}\n";
$redis->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked redis: {$e->getMessage()}\n";
echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
}
try {
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mongodbs as $mongodb) {
echo "Deleting stucked mongodb: {$mongodb->name}\n";
echo "Deleting stuck mongodb: {$mongodb->name}\n";
$mongodb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked mongodb: {$e->getMessage()}\n";
echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n";
}
try {
$mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mysqls as $mysql) {
echo "Deleting stucked mysql: {$mysql->name}\n";
echo "Deleting stuck mysql: {$mysql->name}\n";
$mysql->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked mysql: {$e->getMessage()}\n";
echo "Error in cleaning stuck mysql: {$e->getMessage()}\n";
}
try {
$mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mariadbs as $mariadb) {
echo "Deleting stucked mariadb: {$mariadb->name}\n";
echo "Deleting stuck mariadb: {$mariadb->name}\n";
$mariadb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked mariadb: {$e->getMessage()}\n";
echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n";
}
try {
$services = Service::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($services as $service) {
echo "Deleting stucked service: {$service->name}\n";
echo "Deleting stuck service: {$service->name}\n";
$service->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked service: {$e->getMessage()}\n";
echo "Error in cleaning stuck service: {$e->getMessage()}\n";
}
try {
$serviceApps = ServiceApplication::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($serviceApps as $serviceApp) {
echo "Deleting stucked serviceapp: {$serviceApp->name}\n";
echo "Deleting stuck serviceapp: {$serviceApp->name}\n";
$serviceApp->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked serviceapp: {$e->getMessage()}\n";
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
}
try {
$serviceDbs = ServiceDatabase::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($serviceDbs as $serviceDb) {
echo "Deleting stucked serviceapp: {$serviceDb->name}\n";
echo "Deleting stuck serviceapp: {$serviceDb->name}\n";
$serviceDb->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked serviceapp: {$e->getMessage()}\n";
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
}
// Cleanup any resources that are not attached to any environment or destination or server

View File

@@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Jobs\DeleteResourceJob;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
@@ -91,7 +92,7 @@ class ServicesDelete extends Command
if (!$confirmed) {
break;
}
$toDelete->delete();
DeleteResourceJob::dispatch($toDelete);
}
}
}
@@ -115,7 +116,7 @@ class ServicesDelete extends Command
if (!$confirmed) {
return;
}
$toDelete->delete();
DeleteResourceJob::dispatch($toDelete);
}
}
}
@@ -139,7 +140,7 @@ class ServicesDelete extends Command
if (!$confirmed) {
return;
}
$toDelete->delete();
DeleteResourceJob::dispatch($toDelete);
}
}
}

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)
@@ -68,7 +72,7 @@ class Kernel extends ConsoleKernel
}
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
$schedule->job(new ServerStatusJob($server))->everyFiveMinutes()->onOneServer();
}
}
private function instance_auto_update($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

@@ -3,14 +3,13 @@
namespace App\Http\Controllers;
use App\Events\TestEvent;
use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\StandalonePostgresql;
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;
@@ -35,25 +34,25 @@ class Controller extends BaseController
public function verify() {
return view('auth.verify-email');
}
public function email_verify() {
request()->fulfill();
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() {
public function forgot_password(Request $request) {
if (is_transactional_emails_active()) {
$arrayOfRequest = request()->only(Fortify::email());
request()->merge([
$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']);
$request->validate([Fortify::email() => 'required|email']);
$status = Password::broker(config('fortify.passwords'))->sendResetLink(
request()->only(Fortify::email())
$request->only(Fortify::email())
);
if ($status == Password::RESET_LINK_SENT) {
return app(SuccessfulPasswordResetLinkRequestResponse::class, ['status' => $status]);

View File

@@ -72,6 +72,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private bool $is_debug_enabled;
private $build_args;
private $env_args;
private $env_nixpacks_args;
private $docker_compose;
private $docker_compose_base64;
private ?string $nixpacks_plan = null;
@@ -202,7 +203,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
$this->application->isConfigurationChanged(false);
return;
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
@@ -692,7 +693,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
"save" => "health_check",
"append" => false
],
);
@@ -736,13 +738,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_nixpacks_confs();
}
$this->generate_compose_file();
// Needs separate preview variables
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
if ($this->application->build_pack !== 'nixpacks') {
$this->add_build_env_variables_to_dockerfile();
}
$this->build_image();
$this->stop_running_container();
if ($this->application->destination->server->isSwarm()) {
ray("{$this->workdir}{$this->docker_compose_location}");
$this->push_to_docker_registry();
$this->execute_remote_command(
[
@@ -854,7 +858,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
],
);
}
if ($this->saved_outputs->get('git_commit_sha')) {
$this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t");
}
@@ -910,17 +913,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->nixpacks_plan) {
$parsed = Toml::Parse($this->nixpacks_plan);
// Do any modifications here
$cmds = collect(data_get($parsed, 'phases.setup.cmds', []));
data_set($parsed, 'phases.setup.cmds', $cmds);
$this->nixpacks_plan = json_encode($parsed);
$this->generate_env_variables();
$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();
$nixpacks_command = "nixpacks plan -f toml {$this->env_args}";
$this->generate_nixpacks_env_variables();
$nixpacks_command = "nixpacks plan -f toml {$this->env_nixpacks_args}";
if ($this->application->build_command) {
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
}
@@ -933,27 +937,33 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$nixpacks_command .= " {$this->workdir}";
return $nixpacks_command;
}
private function generate_nixpacks_env_variables()
{
$this->env_nixpacks_args = collect([]);
if ($this->pull_request_id === 0) {
foreach ($this->application->nixpacks_environment_variables as $env) {
$this->env_nixpacks_args->push("--env {$env->key}={$env->value}");
}
} else {
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
$this->env_nixpacks_args->push("--env {$env->key}={$env->value}");
}
}
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');
}
private function generate_env_variables()
{
$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}");
}
foreach ($this->application->build_environment_variables as $env) {
$this->env_args->push("--env {$env->key}={$env->value}");
$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}");
}
foreach ($this->application->build_environment_variables_preview as $env) {
$this->env_args->push("--env {$env->key}={$env->value}");
$this->env_args->put($env->key, $env->value);
}
}
$this->env_args = $this->env_args->implode(' ');
}
private function generate_compose_file()
@@ -1006,7 +1016,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'mem_swappiness' => $this->application->limits_memory_swappiness,
'mem_reservation' => $this->application->limits_memory_reservation,
'cpus' => (float) $this->application->limits_cpus,
'cpuset' => $this->application->limits_cpuset,
'cpu_shares' => $this->application->limits_cpu_shares,
]
],
@@ -1018,6 +1027,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
]
];
if (!is_null($this->application->limits_cpuset)) {
data_set($docker_compose, 'services.' . $this->container_name . '.cpuset', $this->application->limits_cpuset);
}
if ($this->server->isSwarm()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.container_name');
data_forget($docker_compose, 'services.' . $this->container_name . '.expose');
@@ -1275,26 +1287,33 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} else {
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 > {$this->workdir}/thegameplan.json"), "hidden" => true]);
$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 thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), "hidden" => true
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 thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), "hidden" => true
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) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "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}"), "hidden" => true
]);
$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 {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "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}"), "hidden" => true
]);
$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}
@@ -1320,6 +1339,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")
@@ -1328,38 +1349,55 @@ 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 {
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 > {$this->workdir}/thegameplan.json"), "hidden" => true]);
$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 thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), "hidden" => true
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 thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), "hidden" => true
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) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "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}"), "hidden" => true
]);
$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 {
$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
]);
$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
]
);
}
}
}
@@ -1439,11 +1477,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

@@ -2,6 +2,7 @@
namespace App\Jobs;
use App\Actions\Server\CleanupDocker;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -43,9 +44,7 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
ray('Usage before: ' . $this->usageBefore);
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name);
instant_remote_process(['docker image prune -af'], $this->server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server, false);
instant_remote_process(['docker builder prune -af'], $this->server, false);
CleanupDocker::run($this->server);
$usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->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

@@ -209,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)
{
@@ -235,9 +235,16 @@ class General extends Component
]);
}
if (data_get($this->application, 'fqdn')) {
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower();
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$domains = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$domains = $domains->unique();
foreach ($domains as $domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS settings for: $domain failed.<br>Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline' href='https://coolify.io/docs/dns-settings'>documentation</a> for further help.");
}
}
$this->application->fqdn = $domains->implode(',');
}

View File

@@ -39,7 +39,7 @@ class Heading extends Component
} else {
dispatch(new ServerStatusJob($this->application->destination->server));
}
if ($showNotification) $this->dispatch('success', "Application ({$this->application->name}) status updated.");
if ($showNotification) $this->dispatch('success', "Application status updated.");
}
public function force_deploy_without_cache()

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

@@ -10,7 +10,15 @@ class Index extends Component
{
public Project $project;
public Environment $environment;
public function mount () {
public $applications = [];
public $postgresqls = [];
public $redis = [];
public $mongodbs = [];
public $mysqls = [];
public $mariadbs = [];
public $services = [];
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
@@ -21,6 +29,84 @@ class Index extends Component
}
$this->project = $project;
$this->environment = $environment;
$this->applications = $environment->applications->sortBy('name');
$this->applications = $this->applications->map(function ($application) {
if (data_get($application, 'environment.project.uuid')) {
$application->hrefLink = route('project.application.configuration', [
'project_uuid' => data_get($application, 'environment.project.uuid'),
'environment_name' => data_get($application, 'environment.name'),
'application_uuid' => data_get($application, 'uuid')
]);
}
return $application;
});
$this->postgresqls = $environment->postgresqls->sortBy('name');
$this->postgresqls = $this->postgresqls->map(function ($postgresql) {
if (data_get($postgresql, 'environment.project.uuid')) {
$postgresql->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($postgresql, 'environment.project.uuid'),
'environment_name' => data_get($postgresql, 'environment.name'),
'database_uuid' => data_get($postgresql, 'uuid')
]);
}
return $postgresql;
});
$this->redis = $environment->redis->sortBy('name');
$this->redis = $this->redis->map(function ($redis) {
if (data_get($redis, 'environment.project.uuid')) {
$redis->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($redis, 'environment.project.uuid'),
'environment_name' => data_get($redis, 'environment.name'),
'database_uuid' => data_get($redis, 'uuid')
]);
}
return $redis;
});
$this->mongodbs = $environment->mongodbs->sortBy('name');
$this->mongodbs = $this->mongodbs->map(function ($mongodb) {
if (data_get($mongodb, 'environment.project.uuid')) {
$mongodb->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($mongodb, 'environment.project.uuid'),
'environment_name' => data_get($mongodb, 'environment.name'),
'database_uuid' => data_get($mongodb, 'uuid')
]);
}
return $mongodb;
});
$this->mysqls = $environment->mysqls->sortBy('name');
$this->mysqls = $this->mysqls->map(function ($mysql) {
if (data_get($mysql, 'environment.project.uuid')) {
$mysql->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($mysql, 'environment.project.uuid'),
'environment_name' => data_get($mysql, 'environment.name'),
'database_uuid' => data_get($mysql, 'uuid')
]);
}
return $mysql;
});
$this->mariadbs = $environment->mariadbs->sortBy('name');
$this->mariadbs = $this->mariadbs->map(function ($mariadb) {
if (data_get($mariadb, 'environment.project.uuid')) {
$mariadb->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($mariadb, 'environment.project.uuid'),
'environment_name' => data_get($mariadb, 'environment.name'),
'database_uuid' => data_get($mariadb, 'uuid')
]);
}
return $mariadb;
});
$this->services = $environment->services->sortBy('name');
$this->services = $this->services->map(function ($service) {
if (data_get($service, 'environment.project.uuid')) {
$service->hrefLink = route('project.service.configuration', [
'project_uuid' => data_get($service, 'environment.project.uuid'),
'environment_name' => data_get($service, 'environment.name'),
'service_uuid' => data_get($service, 'uuid')
]);
$service->status = serviceStatus($service);
}
return $service;
});
}
public function render()
{

View File

@@ -108,8 +108,7 @@ class ExecuteContainerCommand extends Component
$this->validate();
try {
// Wrap command to prevent escaped execution in the host.
$cmd = 'sh -c "' . str_replace('"', '\"', $this->command) . '"';
$cmd = 'sh -c "if [ -f ~/.profile ]; then . ~/.profile; fi; ' . str_replace('"', '\"', $this->command) . '"';
if (!empty($this->workDir)) {
$exec = "docker exec -w {$this->workDir} {$this->container} {$cmd}";
} else {

View File

@@ -44,8 +44,8 @@ class ResourceLimits extends Component
if (!$this->resource->limits_cpus) {
$this->resource->limits_cpus = "0";
}
if (!$this->resource->limits_cpuset) {
$this->resource->limits_cpuset = "0";
if ($this->resource->limits_cpuset === "") {
$this->resource->limits_cpuset = null;
}
if (!$this->resource->limits_cpu_shares) {
$this->resource->limits_cpu_shares = 1024;

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

@@ -76,7 +76,7 @@ class Form extends Component
$this->server->settings->is_usable = true;
$this->server->settings->save();
} else {
$this->dispatch('error', 'Server is not reachable. Please check your connection and configuration.');
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.');
return;
}
}
@@ -85,7 +85,7 @@ class Form extends Component
try {
$uptime = $this->server->validateConnection();
if (!$uptime) {
$install && $this->dispatch('error', 'Server is not reachable. Please check your connection and configuration.');
$install && $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.');
return;
}
$supported_os_type = $this->server->validateOS();

View File

@@ -39,7 +39,7 @@ class ShowPrivateKey extends Component
if ($uptime) {
$this->dispatch('success', 'Server is reachable.');
} else {
$this->dispatch('error', 'Server is not reachable. Please check your connection and private key configuration.');
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.');
return;
}
} catch (\Throwable $e) {

View File

@@ -15,6 +15,7 @@ class Configuration extends Component
public bool $do_not_track;
public bool $is_auto_update_enabled;
public bool $is_registration_enabled;
public bool $is_dns_validation_enabled;
public bool $next_channel;
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
protected Server $server;
@@ -24,12 +25,14 @@ class Configuration extends Component
'settings.resale_license' => 'nullable',
'settings.public_port_min' => 'required',
'settings.public_port_max' => 'required',
'settings.custom_dns_servers' => 'nullable',
];
protected $validationAttributes = [
'settings.fqdn' => 'FQDN',
'settings.resale_license' => 'Resale License',
'settings.public_port_min' => 'Public port min',
'settings.public_port_max' => 'Public port max',
'settings.custom_dns_servers' => 'Custom DNS servers',
];
public function mount()
@@ -38,6 +41,7 @@ class Configuration extends Component
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
$this->next_channel = $this->settings->next_channel;
$this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled;
}
public function instantSave()
@@ -45,6 +49,7 @@ class Configuration extends Component
$this->settings->do_not_track = $this->do_not_track;
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled;
$this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
if ($this->next_channel) {
$this->settings->next_channel = false;
$this->next_channel = false;
@@ -63,6 +68,14 @@ class Configuration extends Component
return;
}
$this->validate();
$this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim();
$this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) {
return str($dns)->trim()->lower();
});
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique();
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(',');
$this->settings->save();
$this->server = Server::findOrFail(0);
$this->setup_instance_fqdn();

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);
@@ -424,7 +429,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;
$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->dockerfile . $this->dockerfile_location . $this->custom_labels;
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables->all());
} else {
@@ -637,7 +642,6 @@ class Application extends BaseModel
'mem_swappiness' => $this->limits_memory_swappiness,
'mem_reservation' => $this->limits_memory_reservation,
'cpus' => (float) $this->limits_cpus,
'cpuset' => $this->limits_cpuset,
'cpu_shares' => $this->limits_cpu_shares,
]
],
@@ -649,6 +653,9 @@ class Application extends BaseModel
]
]
];
if (!is_null($this->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->limits_cpuset);
}
if ($server->isSwarm()) {
data_forget($docker_compose, 'services.' . $container_name . '.container_name');
data_forget($docker_compose, 'services.' . $container_name . '.expose');

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

@@ -32,10 +32,11 @@ trait ExecuteRemoteCommand
$hidden = data_get($single_command, 'hidden', false);
$customType = data_get($single_command, 'type');
$ignore_errors = data_get($single_command, 'ignore_errors', false);
$append = data_get($single_command, 'append', true);
$this->save = data_get($single_command, 'save');
$remote_command = generateSshCommand($this->server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType) {
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) {
$output = Str::of($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
@@ -59,7 +60,15 @@ trait ExecuteRemoteCommand
$this->application_deployment_queue->save();
if ($this->save) {
$this->saved_outputs[$this->save] = Str::of($output)->trim();
if (data_get($this->saved_outputs, $this->save, null) === null) {
data_set($this->saved_outputs, $this->save, str());
}
if ($append) {
$this->saved_outputs[$this->save] .= str($output)->trim();
$this->saved_outputs[$this->save] = str($this->saved_outputs[$this->save]);
} else {
$this->saved_outputs[$this->save] = str($output)->trim();
}
}
});
$this->application_deployment_queue->update([

View File

@@ -167,7 +167,6 @@ function generateComposeFile(string $deploymentUuid, Server $server, string $net
'mem_swappiness' => $application->limits_memory_swappiness,
'mem_reservation' => $application->limits_memory_reservation,
'cpus' => (int) $application->limits_cpus,
'cpuset' => $application->limits_cpuset,
'cpu_shares' => $application->limits_cpu_shares,
]
],
@@ -179,6 +178,9 @@ function generateComposeFile(string $deploymentUuid, Server $server, string $net
]
]
];
if (!is_null($application->limits_cpuset)) {
data_set($docker_compose, "services.{$containerName}.cpuset", $application->limits_cpuset);
}
if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) {
$docker_compose['services'][$containerName]['logging'] = [
'driver' => 'fluentd',

View File

@@ -229,11 +229,12 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$http_label = "http-{$loop}-{$uuid}";
$https_label = "https-{$loop}-{$uuid}";
$labels->push("traefik.http.middlewares.gzip.compress=true");
$labels->push("traefik.http.routers.{$https_label}.middlewares=gzip");
if ($schema === 'https') {
// Set labels for https
$labels->push("traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
$labels->push("traefik.http.routers.{$https_label}.entryPoints=https");
$labels->push("traefik.http.routers.{$https_label}.middlewares=gzip");
if ($port) {
$labels->push("traefik.http.routers.{$https_label}.service={$https_label}");
$labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port");
@@ -254,13 +255,13 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}");
}
if ($is_force_https_enabled) {
$labels->push("traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https");
$labels->push("traefik.http.routers.{$http_label}.middlewares=redirect-to-https");
}
} else {
// Set labels for http
$labels->push("traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
$labels->push("traefik.http.routers.{$http_label}.entryPoints=http");
$labels->push("traefik.http.routers.{$http_label}.middlewares=gzip");
if ($port) {
$labels->push("traefik.http.services.{$http_label}.loadbalancer.server.port=$port");
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}");

View File

@@ -103,9 +103,6 @@ function generate_default_proxy_configuration(Server $server)
"traefik.http.routers.traefik.entrypoints=http",
"traefik.http.routers.traefik.service=api@internal",
"traefik.http.services.traefik.loadbalancer.server.port=8080",
// Global Middlewares
"traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https",
"traefik.http.middlewares.gzip.compress=true",
];
$config = [
"version" => "3.8",
@@ -198,10 +195,23 @@ function setup_dynamic_configuration()
$traefik_dynamic_conf = [
'http' =>
[
'middlewares' => [
'redirect-to-https' => [
'redirectscheme' => [
'scheme' => 'https',
],
],
'gzip' => [
'compress' => true,
],
],
'routers' =>
[
'coolify-http' =>
[
'middlewares' => [
0 => 'gzip',
],
'entryPoints' => [
0 => 'http',
],
@@ -251,7 +261,7 @@ function setup_dynamic_configuration()
if ($schema === 'https') {
$traefik_dynamic_conf['http']['routers']['coolify-http']['middlewares'] = [
0 => 'redirect-to-https@docker',
0 => 'redirect-to-https',
];
$traefik_dynamic_conf['http']['routers']['coolify-https'] = [
@@ -288,7 +298,7 @@ function setup_dynamic_configuration()
], $server);
if (config('app.env') == 'local') {
ray($yaml);
// ray($yaml);
}
}
}

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;
@@ -85,7 +126,7 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true)
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$command = "test -f ~/.profile && . ~/.profile; PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$ssh_command .= "-i {$privateKeyLocation} "
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
. '-o PasswordAuthentication=no '

View File

@@ -22,14 +22,11 @@ use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Illuminate\Database\QueryException;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route;
@@ -40,6 +37,7 @@ use Visus\Cuid2\Cuid2;
use phpseclib3\Crypt\RSA;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use PurplePixie\PhpDns\DNSQuery;
function base_configuration_dir(): string
{
@@ -1592,3 +1590,50 @@ function getRealtime()
return $envDefined;
}
}
function validate_dns_entry(string $fqdn, Server $server)
{
$url = Url::fromString($fqdn);
$host = $url->getHost();
if (str($host)->contains('sslip.io')) {
return true;
}
$settings = InstanceSettings::get();
$is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled');
if (!$is_dns_validation_enabled) {
return true;
}
$dnsServers = data_get($settings, 'custom_dns_servers');
$dnsServers = str($dnsServers)->explode(',');
if ($server->id === 0) {
$ip = data_get($settings, 'public_ipv4') || data_get($settings, 'public_ipv6') || $server->ip;
} else {
$ip = $server->ip;
}
$foundMatch = false;
$type = \PurplePixie\PhpDns\DNSTypes::NAME_A;
foreach ($dnsServers as $dnsServer) {
try {
ray("Checking $host on $dnsServer");
$query = new DNSQuery($dnsServer);
$results = $query->query($host, $type);
if ($results === false || $query->hasError()) {
ray("Error: " . $query->getLasterror());
} else {
foreach ($results as $result) {
if ($result->getType() == $type) {
if ($result->getData() === $ip) {
ray($host . " has IP address " . $result->getData());
ray($result->getString());
$foundMatch = true;
break;
}
}
}
}
} catch (\Exception $e) {
}
}
ray("Found match: $foundMatch");
return $foundMatch;
}

View File

@@ -28,6 +28,7 @@
"nubs/random-name-generator": "^2.2",
"phpseclib/phpseclib": "~3.0",
"poliander/cron": "^3.0",
"purplepixie/phpdns": "^2.1",
"pusher/pusher-php-server": "^7.2",
"resend/resend-laravel": "^0.5.0",
"sentry/sentry-laravel": "^3.4",

50
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "44337ff4ff1d9c435d9776fec01ebe9c",
"content-hash": "de3b59fade9b132d2582a40dcf3c00f9",
"packages": [
{
"name": "amphp/amp",
@@ -6287,6 +6287,54 @@
},
"time": "2023-10-14T21:56:36+00:00"
},
{
"name": "purplepixie/phpdns",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/purplepixie/phpdns.git",
"reference": "e1e4f18a60d01947e2aac7157325a9e2e7755bf7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/purplepixie/phpdns/zipball/e1e4f18a60d01947e2aac7157325a9e2e7755bf7",
"reference": "e1e4f18a60d01947e2aac7157325a9e2e7755bf7",
"shasum": ""
},
"require": {
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-0": {
"PurplePixie": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-3.0-only"
],
"authors": [
{
"name": "David Cutting",
"email": "dcutting@purplepixie.org"
}
],
"description": "PHP DNS Direct Query Module",
"support": {
"issues": "https://github.com/purplepixie/phpdns/issues",
"source": "https://github.com/purplepixie/phpdns/tree/2.1.0"
},
"time": "2023-11-06T15:37:19+00:00"
},
{
"name": "pusher/pusher-php-server",
"version": "7.2.4",

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

@@ -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.185',
'release' => '4.0.0-beta.191',
// 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.185';
return '4.0.0-beta.191';

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

@@ -0,0 +1,76 @@
<?php
use App\Models\Application;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
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::table('applications', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default(null)->change();
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default(null)->change();
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default(null)->change();
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default(null)->change();
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default(null)->change();
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default(null)->change();
});
Application::where('limits_cpuset', '0')->update(['limits_cpuset' => null]);
StandalonePostgresql::where('limits_cpuset', '0')->update(['limits_cpuset' => null]);
StandaloneRedis::where('limits_cpuset', '0')->update(['limits_cpuset' => null]);
StandaloneMariadb::where('limits_cpuset', '0')->update(['limits_cpuset' => null]);
StandaloneMysql::where('limits_cpuset', '0')->update(['limits_cpuset' => null]);
StandaloneMongodb::where('limits_cpuset', '0')->update(['limits_cpuset' => null]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default("0")->change();
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default("0")->change();
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default("0")->change();
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default("0")->change();
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default("0")->change();
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->string('limits_cpuset')->nullable()->default("0")->change();
});
Application::where('limits_cpuset', null)->update(['limits_cpuset' => '0']);
StandalonePostgresql::where('limits_cpuset', null)->update(['limits_cpuset' => '0']);
StandaloneRedis::where('limits_cpuset', null)->update(['limits_cpuset' => '0']);
StandaloneMariadb::where('limits_cpuset', null)->update(['limits_cpuset' => '0']);
StandaloneMysql::where('limits_cpuset', null)->update(['limits_cpuset' => '0']);
StandaloneMongodb::where('limits_cpuset', null)->update(['limits_cpuset' => '0']);
}
};

View File

@@ -0,0 +1,30 @@
<?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::table('instance_settings', function (Blueprint $table) {
$table->boolean('is_dns_validation_enabled')->default(true);
$table->string('custom_dns_servers')->nullable()->default('1.1.1.1');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('instance_settings', function (Blueprint $table) {
$table->dropColumn('is_dns_validation_enabled');
$table->dropColumn('custom_dns_servers');
});
}
};

View File

@@ -1,7 +1,7 @@
version: '3.8'
services:
coolify:
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-4.0.0-beta.153}"
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-4.0.0-beta.190}"
volumes:
- type: bind
source: /data/coolify/source/.env
@@ -10,6 +10,7 @@ services:
- /data/coolify/ssh:/var/www/html/storage/app/ssh
- /data/coolify/applications:/var/www/html/storage/app/applications
- /data/coolify/databases:/var/www/html/storage/app/databases
- /data/coolify/services:/var/www/html/storage/app/services
- /data/coolify/backups:/var/www/html/storage/app/backups
environment:
- APP_ID

View File

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

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

@@ -80,7 +80,7 @@ a {
}
.description {
@apply pt-2 text-xs font-bold text-neutral-500 group-hover:text-white;
@apply text-xs font-bold text-neutral-500 group-hover:text-white;
}
.lds-heart {

View File

@@ -1,12 +0,0 @@
<x-layout>
<h1>Command Center</h1>
<div class="subtitle">Execute commands on your servers without leaving the browser.</div>
@if ($servers->count() > 0)
<livewire:run-command :servers="$servers" />
@else
<div>
<div>No servers found. Without a server, you won't be able to do much.</div>
<x-use-magic-bar link="/server/new" />
</div>
@endif
</x-layout>

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>

View File

@@ -1,5 +1,6 @@
<x-emails.layout>
Connection could not be establised with one of your S3 Storage ({{ $name }}). Please fix it [here]({{ $url }}).
Connection could not be established with one of your S3 Storage ({{ $name }}). Please fix it [here]({{ $url }}).
{{ $reason }}
</x-emails.layout>

View File

@@ -56,6 +56,8 @@
Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for user
'root' or skip the boarding process and add a new private key manually to Coolify and to the
server.
<br />
Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.
<x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="box" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Check again

View File

@@ -1,7 +1,7 @@
<div>
<dialog id="sendTestEmail" class="modal">
<form method="dialog" class="flex flex-col gap-2 rounded modal-box" wire:submit='submit'>
<x-forms.input placeholder="test@example.com" id="emails" label="Recepients" required />
<x-forms.input placeholder="test@example.com" id="emails" label="Recipients" required />
<x-forms.button onclick="sendTestEmail.close()" wire:click="sendTestNotification">
Send Email
</x-forms.button>

View File

@@ -18,7 +18,9 @@
href="#">Environment
Variables</a>
@endif
<a :class="activeTab === 'scheduled-tasks' && 'text-white'"
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'" href="#">Scheduled Tasks
</a>
@if ($application->git_based())
<a :class="activeTab === 'source' && 'text-white'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@@ -54,6 +56,7 @@
href="#">Resource Limits
</a>
@endif
<a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
</a>
@@ -97,6 +100,9 @@
<div x-cloak x-show="activeTab === 'resource-limits'">
<livewire:project.shared.resource-limits :resource="$application" />
</div>
<div x-cloak x-show="activeTab === 'scheduled-tasks'">
<livewire:project.shared.scheduled-task.all :resource="$application" />
</div>
<div x-cloak x-show="activeTab === 'danger'">
<livewire:project.shared.danger :resource="$application" />
</div>

View File

@@ -7,7 +7,7 @@
</x-forms.button>
</div>
@isset($rate_limit_remaining)
<div class="pt-1 ">Requests remaning till rate limited by Git: {{ $rate_limit_remaining }}</div>
<div class="pt-1 ">Requests remaining till rate limited by Git: {{ $rate_limit_remaining }}</div>
@endisset
@if (count($pull_requests) > 0)
<div wire:loading.remove wire:target='load_prs'>

View File

@@ -1,7 +1,7 @@
<div>
<h1>Configuration</h1>
<livewire:project.database.heading :database="$database" />
<x-modal modalId="startDatabase">
<x-modal modalId="startDatabase" noSubmit>
<x-slot:modalBody>
<livewire:activity-monitor header="Database Startup Logs" />
</x-slot:modalBody>
@@ -31,6 +31,11 @@
window.location.hash = 'storages'"
href="#">Storages
</a>
<a :class="activeTab === 'import' && 'text-white'"
@click.prevent="activeTab = 'import';
window.location.hash = 'import'" href="#">Import
Backup
</a>
<a :class="activeTab === 'webhooks' && 'text-white'"
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
</a>
@@ -39,6 +44,7 @@
window.location.hash = 'resource-limits'"
href="#">Resource Limits
</a>
<a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger';
window.location.hash = 'danger'"
@@ -74,6 +80,9 @@
<div x-cloak x-show="activeTab === 'resource-limits'">
<livewire:project.shared.resource-limits :resource="$database" />
</div>
<div x-cloak x-show="activeTab === 'import'">
<livewire:project.database.import :resource="$database" />
</div>
<div x-cloak x-show="activeTab === 'danger'">
<livewire:project.shared.danger :resource="$database" />
</div>

View File

@@ -0,0 +1,60 @@
<div>
<h2>Import Backup</h2>
<div class="mt-2 mb-4 rounded alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>This is a destructive action, existing data will be replaced!</span>
</div>
@if (str(data_get($resource, 'status'))->startsWith('running'))
@if (!$validated)
<div>{{ $validationMsg }}</div>
@else
<form disabled wire:submit.prevent="runImport" x-data="{ isFinished: false, isUploading: false, progress: 0 }">
@if ($resource->type() === 'standalone-postgresql')
<x-forms.input class="mb-2" label="Custom Import Command"
wire:model='postgresqlRestoreCommand'></x-forms.input>
@elseif ($resource->type() === 'standalone-mysql')
<x-forms.input class="mb-2" label="Custom Import Command"
wire:model='mysqlRestoreCommand'></x-forms.input>
@elseif ($resource->type() === 'standalone-mariadb')
<x-forms.input class="mb-2" label="Custom Import Command"
wire:model='mariadbRestoreCommand'></x-forms.input>
@endif
<div x-on:livewire-upload-start="isUploading = true; isFinished = false"
x-on:livewire-upload-finish="isUploading = false; isFinished = true"
x-on:livewire-upload-error="isUploading = false"
x-on:livewire-upload-progress="progress = $event.detail.progress">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="file_input">Upload
file</label>
<input wire:model="file"
class="block w-full text-sm rounded cursor-pointer text-whiteborder bg-coolgray-100 border-coolgray-400 focus:outline-none"
aria-describedby="file_input_help" id="file_input" type="file">
<p class="mt-1 text-sm text-neutral-500" id="file_input_help">Max file size: 256MB
</p>
@error('file')
<span class="error">{{ $message }}</span>
@enderror
<div x-show="isUploading">
<progress max="100" x-bind:value="progress"
class="progress progress-warning"></progress>
</div>
</div>
<x-forms.button type="submit" class="w-full mt-4" x-show="isFinished">Import Backup</x-forms.button>
</form>
@endif
@if ($scpInProgress)
<div>Database backup is being copied to server...</div>
@endif
<div class="container w-full pt-4 mx-auto">
<livewire:activity-monitor header="Database import output" />
</div>
@else
<div class="text-neutral-500">Database must be running to import a backup.</div>
@endif
</div>

View File

@@ -29,7 +29,7 @@
@if ($database->started_at)
<div class="flex gap-2">
<x-forms.input label="Initial Username" id="database.postgres_username" placeholder="If empty: postgres"
<x-forms.input label="Initial Username" id="database.postgres_user" placeholder="If empty: postgres"
readonly helper="You can only change this in the database." />
<x-forms.input label="Initial Password" id="database.postgres_password" type="password" required
readonly helper="You can only change this in the database." />

View File

@@ -10,8 +10,8 @@
helper="
You can use these variables in your Docker Compose file and Coolify will generate default values or replace them with the values you set on the UI forms.<br>
<br>
- SERVICE_FQDN_*: FQDN - could be changable from the UI. (example: SERVICE_FQDN_GHOST)<br>
- SERVICE_URL_*: URL parsed from FQDN - could be changable from the UI. (example: SERVICE_URL_GHOST)<br>
- SERVICE_FQDN_*: FQDN - could be changeable from the UI. (example: SERVICE_FQDN_GHOST)<br>
- SERVICE_URL_*: URL parsed from FQDN - could be changeable from the UI. (example: SERVICE_URL_GHOST)<br>
- SERVICE_BASE64_64_*: Generated 'base64' string with length of '64' (example: SERVICE_BASE64_64_GHOST, to generate 32 bit: SERVICE_BASE64_32_GHOST)<br>
- SERVICE_USER_*: Generated user (example: SERVICE_USER_MYSQL)<br>
- SERVICE_PASSWORD_*: Generated password (example: SERVICE_PASSWORD_MYSQL)<br>"

View File

@@ -202,10 +202,7 @@
<li class="step step-secondary">Select a Server</li>
<li class="step">Select a Destination</li>
</ul>
@if ($isDatabase)
<div class="text-center">Swarm clusters are excluded from this type of resource at the moment. It will
be activated soon. Stay tuned.</div>
@endif
{{-- @if ($isDatabase)
<div class="flex items-center justify-center pt-4">
<x-forms.checkbox instantSave wire:model="includeSwarm"
@@ -235,6 +232,10 @@
</div>
@endforelse
</div>
@if ($isDatabase)
<div class="text-center">Swarm clusters are excluded from this type of resource at the moment. It will
be activated soon. Stay tuned.</div>
@endif
@endif
@if ($current_step === 'destinations')
<ul class="pb-10 steps">
@@ -267,7 +268,7 @@
</div>
@endforeach
@endif
<a href="{{ route('destination.new', ['server_id' => $server_id]) }}"
<a href="{{ route('destination.new', ['server_id' => $server_id]) }}"
class="items-center justify-center pb-10 text-center box-without-bg group bg-coollabs hover:bg-coollabs-100">
<div class="flex flex-col mx-6 ">
<div class="font-bold text-white">

View File

@@ -44,54 +44,216 @@
<a href="{{ route('project.resource.create', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
class="items-center justify-center box">+ Add New Resource</a>
@endif
<div class="grid gap-2 lg:grid-cols-2">
@foreach ($environment->applications->sortBy('name') as $application)
<a class="relative box group"
href="{{ route('project.application.configuration', [$project->uuid, $environment->name, $application->uuid]) }}">
<div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $application->name }}</div>
<div class="description">{{ $application->description }}</div>
</div>
@if (Str::of(data_get($application, 'status'))->startsWith('running'))
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($application, 'status'))->startsWith('exited'))
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($application, 'status'))->startsWith('restarting'))
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
@endif
</a>
@endforeach
@foreach ($environment->databases()->sortBy('name') as $database)
<a class="relative box group"
href="{{ route('project.database.configuration', [$project->uuid, $environment->name, $database->uuid]) }}">
<div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $database->name }}</div>
<div class="description">{{ $database->description }}</div>
</div>
@if (Str::of(data_get($database, 'status'))->startsWith('running'))
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($database, 'status'))->startsWith('exited'))
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($database, 'status'))->startsWith('restaring'))
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
@endif
</a>
@endforeach
@foreach ($environment->services->sortBy('name') as $service)
<a class="relative box group"
href="{{ route('project.service.configuration', [$project->uuid, $environment->name, $service->uuid]) }}">
<div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $service->name }}</div>
<div class="description">{{ $service->description }}</div>
</div>
@if (Str::of(serviceStatus($service))->startsWith('running'))
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(serviceStatus($service))->startsWith('degraded'))
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(serviceStatus($service))->startsWith('exited'))
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
@endif
</a>
@endforeach
<div x-data="searchComponent()">
<x-forms.input placeholder="Search for name, fqdn..." class="w-full" x-model="search" />
<div class="grid gap-2 pt-4 lg:grid-cols-2">
<template x-for="item in filteredApplications" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
<div class="description" x-text="item.fqdn"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</template>
<template x-for="item in filteredPostgresqls" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</template>
<template x-for="item in filteredRedis" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</template>
<template x-for="item in filteredMongodbs" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</template>
<template x-for="item in filteredMysqls" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</template>
<template x-for="item in filteredMariadbs" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</template>
<template x-for="item in filteredServices" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('degraded')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</template>
</div>
</div>
</div>
<script>
function searchComponent() {
return {
search: '',
applications: @js($applications),
postgresqls: @js($postgresqls),
redis: @js($redis),
mongodbs: @js($mongodbs),
mysqls: @js($mysqls),
mariadbs: @js($mariadbs),
services: @js($services),
get filteredApplications() {
if (this.search === '') {
return this.applications;
}
this.applications = Object.values(this.applications);
return this.applications.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.fqdn?.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
},
get filteredPostgresqls() {
if (this.search === '') {
return this.postgresqls;
}
this.postgresqls = Object.values(this.postgresqls);
return this.postgresqls.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
},
get filteredRedis() {
if (this.search === '') {
return this.redis;
}
this.redis = Object.values(this.redis);
return this.redis.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
},
get filteredMongodbs() {
if (this.search === '') {
return this.mongodbs;
}
this.mongodbs = Object.values(this.mongodbs);
return this.mongodbs.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
},
get filteredMysqls() {
if (this.search === '') {
return this.mysqls;
}
this.mysqls = Object.values(this.mysqls);
return this.mysqls.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
},
get filteredMariadbs() {
if (this.search === '') {
return this.mariadbs;
}
this.mariadbs = Object.values(this.mariadbs);
return this.mariadbs.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
},
get filteredServices() {
if (this.search === '') {
return this.services;
}
this.services = Object.values(this.services);
return this.services.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
},
};
}
</script>

View File

@@ -68,8 +68,7 @@
<div class="text-xs">{{ $application->status }}</div>
</div>
<div class="flex items-center px-4">
<a
class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
<a class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.index', [...$parameters, 'service_name' => $application->name]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
@@ -115,8 +114,7 @@
<div class="text-xs">{{ $database->status }}</div>
</div>
<div class="flex items-center px-4">
<a
class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
<a class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.index', [...$parameters, 'service_name' => $database->name]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
@@ -157,9 +155,7 @@
<livewire:project.shared.execute-container-command :resource="$service" />
</div>
<div x-cloak x-show="activeTab === 'environment-variables'">
<div x-cloak x-show="activeTab === 'environment-variables'">
<livewire:project.shared.environment-variable.all :resource="$service" />
</div>
<livewire:project.shared.environment-variable.all :resource="$service" />
</div>
<div x-cloak x-show="activeTab === 'danger'">
<livewire:project.shared.danger :resource="$service" />

View File

@@ -2,7 +2,7 @@
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex h-full pt-6">
<div class="flex flex-col gap-4 min-w-fit">
<a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
<a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
href="{{ route('project.service.configuration', [...$parameters, 'service_name' => null]) }}">
<button><- Back</button>
</a>
@@ -13,6 +13,10 @@
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'; if(window.location.search) window.location.search = ''"
href="#">Storages
</a>
<a :class="activeTab === 'scheduled-tasks' && 'text-white'"
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
href="#">Scheduled Tasks
</a>
@if (
$serviceDatabase?->databaseType() === 'standalone-mysql' ||
$serviceDatabase?->databaseType() === 'standalone-postgresql' ||
@@ -57,6 +61,9 @@
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
</div>
@endisset
<div x-cloak x-show="activeTab === 'scheduled-tasks'">
<livewire:project.shared.scheduled-task.all :resource="$service" />
</div>
</div>
</div>
</div>

View File

@@ -48,5 +48,10 @@
@endforeach
</div>
@endif
@if (
$resource->persistentStorages()->get()->count() == 0 &&
$resource->fileStorages()->get()->count() == 0)
<div class="pt-4">No storages found.</div>
@endif
@endif
</div>

View File

@@ -7,7 +7,7 @@
server <span class="px-1 text-warning">{{ data_get($resource, 'destination.server.name') }}</span>
in <span class="px-1 text-warning"> {{ data_get($resource, 'destination.network') }} </span> network.</a>
</div>
{{-- Additonal Destinations:
{{-- Additional Destinations:
{{$resource->additional_destinations}} --}}
{{-- @if (count($servers) > 0)
<div>

View File

@@ -7,9 +7,15 @@
<div class="">Limit your container resources by CPU & memory.</div>
<h3 class="pt-4">Limit CPUs</h3>
<div class="flex gap-2">
<x-forms.input placeholder="1.5" label="Number of CPUs" id="resource.limits_cpus" />
<x-forms.input placeholder="0-2" label="CPU sets to use" id="resource.limits_cpuset" />
<x-forms.input placeholder="1024" label="CPU Weight" id="resource.limits_cpu_shares" />
<x-forms.input placeholder="1.5"
helper="0 means use all CPUs. Floating point number, like 0.002 or 1.5. More info <a target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
label="Number of CPUs" id="resource.limits_cpus" />
<x-forms.input placeholder="0-2"
helper="Empty means, use all CPU sets. 0-2 will use CPU 0, CPU 1 and CPU 2. More info <a target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
label="CPU sets to use" id="resource.limits_cpuset" />
<x-forms.input placeholder="1024"
helper="More info <a target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
label="CPU Weight" id="resource.limits_cpu_shares" />
</div>
<h3 class="pt-4">Limit Memory</h3>
<div class="flex gap-2">

View File

@@ -0,0 +1,15 @@
<dialog id="newTask" class="modal">
<form method="dialog" class="flex flex-col gap-2 rounded modal-box" wire:submit='submit'>
<h3 class="text-lg font-bold">Add Scheduled Task</h3>
<x-forms.input placeholder="Run cron" id="name" label="Name" required />
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" required />
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" required />
<x-forms.input placeholder="php" id="container" helper="You can leave it empty if your resource only have one container." label="Container name" />
<x-forms.button onclick="newTask.close()" type="submit">
Save
</x-forms.button>
</form>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@@ -0,0 +1,24 @@
<div>
<div class="flex gap-2">
<h2 class="pb-4">Scheduled Tasks</h2>
<x-forms.button class="btn" onclick="newTask.showModal()">+ Add</x-forms.button>
<livewire:project.shared.scheduled-task.add />
</div>
<div class="flex flex-wrap gap-2">
@forelse($resource->scheduled_tasks as $task)
<a class="flex flex-col box"
@if ($resource->type() == 'application')
href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
@elseif ($resource->type() == 'service')
href="{{ route('project.service.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
@endif
<div><span class="font-bold text-warning">{{ $task->name }}<span></div>
<div>Frequency: {{ $task->frequency }}</div>
<div>Last run: {{ data_get($task->latest_log, 'status', 'No runs yet') }}</div>
</a>
@empty
<div class="text-neutral-500">No scheduled tasks configured.</div>
@endforelse
</div>
</div>

View File

@@ -0,0 +1,32 @@
<div class="flex flex-col-reverse gap-2">
@forelse($executions as $execution)
@if (data_get($execution, 'id') == $selectedKey)
<div class="p-2">
@if (data_get($execution, 'message'))
<div>
<pre>{{ data_get($execution, 'message') }}</pre>
</div>
@else
<div>No output was recorded for this execution.</div>
@endif
</div>
@endif
<a wire:click="selectTask({{ data_get($execution, 'id') }})" @class([
'flex flex-col border-l border-dashed transition-colors box-without-bg bg-coolgray-100 hover:bg-coolgray-100',
'bg-coolgray-200 text-white hover:bg-coolgray-200' =>
data_get($execution, 'id') == $selectedKey,
'border-green-500' => data_get($execution, 'status') === 'success',
'border-red-500' => data_get($execution, 'status') === 'failed',
])>
@if (data_get($execution, 'status') === 'running')
<div class="absolute top-2 right-2">
<x-loading />
</div>
@endif
<div>Status: {{ data_get($execution, 'status') }}</div>
<div>Started At: {{ data_get($execution, 'created_at') }}</div>
</a>
@empty
<div>No executions found.</div>
@endforelse
</div>

View File

@@ -0,0 +1,43 @@
<div>
<x-modal yesOrNo modalId="{{ $modalId }}" modalTitle="Delete Scheduled Task">
<x-slot:modalBody>
<p>Are you sure you want to delete this scheduled task <span
class="font-bold text-warning">({{ $task->name }})</span>?</p>
</x-slot:modalBody>
</x-modal>
<h1>Scheduled Task</h1>
@if ($type === 'application')
<livewire:project.application.heading :application="$resource" />
@elseif ($type === 'service')
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" />
@endif
<form wire:submit="submit" class="w-full">
<div class="flex flex-col gap-2 pb-4">
<div class="flex items-end gap-2 pt-4">
<h2>Scheduled Task</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
<x-forms.button isError isModal modalId="{{ $modalId }}">
Delete
</x-forms.button>
</div>
</div>
<div class="flex w-full gap-2">
<x-forms.input placeholder="Name" id="task.name" label="Name" required />
<x-forms.input placeholder="php artisan schedule:run" id="task.command" label="Command" required />
<x-forms.input placeholder="0 0 * * * or daily" id="task.frequency" label="Frequency" required />
<x-forms.input placeholder="php" helper="You can leave it empty if your resource only have one container."
id="task.container" label="Container name" />
</div>
</form>
<div class="pt-4">
<h3 class="py-4">Recent executions</h3>
<livewire:project.shared.scheduled-task.executions key="{{ $task->id }}" selectedKey="" :executions="$task->executions->take(-20)" />
</div>
</div>

View File

@@ -9,8 +9,10 @@
<div>General configuration for your Coolify instance.</div>
<div class="flex flex-col gap-2 pt-4">
<div class="flex gap-2 w-96">
<div class="flex items-end gap-2">
<x-forms.input id="settings.fqdn" label="Instance's Domain" placeholder="https://coolify.io" />
<x-forms.input id="settings.custom_dns_servers" label="DNS Servers" helper="DNS servers for validation FQDNS againts. A comma separated list of DNS servers." placeholder="1.1.1.1,8.8.8.8" />
<x-forms.checkbox instantSave id="is_dns_validation_enabled" label="Validate DNS settings?" />
</div>
{{-- <div class="flex gap-2 ">

View File

@@ -1,7 +1,7 @@
<div>
<dialog id="sendTestEmail" class="modal">
<form method="dialog" class="flex flex-col gap-2 rounded modal-box" wire:submit='submit'>
<x-forms.input placeholder="test@example.com" id="emails" label="Recepients" required />
<x-forms.input placeholder="test@example.com" id="emails" label="Recipients" required />
<x-forms.button onclick="sendTestEmail.close()" wire:click="sendTestNotification">
Send Email
</x-forms.button>

View File

@@ -1,83 +0,0 @@
<x-layout>
<h1>Profile</h1>
<div class="subtitle ">Your user profile settings.</div>
<livewire:profile.form :request="$request" />
<h2 class="py-4">Subscription</h2>
<a href="{{ route('team.index') }}">Check in Team Settings</a>
<h2 class="py-4">Two-factor Authentication</h2>
@if (session('status') == 'two-factor-authentication-enabled')
<div class="mb-4 font-medium">
Please finish configuring two factor authentication below. Read the QR code or enter the secret key
manually.
</div>
<div class="flex flex-col gap-2">
<form action="/user/confirmed-two-factor-authentication" method="POST" class="flex items-end gap-2">
@csrf
<x-forms.input type="number" id="code" label="One-time code" required />
<x-forms.button type="submit">Validate 2FA</x-forms.button>
</form>
<div>
<div>{!! $request->user()->twoFactorQrCodeSvg() !!}</div>
<div x-data="{ showCode: false }" class="py-2">
<template x-if="showCode">
<div class="py-2 ">{!! decrypt($request->user()->two_factor_secret) !!}</div>
</template>
<x-forms.button x-on:click="showCode = !showCode">Show secret key to manually
enter</x-forms.button>
</div>
</div>
</div>
@elseif(session('status') == 'two-factor-authentication-confirmed')
<div class="mb-4 ">
Two factor authentication confirmed and enabled successfully.
</div>
<div>
<div class="pb-6 ">Here are the recovery codes for your account. Please store them in a secure
location.
</div>
<div class="text-white">
@foreach ($request->user()->recoveryCodes() as $code)
<div>{{ $code }}</div>
@endforeach
</div>
</div>
@else
@if ($request->user()->two_factor_confirmed_at)
<div class="pb-4 "> Two factor authentication is <span class="text-helper">enabled</span>.</div>
<div class="flex gap-2">
<form action="/user/two-factor-authentication" method="POST">
@csrf
@method ('DELETE')
<x-forms.button type="submit">Disable</x-forms.button>
</form>
<form action="/user/two-factor-recovery-codes" method="POST">
@csrf
<x-forms.button type="submit">Regenerate Recovery Codes</x-forms.button>
</form>
</div>
@if (session('status') == 'recovery-codes-generated')
<div>
<div class="py-6 ">Here are the recovery codes for your account. Please store them in a
secure
location.
</div>
<div class="text-white">
@foreach ($request->user()->recoveryCodes() as $code)
<div>{{ $code }}</div>
@endforeach
</div>
</div>
@endif
@else
<form action="/user/two-factor-authentication" method="POST">
@csrf
<x-forms.button type="submit">Configure 2FA</x-forms.button>
</form>
@endif
@endif
@if (session()->has('errors'))
<div class="text-error">
Something went wrong. Please try again.
</div>
@endif
</x-layout>

View File

@@ -50,6 +50,7 @@ use App\Livewire\Project\Service\Index as ServiceIndex;
use App\Livewire\Project\EnvironmentEdit;
use App\Livewire\Project\Shared\ExecuteContainerCommand;
use App\Livewire\Project\Shared\Logs;
use App\Livewire\Project\Shared\ScheduledTask\Show as ScheduledTaskShow;
use App\Livewire\Security\ApiTokens;
use App\Livewire\Security\PrivateKey\Create as SecurityPrivateKeyCreate;
@@ -67,6 +68,12 @@ use App\Livewire\Server\Proxy\Logs as ProxyLogs;
use App\Livewire\Source\Github\Change as GitHubChange;
use App\Livewire\Subscription\Index as SubscriptionIndex;
use App\Livewire\Waitlist\Index as WaitlistIndex;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Support\Facades\Password;
use Laravel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse;
use Laravel\Fortify\Contracts\SuccessfulPasswordResetLinkRequestResponse;
use Laravel\Fortify\Fortify;
use Illuminate\Support\Str;
if (isDev()) {
Route::get('/dev/compose', Compose::class)->name('dev.compose');
@@ -139,6 +146,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/deployment/{deployment_uuid}', DeploymentShow::class)->name('project.application.deployment.show');
Route::get('/logs', Logs::class)->name('project.application.logs');
Route::get('/command', ExecuteContainerCommand::class)->name('project.application.command');
Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.application.scheduled-tasks');
});
Route::prefix('project/{project_uuid}/{environment_name}/database/{database_uuid}')->group(function () {
Route::get('/', DatabaseConfiguration::class)->name('project.database.configuration');
@@ -151,6 +159,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/', ServiceConfiguration::class)->name('project.service.configuration');
Route::get('/{service_name}', ServiceIndex::class)->name('project.service.index');
Route::get('/command', ExecuteContainerCommand::class)->name('project.service.command');
Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.service.scheduled-tasks');
});
Route::get('/servers', ServerIndex::class)->name('server.index');

View File

@@ -6,7 +6,7 @@ set -e # Exit immediately if a command exits with a non-zero status
#set -u # Treat unset variables as an error and exit
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
VERSION="1.1.0"
VERSION="1.2.0"
DOCKER_VERSION="24.0"
CDN="https://cdn.coollabs.io/coolify"
@@ -28,7 +28,7 @@ ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensu
;;
esac
# Ovewrite LATEST_VERSION if user pass a version number
# Overwrite LATEST_VERSION if user pass a version number
if [ "$1" != "" ]; then
LATEST_VERSION=$1
LATEST_VERSION="${LATEST_VERSION,,}"
@@ -65,6 +65,44 @@ sles | opensuse-leap | opensuse-tumbleweed)
;;
esac
# Detect OpenSSH server
SSH_DETECTED=false
if [ -x "$(command -v systemctl)" ]; then
if systemctl status sshd >/dev/null 2>&1; then
echo "OpenSSH server is installed and running."
SSH_DETECTED=true
fi
elif [ -x "$(command -v service)" ]; then
if service sshd status >/dev/null 2>&1; then
echo "OpenSSH server is installed and running."
SSH_DETECTED=true
fi
fi
if [ "$SSH_DETECTED" = "false" ]; then
echo "###############################################################################"
echo "WARNING: Could not detect if OpenSSH server is installed and running - this does not mean that it is not installed, just that we could not detect it."
echo -e "Please make sure it is set, otherwise Coolify cannot connect to the host system. \n"
echo "###############################################################################"
fi
# Detect SSH PermitRootLogin
SSH_PERMIT_ROOT_LOGIN=false
SSH_PERMIT_ROOT_LOGIN_CONFIG=$(grep "^PermitRootLogin" /etc/ssh/sshd_config | awk '{print $2}') || SSH_PERMIT_ROOT_LOGIN_CONFIG="N/A (commented out or not found at all)"
if [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "prohibit-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "without-password" ]; then
echo "PermitRootLogin is enabled."
SSH_PERMIT_ROOT_LOGIN=true
fi
if [ "$SSH_PERMIT_ROOT_LOGIN" != "true" ]; then
echo "###############################################################################"
echo "WARNING: PermitRootLogin is not enabled in /etc/ssh/sshd_config."
echo -e "It is set to $SSH_PERMIT_ROOT_LOGIN_CONFIG. Should be prohibit-password, yes or without-password.\n"
echo -e "Please make sure it is set, otherwise Coolify cannot connect to the host system. \n"
echo "(Currently we only support root user to login via SSH, this will be changed in the future.)"
echo "###############################################################################"
fi
if ! [ -x "$(command -v docker)" ]; then
echo "Docker is not installed. Installing Docker."
curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh
@@ -127,9 +165,8 @@ fi
echo -e "-------------"
mkdir -p /data/coolify/ssh/keys
mkdir -p /data/coolify/ssh/mux
mkdir -p /data/coolify/source
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy}
mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic
chown -R 9999:root /data/coolify

View File

@@ -565,7 +565,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- appwrite-builds:/storage/builds:rw
- appwrite-functions:/storage/functions:rw
# Host mount nessessary to share files between executor and runtimes.
# Host mount necessary to share files between executor and runtimes.
# It's not possible to share mount file between 2 containers without host mount (copying is too slow)
- /tmp:/tmp:rw
environment:

View File

@@ -1,5 +1,5 @@
# documentation: https://github.com/mregni/EmbyStat/wiki/docker
# slogan: EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployement, all within your control.
# slogan: EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployment, all within your control.
# tags: media, server, movies, tv, music
services:

View File

@@ -121,7 +121,7 @@
},
"embystat": {
"documentation": "https:\/\/github.com\/mregni\/EmbyStat\/wiki\/docker",
"slogan": "EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployement, all within your control.",
"slogan": "EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployment, all within your control.",
"compose": "c2VydmljZXM6CiAgZW1ieXN0YXQ6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZW1ieXN0YXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0VNQllTVEFUCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZW1ieXN0YXQtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6NjU1NScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=",
"tags": [
"media",

View File

@@ -4,7 +4,7 @@
"version": "3.12.36"
},
"v4": {
"version": "4.0.0-beta.185"
"version": "4.0.0-beta.191"
}
}
}