Merge branch 'next' into main

This commit is contained in:
basigabri
2025-01-15 18:47:59 +02:00
committed by GitHub
345 changed files with 59914 additions and 2876 deletions

View File

@@ -1,16 +1,12 @@
# Coolify Configuration
APP_ID=
APP_NAME=Coolify
APP_KEY=
# PostgreSQL Database Configuration
DB_USERNAME=coolify
DB_PASSWORD=
# Redis Configuration
REDIS_PASSWORD=
# Pusher Configuration
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=

View File

@@ -58,7 +58,7 @@ Special thanks to our biggest sponsors!
* [PFGlabs](https://pfglabs.com/?ref=coolify.io) - Build real project with Golang.
* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
* [Brand Dev](https://brand.dev/?ref=coolify.io) - The #1 Brand API for B2B software startups - instantly pull logos, fonts, descriptions, social links, slogans, and so much more from any domain via a single api call.
* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
* [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps consulting company providing infrastructure automation and cloud optimization services.
@@ -75,6 +75,7 @@ Special thanks to our biggest sponsors!
<a href="https://www.runpod.io/?ref=coolify.io">
<svg style="width:60px;height:60px;background:#fff;" xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 200"><g><path d="M74.5 51.1c-25.4 14.9-27 16-29.6 20.2-1.8 3-1.9 5.3-1.9 32.3 0 21.7.3 29.4 1.3 30.6 1.9 2.5 46.7 27.9 48.5 27.6 1.5-.3 1.7-3.1 2-27.7.2-21.9 0-27.8-1.1-29.5-.8-1.2-9.9-6.8-20.2-12.6-10.3-5.8-19.4-11.5-20.2-12.7-1.8-2.6-.9-5.9 1.8-7.4 1.6-.8 6.3 0 21.8 4C87.8 78.7 98 81 99.6 81c4.4 0 49.9-25.9 49.9-28.4 0-1.6-3.4-2.8-24-8.2-13.2-3.5-25.1-6.3-26.5-6.3-1.4.1-12.4 5.9-24.5 13z"></path><path d="m137.2 68.1-3.3 2.1 6.3 3.7c3.5 2 6.3 4.3 6.3 5.1 0 .9-8 6.1-19.4 12.6-10.6 6-20 11.9-20.7 12.9-1.2 1.6-1.4 7.2-1.2 29.4.3 24.8.5 27.6 2 27.9 1.8.3 46.6-25.1 48.6-27.6.9-1.2 1.2-8.8 1.2-30.2s-.3-29-1.2-30.2c-1.6-1.9-12.1-7.8-13.9-7.8-.8 0-2.9 1-4.7 2.1z"></path></g></svg></a>
<a href="https://lightspeed.run/?ref=coolify.io"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
<a href="https://dartnode.com/?ref=coolify.io"><img src="https://github.com/DartNode-com.png" width="60px" alt="DartNode"/></a>
<a href="https://www.flint.sh/en/home?ref=coolify.io"> <img src="https://github.com/Flint-company.png" width="60px" alt="FlintCompany"/></a>
<a href="https://americancloud.com/?ref=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a>
<a href="https://cryptojobslist.com/?ref=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>

View File

@@ -91,16 +91,9 @@ class RunRemoteProcess
} else {
if ($processResult->exitCode() == 0) {
$status = ProcessStatus::FINISHED;
}
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
} else {
$status = ProcessStatus::ERROR;
}
// if (($processResult->exitCode() == 0 && $this->is_finished) || $this->activity->properties->get('status') === ProcessStatus::FINISHED->value) {
// $status = ProcessStatus::FINISHED;
// }
// if ($processResult->exitCode() != 0 && !$this->ignore_errors) {
// $status = ProcessStatus::ERROR;
// }
}
$this->activity->properties = $this->activity->properties->merge([
@@ -110,9 +103,6 @@ class RunRemoteProcess
'status' => $status->value,
]);
$this->activity->save();
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
throw new \RuntimeException($processResult->errorOutput(), $processResult->exitCode());
}
if ($this->call_event_on_finish) {
try {
if ($this->call_event_data) {
@@ -128,6 +118,9 @@ class RunRemoteProcess
Log::error('Error calling event: '.$e->getMessage());
}
}
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
throw new \RuntimeException($processResult->errorOutput(), $processResult->exitCode());
}
return $processResult;
}

View File

@@ -49,11 +49,7 @@ class StartClickhouse
'hard' => 262144,
],
],
'labels' => [
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
'interval' => '5s',

View File

@@ -67,6 +67,10 @@ class StartDatabaseProxy
$type = \App\Models\StandaloneClickhouse::class;
$containerName = "clickhouse-{$database->service->uuid}";
break;
case 'standalone-supabase/postgres':
$type = \App\Models\StandalonePostgresql::class;
$containerName = "supabase-db-{$database->service->uuid}";
break;
}
}
if ($type === \App\Models\StandaloneRedis::class) {

View File

@@ -46,11 +46,7 @@ class StartDragonfly
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => "redis-cli -a {$this->database->dragonfly_password} ping",
'interval' => '5s',

View File

@@ -48,11 +48,7 @@ class StartKeydb
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => "keydb-cli --pass {$this->database->keydb_password} ping",
'interval' => '5s',

View File

@@ -43,11 +43,7 @@ class StartMariadb
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'],
'interval' => '5s',

View File

@@ -51,11 +51,7 @@ class StartMongodb
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => [
'CMD',

View File

@@ -43,11 +43,7 @@ class StartMysql
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"],
'interval' => '5s',

View File

@@ -23,6 +23,9 @@ class StartPostgresql
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
if (isDev()) {
$this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
}
$this->commands = [
"echo 'Starting database.'",
@@ -47,11 +50,7 @@ class StartPostgresql
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => [
'CMD-SHELL',
@@ -78,7 +77,7 @@ class StartPostgresql
],
],
];
if (! is_null($this->database->limits_cpuset)) {
if (filled($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()) {
@@ -108,7 +107,7 @@ class StartPostgresql
];
}
}
if (! is_null($this->database->postgres_conf) && ! empty($this->database->postgres_conf)) {
if (filled($this->database->postgres_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/custom-postgres.conf',
@@ -199,9 +198,12 @@ class StartPostgresql
private function generate_init_scripts()
{
if (is_null($this->database->init_scripts) || count($this->database->init_scripts) === 0) {
$this->commands[] = "rm -rf $this->configuration_dir/docker-entrypoint-initdb.d/*";
if (blank($this->database->init_scripts) || count($this->database->init_scripts) === 0) {
return;
}
foreach ($this->database->init_scripts as $init_script) {
$filename = data_get($init_script, 'filename');
$content = data_get($init_script, 'content');
@@ -213,10 +215,15 @@ class StartPostgresql
private function add_custom_conf()
{
if (is_null($this->database->postgres_conf) || empty($this->database->postgres_conf)) {
$filename = 'custom-postgres.conf';
$config_file_path = "$this->configuration_dir/$filename";
if (blank($this->database->postgres_conf)) {
$this->commands[] = "rm -f $config_file_path";
return;
}
$filename = 'custom-postgres.conf';
$content = $this->database->postgres_conf;
if (! str($content)->contains('listen_addresses')) {
$content .= "\nlisten_addresses = '*'";
@@ -224,6 +231,6 @@ class StartPostgresql
$this->database->save();
}
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null";
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $config_file_path > /dev/null";
}
}

View File

@@ -48,11 +48,7 @@ class StartRedis
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => [
'CMD-SHELL',

View File

@@ -112,7 +112,7 @@ class GetContainersStatus
$preview->update(['last_online_at' => now()]);
}
} else {
//Notify user that this container should not be there.
// Notify user that this container should not be there.
}
} else {
$application = $this->applications->where('id', $applicationId)->first();
@@ -125,7 +125,7 @@ class GetContainersStatus
$application->update(['last_online_at' => now()]);
}
} else {
//Notify user that this container should not be there.
// Notify user that this container should not be there.
}
}
} else {

View File

@@ -39,6 +39,11 @@ class CleanupStuckedResources extends Command
$servers = Server::all()->filter(function ($server) {
return $server->isFunctional();
});
if (isCloud()) {
$servers = $servers->filter(function ($server) {
return data_get($server->team->subscription, 'stripe_invoice_paid', false) === true;
});
}
foreach ($servers as $server) {
CleanupHelperContainersJob::dispatch($server);
}

View File

@@ -183,7 +183,7 @@ class Emails extends Command
'team_id' => 0,
]);
}
//$this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail();
// $this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail();
$this->sendEmail();
break;
// case 'invitation-link':

View File

@@ -0,0 +1,178 @@
<?php
namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\ApplicationDeploymentQueue;
use App\Repositories\CustomJobRepository;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Laravel\Horizon\Contracts\JobRepository;
use Laravel\Horizon\Contracts\MetricsRepository;
use Laravel\Horizon\Repositories\RedisJobRepository;
use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\select;
use function Laravel\Prompts\table;
use function Laravel\Prompts\text;
class HorizonManage extends Command
{
protected $signature = 'horizon:manage {--can-i-restart-this-worker} {--job-status=}';
protected $description = 'Manage horizon';
public function handle()
{
if ($this->option('can-i-restart-this-worker')) {
return $this->isThereAJobInProgress();
}
if ($this->option('job-status')) {
return $this->getJobStatus($this->option('job-status'));
}
$action = select(
label: 'What to do?',
options: [
'pending' => 'Pending Jobs',
'running' => 'Running Jobs',
'can-i-restart-this-worker' => 'Can I restart this worker?',
'job-status' => 'Job Status',
'workers' => 'Workers',
'failed' => 'Failed Jobs',
'failed-delete' => 'Failed Jobs - Delete',
'purge-queues' => 'Purge Queues',
]
);
if ($action === 'can-i-restart-this-worker') {
$this->isThereAJobInProgress();
}
if ($action === 'job-status') {
$jobId = text('Which job to check?');
$jobStatus = $this->getJobStatus($jobId);
$this->info('Job Status: '.$jobStatus);
}
if ($action === 'pending') {
$pendingJobs = app(JobRepository::class)->getPending();
$pendingJobsTable = [];
if (count($pendingJobs) === 0) {
$this->info('No pending jobs found.');
return;
}
foreach ($pendingJobs as $pendingJob) {
$pendingJobsTable[] = [
'id' => $pendingJob->id,
'name' => $pendingJob->name,
'status' => $pendingJob->status,
'reserved_at' => $pendingJob->reserved_at ? now()->parse($pendingJob->reserved_at)->format('Y-m-d H:i:s') : null,
];
}
table($pendingJobsTable);
}
if ($action === 'failed') {
$failedJobs = app(JobRepository::class)->getFailed();
$failedJobsTable = [];
if (count($failedJobs) === 0) {
$this->info('No failed jobs found.');
return;
}
foreach ($failedJobs as $failedJob) {
$failedJobsTable[] = [
'id' => $failedJob->id,
'name' => $failedJob->name,
'failed_at' => $failedJob->failed_at ? now()->parse($failedJob->failed_at)->format('Y-m-d H:i:s') : null,
];
}
table($failedJobsTable);
}
if ($action === 'failed-delete') {
$failedJobs = app(JobRepository::class)->getFailed();
$failedJobsTable = [];
foreach ($failedJobs as $failedJob) {
$failedJobsTable[] = [
'id' => $failedJob->id,
'name' => $failedJob->name,
'failed_at' => $failedJob->failed_at ? now()->parse($failedJob->failed_at)->format('Y-m-d H:i:s') : null,
];
}
app(MetricsRepository::class)->clear();
if (count($failedJobsTable) === 0) {
$this->info('No failed jobs found.');
return;
}
$jobIds = multiselect(
label: 'Which job to delete?',
options: collect($failedJobsTable)->mapWithKeys(fn ($job) => [$job['id'] => $job['id'].' - '.$job['name']])->toArray(),
);
foreach ($jobIds as $jobId) {
Artisan::queue('horizon:forget', ['id' => $jobId]);
}
}
if ($action === 'running') {
$redisJobRepository = app(CustomJobRepository::class);
$runningJobs = $redisJobRepository->getReservedJobs();
$runningJobsTable = [];
if (count($runningJobs) === 0) {
$this->info('No running jobs found.');
return;
}
foreach ($runningJobs as $runningJob) {
$runningJobsTable[] = [
'id' => $runningJob->id,
'name' => $runningJob->name,
'reserved_at' => $runningJob->reserved_at ? now()->parse($runningJob->reserved_at)->format('Y-m-d H:i:s') : null,
];
}
table($runningJobsTable);
}
if ($action === 'workers') {
$redisJobRepository = app(CustomJobRepository::class);
$workers = $redisJobRepository->getHorizonWorkers();
$workersTable = [];
foreach ($workers as $worker) {
$workersTable[] = [
'name' => $worker->name,
];
}
table($workersTable);
}
if ($action === 'purge-queues') {
$getQueues = app(CustomJobRepository::class)->getQueues();
$queueName = select(
label: 'Which queue to purge?',
options: $getQueues,
);
$redisJobRepository = app(RedisJobRepository::class);
$redisJobRepository->purge($queueName);
}
}
public function isThereAJobInProgress()
{
$runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get();
$count = $runningJobs->count();
if ($count === 0) {
return false;
}
return true;
}
public function getJobStatus(string $jobId)
{
return getJobStatus($jobId);
}
}

View File

@@ -91,7 +91,13 @@ class Kernel extends ConsoleKernel
private function pullImages(): void
{
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
if (isCloud()) {
$servers = $this->allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
} else {
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
}
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
$this->scheduleInstance->job(function () use ($server) {
@@ -124,7 +130,7 @@ class Kernel extends ConsoleKernel
private function checkResources(): void
{
if (isCloud()) {
$servers = $this->allServers->whereHas('team.subscription')->get();
$servers = $this->allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get();
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
} else {
@@ -133,14 +139,14 @@ class Kernel extends ConsoleKernel
foreach ($servers as $server) {
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
if (validate_timezone($serverTimezone) === false) {
$serverTimezone = config('app.timezone');
}
// Sentinel check
$lastSentinelUpdate = $server->sentinel_updated_at;
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
// Check container status every minute if Sentinel does not activated
if (validate_timezone($serverTimezone) === false) {
$serverTimezone = config('app.timezone');
}
if (isCloud()) {
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
} else {
@@ -148,15 +154,11 @@ class Kernel extends ConsoleKernel
}
// $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer();
// Check storage usage every 10 minutes if Sentinel does not activated
$this->scheduleInstance->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer();
}
if ($server->settings->force_docker_cleanup) {
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
} else {
$this->scheduleInstance->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
$this->scheduleInstance->job(new ServerStorageCheckJob($server))->cron($server->settings->server_disk_usage_check_frequency)->timezone($serverTimezone)->onOneServer();
}
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
// Cleanup multiplexed connections every hour
// $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
@@ -175,25 +177,47 @@ class Kernel extends ConsoleKernel
if ($scheduled_backups->isEmpty()) {
return;
}
$finalScheduledBackups = collect();
foreach ($scheduled_backups as $scheduled_backup) {
if (is_null(data_get($scheduled_backup, 'database'))) {
if (blank(data_get($scheduled_backup, 'database'))) {
$scheduled_backup->delete();
continue;
}
$server = $scheduled_backup->server();
if (blank($server)) {
$scheduled_backup->delete();
continue;
}
if ($server->isFunctional() === false) {
continue;
}
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
continue;
}
$finalScheduledBackups->push($scheduled_backup);
}
foreach ($finalScheduledBackups as $scheduled_backup) {
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
$server = $scheduled_backup->server();
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
if (is_null($server)) {
continue;
if (validate_timezone($serverTimezone) === false) {
$serverTimezone = config('app.timezone');
}
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
$this->scheduleInstance->job(new DatabaseBackupJob(
backup: $scheduled_backup
))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer();
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
}
}
@@ -203,37 +227,55 @@ class Kernel extends ConsoleKernel
if ($scheduled_tasks->isEmpty()) {
return;
}
$finalScheduledTasks = collect();
foreach ($scheduled_tasks as $scheduled_task) {
$service = $scheduled_task->service;
$application = $scheduled_task->application;
if (! $application && ! $service) {
$server = $scheduled_task->server();
if (blank($server)) {
$scheduled_task->delete();
continue;
}
if ($application) {
if (str($application->status)->contains('running') === false) {
continue;
}
}
if ($service) {
if (str($service->status)->contains('running') === false) {
continue;
}
}
$server = $scheduled_task->server();
if (! $server) {
if ($server->isFunctional() === false) {
continue;
}
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
continue;
}
if (! $service && ! $application) {
$scheduled_task->delete();
continue;
}
if ($application && str($application->status)->contains('running') === false) {
continue;
}
if ($service && str($service->status)->contains('running') === false) {
continue;
}
$finalScheduledTasks->push($scheduled_task);
}
foreach ($finalScheduledTasks as $scheduled_task) {
$server = $scheduled_task->server();
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
if (validate_timezone($serverTimezone) === false) {
$serverTimezone = config('app.timezone');
}
$this->scheduleInstance->job(new ScheduledTaskJob(
task: $scheduled_task
))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer();
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Contracts;
use Illuminate\Support\Collection;
use Laravel\Horizon\Contracts\JobRepository;
interface CustomJobRepositoryInterface extends JobRepository
{
/**
* Get all jobs with a specific status.
*/
public function getJobsByStatus(string $status): Collection;
/**
* Get the count of jobs with a specific status.
*/
public function countJobsByStatus(string $status): int;
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Events;
use App\Models\Server;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class RestoreJobFinished
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct($data)
{
$scriptPath = data_get($data, 'scriptPath');
$tmpPath = data_get($data, 'tmpPath');
$container = data_get($data, 'container');
$serverId = data_get($data, 'serverId');
if (filled($scriptPath) && filled($tmpPath) && filled($container) && filled($serverId)) {
if (str($tmpPath)->startsWith('/tmp/')
&& str($scriptPath)->startsWith('/tmp/')
&& ! str($tmpPath)->contains('..')
&& ! str($scriptPath)->contains('..')
&& strlen($tmpPath) > 5 // longer than just "/tmp/"
&& strlen($scriptPath) > 5
) {
$commands[] = "docker exec {$container} sh -c 'rm {$scriptPath}'";
$commands[] = "docker exec {$container} sh -c 'rm {$tmpPath}'";
instant_remote_process($commands, Server::find($serverId), throwError: true);
}
}
}
}

View File

@@ -27,6 +27,9 @@ class ApplicationsController extends Controller
{
$application->makeHidden([
'id',
'resourceable',
'resourceable_id',
'resourceable_type',
]);
if (request()->attributes->get('can_read_sensitive', false) === false) {
$application->makeHidden([
@@ -114,11 +117,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
@@ -185,8 +189,17 @@ class ApplicationsController extends Controller
),
responses: [
new OA\Response(
response: 200,
response: 201,
description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
),
new OA\Response(
response: 401,
@@ -220,11 +233,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'],
'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
@@ -291,8 +305,17 @@ class ApplicationsController extends Controller
),
responses: [
new OA\Response(
response: 200,
response: 201,
description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
),
new OA\Response(
response: 401,
@@ -326,11 +349,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'private_key_uuid' => ['type' => 'string', 'description' => 'The private key UUID.'],
'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
@@ -397,8 +421,17 @@ class ApplicationsController extends Controller
),
responses: [
new OA\Response(
response: 200,
response: 201,
description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
),
new OA\Response(
response: 401,
@@ -432,11 +465,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'dockerfile'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'dockerfile'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
@@ -487,8 +521,17 @@ class ApplicationsController extends Controller
),
responses: [
new OA\Response(
response: 200,
response: 201,
description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
),
new OA\Response(
response: 401,
@@ -522,11 +565,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'docker_registry_image_name', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'docker_registry_image_name', 'ports_exposes'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
@@ -574,8 +618,17 @@ class ApplicationsController extends Controller
),
responses: [
new OA\Response(
response: 200,
response: 201,
description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
),
new OA\Response(
response: 401,
@@ -609,11 +662,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'docker_compose_raw'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID if the server has more than one destinations.'],
'name' => ['type' => 'string', 'description' => 'The application name.'],
@@ -627,8 +681,17 @@ class ApplicationsController extends Controller
),
responses: [
new OA\Response(
response: 200,
response: 201,
description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
),
new OA\Response(
response: 401,
@@ -647,7 +710,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type)
{
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration'];
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -661,7 +724,8 @@ class ApplicationsController extends Controller
'name' => 'string|max:255',
'description' => 'string|nullable',
'project_uuid' => 'string|required',
'environment_name' => 'string|required',
'environment_name' => 'string|nullable',
'environment_uuid' => 'string|nullable',
'server_uuid' => 'string|required',
'destination_uuid' => 'string',
]);
@@ -681,6 +745,11 @@ class ApplicationsController extends Controller
], 422);
}
$environmentUuid = $request->environment_uuid;
$environmentName = $request->environment_name;
if (blank($environmentUuid) && blank($environmentName)) {
return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
}
$serverUuid = $request->server_uuid;
$fqdn = $request->domains;
$instantDeploy = $request->instant_deploy;
@@ -713,7 +782,10 @@ class ApplicationsController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$environment = $project->environments()->where('name', $request->environment_name)->first();
$environment = $project->environments()->where('name', $environmentName)->first();
if (! $environment) {
$environment = $project->environments()->where('uuid', $environmentUuid)->first();
}
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
@@ -730,12 +802,6 @@ class ApplicationsController extends Controller
}
$destination = $destinations->first();
if ($type === 'public') {
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
@@ -745,7 +811,7 @@ class ApplicationsController extends Controller
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
@@ -753,6 +819,12 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(),
], 422);
}
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
@@ -791,7 +863,7 @@ class ApplicationsController extends Controller
$application->settings->save();
}
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
}
@@ -815,14 +887,8 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([
'uuid' => data_get($application, 'uuid'),
'domains' => data_get($application, 'domains'),
]));
]))->setStatusCode(201);
} elseif ($type === 'private-gh-app') {
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
@@ -833,7 +899,7 @@ class ApplicationsController extends Controller
'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
@@ -842,6 +908,14 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(),
], 422);
}
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@@ -884,13 +958,13 @@ class ApplicationsController extends Controller
$application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id;
$application->save();
$application->refresh();
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
}
@@ -914,14 +988,8 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([
'uuid' => data_get($application, 'uuid'),
'domains' => data_get($application, 'domains'),
]));
]))->setStatusCode(201);
} elseif ($type === 'private-deploy-key') {
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validationRules = [
'git_repository' => 'string|required',
@@ -934,7 +1002,7 @@ class ApplicationsController extends Controller
'docker_compose_raw' => 'string|nullable',
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
@@ -943,6 +1011,13 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(),
], 422);
}
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@@ -980,13 +1055,13 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
$application->save();
$application->refresh();
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
}
@@ -1010,16 +1085,12 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([
'uuid' => data_get($application, 'uuid'),
'domains' => data_get($application, 'domains'),
]));
]))->setStatusCode(201);
} elseif ($type === 'dockerfile') {
if (! $request->has('name')) {
$request->offsetSet('name', 'dockerfile-'.new Cuid2);
}
$validationRules = [
'dockerfile' => 'string|required',
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
@@ -1028,6 +1099,10 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(),
], 422);
}
if (! $request->has('name')) {
$request->offsetSet('name', 'dockerfile-'.new Cuid2);
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@@ -1066,16 +1141,16 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
}
@@ -1095,17 +1170,14 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([
'uuid' => data_get($application, 'uuid'),
'domains' => data_get($application, 'domains'),
]));
]))->setStatusCode(201);
} elseif ($type === 'dockerimage') {
if (! $request->has('name')) {
$request->offsetSet('name', 'docker-image-'.new Cuid2);
}
$validationRules = [
'docker_registry_image_name' => 'string|required',
'docker_registry_image_tag' => 'string',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
@@ -1114,6 +1186,9 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(),
], 422);
}
if (! $request->has('name')) {
$request->offsetSet('name', 'docker-image-'.new Cuid2);
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@@ -1130,16 +1205,16 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
}
@@ -1159,9 +1234,9 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([
'uuid' => data_get($application, 'uuid'),
'domains' => data_get($application, 'domains'),
]));
]))->setStatusCode(201);
} elseif ($type === 'dockercompose') {
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw'];
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw'];
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
@@ -1183,7 +1258,7 @@ class ApplicationsController extends Controller
$validationRules = [
'docker_compose_raw' => 'string|required',
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
@@ -1241,7 +1316,7 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([
'uuid' => data_get($service, 'uuid'),
'domains' => data_get($service, 'domains'),
]));
]))->setStatusCode(201);
}
return response()->json(['message' => 'Invalid type.'], 400);
@@ -1551,7 +1626,7 @@ class ApplicationsController extends Controller
'docker_compose_custom_build_command' => 'string|nullable',
'custom_nginx_configuration' => 'string|nullable',
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
// Validate ports_exposes
@@ -1668,7 +1743,10 @@ class ApplicationsController extends Controller
removeUnnecessaryFieldsFromRequest($request);
$data = $request->all();
data_set($data, 'fqdn', $domains);
if ($request->has('domains') && $server->isProxyShouldRun()) {
data_set($data, 'fqdn', $domains);
}
if ($dockerComposeDomainsJson->count() > 0) {
data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson));
}
@@ -1893,8 +1971,9 @@ class ApplicationsController extends Controller
$is_preview = $request->is_preview ?? false;
$is_build_time = $request->is_build_time ?? false;
$is_literal = $request->is_literal ?? false;
$key = str($request->key)->trim()->replace(' ', '_')->value;
if ($is_preview) {
$env = $application->environment_variables_preview->where('key', $request->key)->first();
$env = $application->environment_variables_preview->where('key', $key)->first();
if ($env) {
$env->value = $request->value;
if ($env->is_build_time != $is_build_time) {
@@ -1921,7 +2000,7 @@ class ApplicationsController extends Controller
], 404);
}
} else {
$env = $application->environment_variables->where('key', $request->key)->first();
$env = $application->environment_variables->where('key', $key)->first();
if ($env) {
$env->value = $request->value;
if ($env->is_build_time != $is_build_time) {
@@ -2064,6 +2143,7 @@ class ApplicationsController extends Controller
$bulk_data = collect($bulk_data)->map(function ($item) {
return collect($item)->only(['key', 'value', 'is_preview', 'is_build_time', 'is_literal']);
});
$returnedEnvs = collect();
foreach ($bulk_data as $item) {
$validator = customApiValidator($item, [
'key' => 'string|required',
@@ -2085,8 +2165,9 @@ class ApplicationsController extends Controller
$is_literal = $item->get('is_literal') ?? false;
$is_multi_line = $item->get('is_multiline') ?? false;
$is_shown_once = $item->get('is_shown_once') ?? false;
$key = str($item->get('key'))->trim()->replace(' ', '_')->value;
if ($is_preview) {
$env = $application->environment_variables_preview->where('key', $item->get('key'))->first();
$env = $application->environment_variables_preview->where('key', $key)->first();
if ($env) {
$env->value = $item->get('value');
if ($env->is_build_time != $is_build_time) {
@@ -2111,10 +2192,12 @@ class ApplicationsController extends Controller
'is_literal' => $is_literal,
'is_multiline' => $is_multi_line,
'is_shown_once' => $is_shown_once,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
}
} else {
$env = $application->environment_variables->where('key', $item->get('key'))->first();
$env = $application->environment_variables->where('key', $key)->first();
if ($env) {
$env->value = $item->get('value');
if ($env->is_build_time != $is_build_time) {
@@ -2139,12 +2222,15 @@ class ApplicationsController extends Controller
'is_literal' => $is_literal,
'is_multiline' => $is_multi_line,
'is_shown_once' => $is_shown_once,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
}
}
$returnedEnvs->push($this->removeSensitiveData($env));
}
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
return response()->json($returnedEnvs)->setStatusCode(201);
}
#[OA\Post(
@@ -2257,8 +2343,10 @@ class ApplicationsController extends Controller
], 422);
}
$is_preview = $request->is_preview ?? false;
$key = str($request->key)->trim()->replace(' ', '_')->value;
if ($is_preview) {
$env = $application->environment_variables_preview->where('key', $request->key)->first();
$env = $application->environment_variables_preview->where('key', $key)->first();
if ($env) {
return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.',
@@ -2272,6 +2360,8 @@ class ApplicationsController extends Controller
'is_literal' => $request->is_literal ?? false,
'is_multiline' => $request->is_multiline ?? false,
'is_shown_once' => $request->is_shown_once ?? false,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
return response()->json([
@@ -2279,7 +2369,7 @@ class ApplicationsController extends Controller
])->setStatusCode(201);
}
} else {
$env = $application->environment_variables->where('key', $request->key)->first();
$env = $application->environment_variables->where('key', $key)->first();
if ($env) {
return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.',
@@ -2293,6 +2383,8 @@ class ApplicationsController extends Controller
'is_literal' => $request->is_literal ?? false,
'is_multiline' => $request->is_multiline ?? false,
'is_shown_once' => $request->is_shown_once ?? false,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
return response()->json([
@@ -2380,7 +2472,10 @@ class ApplicationsController extends Controller
'message' => 'Application not found.',
], 404);
}
$found_env = EnvironmentVariable::where('uuid', $request->env_uuid)->where('application_id', $application->id)->first();
$found_env = EnvironmentVariable::where('uuid', $request->env_uuid)
->where('resourceable_type', Application::class)
->where('resourceable_id', $application->id)
->first();
if (! $found_env) {
return response()->json([
'message' => 'Environment variable not found.',

View File

@@ -523,11 +523,12 @@ class DatabasesController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'],
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'postgres_user' => ['type' => 'string', 'description' => 'PostgreSQL user'],
'postgres_password' => ['type' => 'string', 'description' => 'PostgreSQL password'],
'postgres_db' => ['type' => 'string', 'description' => 'PostgreSQL database'],
@@ -589,11 +590,12 @@ class DatabasesController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'],
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'clickhouse_admin_user' => ['type' => 'string', 'description' => 'Clickhouse admin user'],
'clickhouse_admin_password' => ['type' => 'string', 'description' => 'Clickhouse admin password'],
@@ -651,11 +653,12 @@ class DatabasesController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'],
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'dragonfly_password' => ['type' => 'string', 'description' => 'DragonFly password'],
'name' => ['type' => 'string', 'description' => 'Name of the database'],
@@ -712,11 +715,12 @@ class DatabasesController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'],
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'redis_password' => ['type' => 'string', 'description' => 'Redis password'],
'redis_conf' => ['type' => 'string', 'description' => 'Redis conf'],
@@ -774,11 +778,12 @@ class DatabasesController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'],
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'keydb_password' => ['type' => 'string', 'description' => 'KeyDB password'],
'keydb_conf' => ['type' => 'string', 'description' => 'KeyDB conf'],
@@ -836,11 +841,12 @@ class DatabasesController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'],
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mariadb_conf' => ['type' => 'string', 'description' => 'MariaDB conf'],
'mariadb_root_password' => ['type' => 'string', 'description' => 'MariaDB root password'],
@@ -901,11 +907,12 @@ class DatabasesController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'],
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
@@ -966,11 +973,12 @@ class DatabasesController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'],
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mongo_conf' => ['type' => 'string', 'description' => 'MongoDB conf'],
'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'MongoDB initdb root username'],
@@ -1013,7 +1021,7 @@ class DatabasesController extends Controller
public function create_database(Request $request, NewDatabaseTypes $type)
{
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -1039,6 +1047,11 @@ class DatabasesController extends Controller
'errors' => $errors,
], 422);
}
$environmentUuid = $request->environment_uuid;
$environmentName = $request->environment_name;
if (blank($environmentUuid) && blank($environmentName)) {
return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
}
$serverUuid = $request->server_uuid;
$instantDeploy = $request->instant_deploy ?? false;
if ($request->is_public && ! $request->public_port) {
@@ -1048,9 +1061,12 @@ class DatabasesController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$environment = $project->environments()->where('name', $request->environment_name)->first();
$environment = $project->environments()->where('name', $environmentName)->first();
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
$environment = $project->environments()->where('uuid', $environmentUuid)->first();
}
if (! $environment) {
return response()->json(['message' => 'You need to provide a valid environment_name or environment_uuid.'], 422);
}
$server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
if (! $server) {
@@ -1074,7 +1090,8 @@ class DatabasesController extends Controller
'description' => 'string|nullable',
'image' => 'string',
'project_uuid' => 'string|required',
'environment_name' => 'string|required',
'environment_name' => 'string|nullable',
'environment_uuid' => 'string|nullable',
'server_uuid' => 'string|required',
'destination_uuid' => 'string',
'is_public' => 'boolean',
@@ -1105,7 +1122,7 @@ class DatabasesController extends Controller
}
}
if ($type === NewDatabaseTypes::POSTGRESQL) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
$validator = customApiValidator($request->all(), [
'postgres_user' => 'string',
'postgres_password' => 'string',
@@ -1164,7 +1181,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MARIADB) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
$validator = customApiValidator($request->all(), [
'clickhouse_admin_user' => 'string',
'clickhouse_admin_password' => 'string',
@@ -1220,7 +1237,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MYSQL) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
'mysql_password' => 'string',
@@ -1279,7 +1296,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::REDIS) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
$validator = customApiValidator($request->all(), [
'redis_password' => 'string',
'redis_conf' => 'string',
@@ -1335,7 +1352,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::DRAGONFLY) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
$validator = customApiValidator($request->all(), [
'dragonfly_password' => 'string',
]);
@@ -1365,7 +1382,7 @@ class DatabasesController extends Controller
'uuid' => $database->uuid,
]))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::KEYDB) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
$validator = customApiValidator($request->all(), [
'keydb_password' => 'string',
'keydb_conf' => 'string',
@@ -1421,7 +1438,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::CLICKHOUSE) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
$validator = customApiValidator($request->all(), [
'clickhouse_admin_user' => 'string',
'clickhouse_admin_password' => 'string',
@@ -1457,7 +1474,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MONGODB) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',

View File

@@ -90,11 +90,13 @@ class ProjectController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
$project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
$project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$project->load(['environments']);
return response()->json(
serializeApiResponse($project),
);
@@ -102,16 +104,16 @@ class ProjectController extends Controller
#[OA\Get(
summary: 'Environment',
description: 'Get environment by name.',
path: '/projects/{uuid}/{environment_name}',
operationId: 'get-environment-by-name',
description: 'Get environment by name or UUID.',
path: '/projects/{uuid}/{environment_name_or_uuid}',
operationId: 'get-environment-by-name-or-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'environment_name', in: 'path', required: true, description: 'Environment name', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'environment_name_or_uuid', in: 'path', required: true, description: 'Environment name or UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -141,14 +143,17 @@ class ProjectController extends Controller
if (! $request->uuid) {
return response()->json(['message' => 'UUID is required.'], 422);
}
if (! $request->environment_name) {
return response()->json(['message' => 'Environment name is required.'], 422);
if (! $request->environment_name_or_uuid) {
return response()->json(['message' => 'Environment name or UUID is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$environment = $project->environments()->whereName($request->environment_name)->first();
$environment = $project->environments()->whereName($request->environment_name_or_uuid)->first();
if (! $environment) {
$environment = $project->environments()->whereUuid($request->environment_name_or_uuid)->first();
}
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}

View File

@@ -20,6 +20,9 @@ class ServicesController extends Controller
{
$service->makeHidden([
'id',
'resourceable',
'resourceable_id',
'resourceable_type',
]);
if (request()->attributes->get('can_read_sensitive', false) === false) {
$service->makeHidden([
@@ -99,7 +102,7 @@ class ServicesController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name', 'type'],
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'type'],
properties: [
'type' => [
'description' => 'The one-click service type',
@@ -196,7 +199,8 @@ class ServicesController extends Controller
'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'],
'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'],
'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'Environment name.'],
'environment_name' => ['type' => 'string', 'description' => 'Environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'Environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
@@ -233,7 +237,7 @@ class ServicesController extends Controller
)]
public function create_service(Request $request)
{
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy'];
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -247,7 +251,8 @@ class ServicesController extends Controller
$validator = customApiValidator($request->all(), [
'type' => 'string|required',
'project_uuid' => 'string|required',
'environment_name' => 'string|required',
'environment_name' => 'string|nullable',
'environment_uuid' => 'string|nullable',
'server_uuid' => 'string|required',
'destination_uuid' => 'string',
'name' => 'string|max:255',
@@ -269,6 +274,11 @@ class ServicesController extends Controller
'errors' => $errors,
], 422);
}
$environmentUuid = $request->environment_uuid;
$environmentName = $request->environment_name;
if (blank($environmentUuid) && blank($environmentName)) {
return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
}
$serverUuid = $request->server_uuid;
$instantDeploy = $request->instant_deploy ?? false;
if ($request->is_public && ! $request->public_port) {
@@ -278,7 +288,10 @@ class ServicesController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$environment = $project->environments()->where('name', $request->environment_name)->first();
$environment = $project->environments()->where('name', $environmentName)->first();
if (! $environment) {
$environment = $project->environments()->where('uuid', $environmentUuid)->first();
}
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
@@ -333,7 +346,8 @@ class ServicesController extends Controller
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'service_id' => $service->id,
'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
'is_build_time' => false,
'is_preview' => false,
]);
@@ -345,7 +359,11 @@ class ServicesController extends Controller
}
$domains = $service->applications()->get()->pluck('fqdn')->sort();
$domains = $domains->map(function ($domain) {
return str($domain)->beforeLast(':')->value();
if (count(explode(':', $domain)) > 2) {
return str($domain)->beforeLast(':')->value();
}
return $domain;
});
return response()->json([
@@ -673,7 +691,8 @@ class ServicesController extends Controller
], 422);
}
$env = $service->environment_variables()->where('key', $request->key)->first();
$key = str($request->key)->trim()->replace(' ', '_')->value;
$env = $service->environment_variables()->where('key', $key)->first();
if (! $env) {
return response()->json(['message' => 'Environment variable not found.'], 404);
}
@@ -799,9 +818,9 @@ class ServicesController extends Controller
'errors' => $validator->errors(),
], 422);
}
$key = str($item['key'])->trim()->replace(' ', '_')->value;
$env = $service->environment_variables()->updateOrCreate(
['key' => $item['key']],
['key' => $key],
$item
);
@@ -909,7 +928,8 @@ class ServicesController extends Controller
], 422);
}
$existingEnv = $service->environment_variables()->where('key', $request->key)->first();
$key = str($request->key)->trim()->replace(' ', '_')->value;
$existingEnv = $service->environment_variables()->where('key', $key)->first();
if ($existingEnv) {
return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.',
@@ -995,7 +1015,8 @@ class ServicesController extends Controller
}
$env = EnvironmentVariable::where('uuid', $request->env_uuid)
->where('service_id', $service->id)
->where('resourceable_type', Service::class)
->where('resourceable_id', $service->id)
->first();
if (! $env) {

View File

@@ -39,12 +39,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 3600;
public static int $batch_counter = 0;
private int $application_deployment_queue_id;
private bool $newVersionIsHealthy = false;
private ApplicationDeploymentQueue $application_deployment_queue;
@@ -126,6 +126,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $nixpacks_plan = null;
private Collection $nixpacks_plan_json;
private ?string $nixpacks_type = null;
private string $dockerfile_location = '/Dockerfile';
@@ -164,18 +166,23 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private bool $preserveRepository = false;
public $tries = 1;
public function tags()
{
// Do not remove this one, it needs to properly identify which worker is running the job
return ['App\Models\ApplicationDeploymentQueue:'.$this->application_deployment_queue_id];
}
public function __construct(int $application_deployment_queue_id)
public function __construct(public int $application_deployment_queue_id)
{
$this->onQueue('high');
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->application_deployment_queue = ApplicationDeploymentQueue::find($this->application_deployment_queue_id);
$this->nixpacks_plan_json = collect([]);
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
$this->build_args = collect([]);
$this->application_deployment_queue_id = $application_deployment_queue_id;
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit;
@@ -233,15 +240,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
public function tags(): array
{
return ['server:'.gethostname()];
}
public function handle(): void
{
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
'horizon_job_worker' => gethostname(),
]);
if ($this->server->isFunctional() === false) {
$this->application_deployment_queue->addLogEntry('Server is not functional.');
@@ -1405,7 +1408,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'project_uuid' => data_get($this->application, 'environment.project.uuid'),
'application_uuid' => data_get($this->application, 'uuid'),
'deployment_uuid' => $deployment_uuid,
'environment_name' => data_get($this->application, 'environment.name'),
'environment_uuid' => data_get($this->application, 'environment.uuid'),
]));
}
}
@@ -1545,7 +1548,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Do any modifications here
$this->generate_env_variables();
$merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', [])));
$merged_envs = collect(data_get($parsed, 'variables', []))->merge($this->env_args);
$aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []);
if (count($aptPkgs) === 0) {
$aptPkgs = ['curl', 'wget'];
@@ -1570,6 +1573,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->elixir_finetunes();
}
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
$this->nixpacks_plan_json = collect($parsed);
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
if ($this->nixpacks_type === 'rust') {
// temporary: disable healthcheck for rust because the start phase does not have curl/wget
@@ -1678,7 +1682,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application->custom_labels = base64_encode($labels->implode("\n"));
$this->application->save();
} else {
if (! $this->application->settings->is_container_label_readonly_enabled) {
if ($this->application->settings->is_container_label_readonly_enabled) {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
}
}
@@ -1690,7 +1694,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return escapeDollarSign($value);
});
}
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->application->project()->name, $this->application->name, $this->application->environment->name, $this->pull_request_id))->toArray();
// Check for custom HEALTHCHECK
if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) {
@@ -2278,18 +2282,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function generate_build_env_variables()
{
$this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]);
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
$value = escapeshellarg($env->real_value);
$this->build_args->push("--build-arg {$env->key}={$value}");
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
$value = escapeshellarg($env->real_value);
$this->build_args->push("--build-arg {$env->key}={$value}");
}
}
$variables = collect($this->nixpacks_plan_json->get('variables'));
$this->build_args = $variables->map(function ($value, $key) {
return "--build-arg {$key}={$value}";
});
}
private function add_build_env_variables_to_dockerfile()
@@ -2394,7 +2390,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
queue_next_deployment($this->application);
// If the deployment is cancelled by the user, don't update the status
if (
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value &&
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value
) {
$this->application_deployment_queue->update([
'status' => $status,

View File

@@ -24,7 +24,7 @@ class CheckAndStartSentinelJob implements ShouldBeEncrypted, ShouldQueue
$latestVersion = get_latest_sentinel_version();
// Check if sentinel is running
$sentinelFound = instant_remote_process(['docker inspect coolify-sentinel'], $this->server, false);
$sentinelFound = instant_remote_process_with_timeout(['docker inspect coolify-sentinel'], $this->server, false, 10);
$sentinelFoundJson = json_decode($sentinelFound, true);
$sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited');
if ($sentinelStatus !== 'running') {
@@ -33,7 +33,7 @@ class CheckAndStartSentinelJob implements ShouldBeEncrypted, ShouldQueue
return;
}
// If sentinel is running, check if it needs an update
$runningVersion = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
$runningVersion = instant_remote_process_with_timeout(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
if (empty($runningVersion)) {
$runningVersion = '0.0.0';
}

View File

@@ -20,11 +20,11 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
public function handle(): void
{
try {
$containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
$containerIds = collect(json_decode($containers))->pluck('ID');
if ($containerIds->count() > 0) {
foreach ($containerIds as $containerId) {
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
instant_remote_process_with_timeout(['docker container rm -f '.$containerId], $this->server, false);
}
}
} catch (\Throwable $e) {

View File

@@ -32,8 +32,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public Server $server;
public ScheduledDatabaseBackup $backup;
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database;
public ?string $container_name = null;
@@ -58,10 +56,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public ?S3Storage $s3 = null;
public function __construct($backup)
public function __construct(public ScheduledDatabaseBackup $backup)
{
$this->onQueue('high');
$this->backup = $backup;
}
public function handle(): void
@@ -302,7 +299,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
throw new \Exception('Unsupported database type');
}
$size = $this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
@@ -326,6 +322,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
}
}
if ($this->backup_log && $this->backup_log->status === 'success') {
removeOldBackups($this->backup);
}
} catch (\Throwable $e) {
throw $e;
} finally {
@@ -460,19 +459,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false);
}
private function remove_old_backups(): void
{
if ($this->backup->number_of_backups_locally === 0) {
$deletable = $this->backup->executions()->where('status', 'success');
} else {
$deletable = $this->backup->executions()->where('status', 'success')->skip($this->backup->number_of_backups_locally - 1);
}
foreach ($deletable->get() as $execution) {
delete_backup_locally($execution->filename, $this->server);
$execution->delete();
}
}
private function upload_to_s3(): void
{
try {

View File

@@ -19,6 +19,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
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;
@@ -68,6 +69,11 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
public bool $foundLogDrainContainer = false;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function backoff(): int
{
return isDev() ? 1 : 3;

View File

@@ -44,7 +44,7 @@ class SendMessageToPushoverJob implements ShouldBeEncrypted, ShouldQueue
{
$response = Http::post('https://api.pushover.net/1/messages.json', $this->message->toPayload($this->token, $this->user));
if ($response->failed()) {
throw new \RuntimeException('Pushover notification failed with ' . $response->status() . ' status code.' . $response->body());
throw new \RuntimeException('Pushover notification failed with '.$response->status().' status code.'.$response->body());
}
}
}

104
app/Jobs/VolumeCloneJob.php Normal file
View File

@@ -0,0 +1,104 @@
<?php
namespace App\Jobs;
use App\Models\LocalPersistentVolume;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class VolumeCloneJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected string $cloneDir = '/data/coolify/clone';
public function __construct(
protected string $sourceVolume,
protected string $targetVolume,
protected Server $sourceServer,
protected ?Server $targetServer,
protected LocalPersistentVolume $persistentVolume
) {
$this->onQueue('high');
}
public function handle()
{
try {
if (! $this->targetServer || $this->targetServer->id === $this->sourceServer->id) {
$this->cloneLocalVolume();
} else {
$this->cloneRemoteVolume();
}
} catch (\Exception $e) {
\Log::error("Failed to copy volume data for {$this->sourceVolume}: ".$e->getMessage());
throw $e;
}
}
protected function cloneLocalVolume()
{
instant_remote_process([
"docker volume create $this->targetVolume",
"docker run --rm -v $this->sourceVolume:/source -v $this->targetVolume:/target alpine sh -c 'cp -a /source/. /target/ && chown -R 1000:1000 /target'",
], $this->sourceServer);
}
protected function cloneRemoteVolume()
{
$sourceCloneDir = "{$this->cloneDir}/{$this->sourceVolume}";
$targetCloneDir = "{$this->cloneDir}/{$this->targetVolume}";
try {
instant_remote_process([
"mkdir -p $sourceCloneDir",
"chmod 777 $sourceCloneDir",
"docker run --rm -v $this->sourceVolume:/source -v $sourceCloneDir:/clone alpine sh -c 'cd /source && tar czf /clone/volume-data.tar.gz .'",
], $this->sourceServer);
instant_remote_process([
"mkdir -p $targetCloneDir",
"chmod 777 $targetCloneDir",
], $this->targetServer);
instant_scp(
"$sourceCloneDir/volume-data.tar.gz",
"$targetCloneDir/volume-data.tar.gz",
$this->sourceServer,
$this->targetServer
);
instant_remote_process([
"docker volume create $this->targetVolume",
"docker run --rm -v $this->targetVolume:/target -v $targetCloneDir:/clone alpine sh -c 'cd /target && tar xzf /clone/volume-data.tar.gz && chown -R 1000:1000 /target'",
], $this->targetServer);
} catch (\Exception $e) {
\Log::error("Failed to clone volume {$this->sourceVolume} to {$this->targetVolume}: ".$e->getMessage());
throw $e;
} finally {
try {
instant_remote_process([
"rm -rf $sourceCloneDir",
], $this->sourceServer, false);
} catch (\Exception $e) {
\Log::warning('Failed to clean up source server clone directory: '.$e->getMessage());
}
try {
if ($this->targetServer) {
instant_remote_process([
"rm -rf $targetCloneDir",
], $this->targetServer, false);
}
} catch (\Exception $e) {
\Log::warning('Failed to clean up target server clone directory: '.$e->getMessage());
}
}
}
}

View File

@@ -42,14 +42,8 @@ class ActivityMonitor extends Component
public function polling()
{
$this->hydrateActivity();
// $this->setStatus(ProcessStatus::IN_PROGRESS);
$exit_code = data_get($this->activity, 'properties.exitCode');
if ($exit_code !== null) {
// if ($exit_code === 0) {
// // $this->setStatus(ProcessStatus::FINISHED);
// } else {
// // $this->setStatus(ProcessStatus::ERROR);
// }
$this->isPollingActive = false;
if ($exit_code === 0) {
if ($this->eventToDispatch !== null) {
@@ -70,12 +64,4 @@ class ActivityMonitor extends Component
}
}
}
// protected function setStatus($status)
// {
// $this->activity->properties = $this->activity->properties->merge([
// 'status' => $status,
// ]);
// $this->activity->save();
// }
}

View File

@@ -21,16 +21,28 @@ class Index extends Component
public function mount()
{
if (! isCloud()) {
if (! isCloud() && ! isDev()) {
return redirect()->route('dashboard');
}
if (Auth::id() !== 0) {
if (Auth::id() !== 0 && ! session('impersonating')) {
return redirect()->route('dashboard');
}
$this->getSubscribers();
}
public function back()
{
if (session('impersonating')) {
session()->forget('impersonating');
$user = User::find(0);
$team_to_switch_to = $user->teams->first();
Auth::login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
}
}
public function submitSearch()
{
if ($this->search !== '') {
@@ -52,9 +64,10 @@ class Index extends Component
if (Auth::id() !== 0) {
return redirect()->route('dashboard');
}
session(['impersonating' => true]);
$user = User::find($user_id);
$team_to_switch_to = $user->teams->first();
Cache::forget("team:{$user->id}");
// Cache::forget("team:{$user->id}");
Auth::login($user);
refreshSession($team_to_switch_to);

View File

@@ -9,6 +9,7 @@ use App\Models\Server;
use App\Models\Team;
use Illuminate\Support\Collection;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Index extends Component
{
@@ -334,6 +335,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdProject = Project::create([
'name' => 'My first project',
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
]);
$this->currentState = 'create-resource';
}
@@ -346,7 +348,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'project.resource.create',
[
'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production',
'environment_uuid' => $this->createdProject->environments->first()->uuid,
'server' => $this->createdServer->id,
]
);

View File

@@ -8,6 +8,7 @@ use App\Models\Project;
use App\Models\Server;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Redirect;
use Livewire\Component;
class Dashboard extends Component
@@ -49,6 +50,20 @@ class Dashboard extends Component
])->sortBy('id')->groupBy('server_name')->toArray();
}
public function navigateToProject($projectUuid)
{
$project = Project::where('uuid', $projectUuid)->first();
if ($project && $project->environments->count() === 1) {
return Redirect::route('project.resource.index', [
'project_uuid' => $projectUuid,
'environment_uuid' => $project->environments->first()->uuid,
]);
}
return Redirect::route('project.show', ['project_uuid' => $projectUuid]);
}
public function render()
{
return view('livewire.dashboard');

View File

@@ -83,9 +83,7 @@ class Docker extends Component
]);
}
}
$connectProxyToDockerNetworks = connectProxyToNetworks($this->selectedServer);
instant_remote_process($connectProxyToDockerNetworks, $this->selectedServer, false);
$this->dispatch('reloadWindow');
$this->redirect(route('destination.show', $docker->uuid));
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -5,6 +5,7 @@ namespace App\Livewire\Project;
use App\Models\Project;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class AddEmpty extends Component
{
@@ -22,6 +23,7 @@ class AddEmpty extends Component
'name' => $this->name,
'description' => $this->description,
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
]);
return redirect()->route('project.show', $project->uuid);

View File

@@ -3,43 +3,42 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
use App\Models\Server;
use Livewire\Component;
class Configuration extends Component
{
public $currentRoute;
public Application $application;
public $project;
public $environment;
public $servers;
protected $listeners = ['buildPackUpdated' => '$refresh'];
public function mount()
{
$this->currentRoute = request()->route()->getName();
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
->where('uuid', request()->route('project_uuid'))
->firstOrFail();
$environment = $project->environments()
->select('id', 'name', 'project_id')
->where('name', request()->route('environment_name'))
->select('id', 'uuid', 'name', 'project_id')
->where('uuid', request()->route('environment_uuid'))
->firstOrFail();
$application = $environment->applications()
->with(['destination'])
->where('uuid', request()->route('application_uuid'))
->firstOrFail();
$this->project = $project;
$this->environment = $environment;
$this->application = $application;
if ($application->destination && $application->destination->server) {
$mainServer = $application->destination->server;
$this->servers = Server::ownedByCurrentTeam()
->select('id', 'name')
->where('id', '!=', $mainServer->id)
->get();
} else {
$this->servers = collect();
}
}
public function render()

View File

@@ -34,7 +34,7 @@ class Index extends Component
if (! $project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
$environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']);
if (! $environment) {
return redirect()->route('dashboard');
}

View File

@@ -14,6 +14,8 @@ class Show extends Component
public string $deployment_uuid;
public string $horizon_job_status;
public $isKeepAliveOn = true;
protected $listeners = ['refreshQueue'];
@@ -26,7 +28,7 @@ class Show extends Component
if (! $project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
$environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']);
if (! $environment) {
return redirect()->route('dashboard');
}
@@ -34,25 +36,19 @@ class Show extends Component
if (! $application) {
return redirect()->route('dashboard');
}
// $activity = Activity::where('properties->type_uuid', '=', $deploymentUuid)->first();
// if (!$activity) {
// return redirect()->route('project.application.deployment.index', [
// 'project_uuid' => $project->uuid,
// 'environment_name' => $environment->name,
// 'application_uuid' => $application->uuid,
// ]);
// }
$application_deployment_queue = ApplicationDeploymentQueue::where('deployment_uuid', $deploymentUuid)->first();
if (! $application_deployment_queue) {
return redirect()->route('project.application.deployment.index', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'application_uuid' => $application->uuid,
]);
}
$this->application = $application;
$this->application_deployment_queue = $application_deployment_queue;
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
$this->deployment_uuid = $deploymentUuid;
$this->isKeepAliveOn();
}
public function refreshQueue()
@@ -60,13 +56,21 @@ class Show extends Component
$this->application_deployment_queue->refresh();
}
private function isKeepAliveOn()
{
if (data_get($this->application_deployment_queue, 'status') === 'finished' || data_get($this->application_deployment_queue, 'status') === 'failed') {
$this->isKeepAliveOn = false;
} else {
$this->isKeepAliveOn = true;
}
}
public function polling()
{
$this->dispatch('deploymentFinished');
$this->application_deployment_queue->refresh();
if (data_get($this->application_deployment_queue, 'status') === 'finished' || data_get($this->application_deployment_queue, 'status') === 'failed') {
$this->isKeepAliveOn = false;
}
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
$this->isKeepAliveOn();
}
public function getLogLinesProperty()

View File

@@ -23,7 +23,7 @@ class DeploymentNavbar extends Component
public function mount()
{
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->application = Application::ownedByCurrentTeam()->find($this->application_deployment_queue->application_id);
$this->server = $this->application->destination->server;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
}

View File

@@ -153,7 +153,7 @@ class General extends Component
$this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->customLabels = $this->application->parseContainerLabels();
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && $this->application->settings->is_container_label_readonly_enabled === true) {
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
@@ -327,7 +327,7 @@ class General extends Component
}
}
public function set_redirect()
public function setRedirect()
{
try {
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
@@ -360,10 +360,10 @@ class General extends Component
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
$this->resetDefaultLabels();
// $this->resetDefaultLabels();
if ($this->application->isDirty('redirect')) {
$this->set_redirect();
$this->setRedirect();
}
$this->checkFqdns();

View File

@@ -38,7 +38,7 @@ class Heading extends Component
{
$this->parameters = [
'project_uuid' => $this->application->project()->uuid,
'environment_name' => $this->application->environment->name,
'environment_uuid' => $this->application->environment->uuid,
'application_uuid' => $this->application->uuid,
];
$lastDeployment = $this->application->get_last_successful_deployment();
@@ -94,7 +94,7 @@ class Heading extends Component
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
'environment_uuid' => $this->parameters['environment_uuid'],
]);
}
@@ -136,7 +136,7 @@ class Heading extends Component
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
'environment_uuid' => $this->parameters['environment_uuid'],
]);
}

View File

@@ -171,7 +171,7 @@ class Previews extends Component
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deployment_uuid,
'environment_name' => $this->parameters['environment_name'],
'environment_uuid' => $this->parameters['environment_uuid'],
]);
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -37,7 +37,7 @@ class Rollback extends Component
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $deployment_uuid,
'environment_name' => $this->parameters['environment_name'],
'environment_uuid' => $this->parameters['environment_uuid'],
]);
}

View File

@@ -2,6 +2,12 @@
namespace App\Livewire\Project;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StartDatabase;
use App\Actions\Database\StopDatabase;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Jobs\VolumeCloneJob;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
@@ -12,7 +18,7 @@ class CloneMe extends Component
{
public string $project_uuid;
public string $environment_name;
public string $environment_uuid;
public int $project_id;
@@ -34,6 +40,8 @@ class CloneMe extends Component
public string $newName = '';
public bool $cloneVolumeData = false;
protected $messages = [
'selectedServer' => 'Please select a server.',
'selectedDestination' => 'Please select a server & destination.',
@@ -44,12 +52,17 @@ class CloneMe extends Component
{
$this->project_uuid = $project_uuid;
$this->project = Project::where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments->where('name', $this->environment_name)->first();
$this->environment = $this->project->environments->where('uuid', $this->environment_uuid)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()->servers;
$this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug();
}
public function toggleVolumeCloning(bool $value)
{
$this->cloneVolumeData = $value;
}
public function render()
{
return view('livewire.project.clone-me');
@@ -89,6 +102,7 @@ class CloneMe extends Component
if ($this->environment->name !== 'production') {
$project->environments()->create([
'name' => $this->environment->name,
'uuid' => (string) new Cuid2,
]);
}
$environment = $project->environments->where('name', $this->environment->name)->first();
@@ -100,41 +114,160 @@ class CloneMe extends Component
$project = $this->project;
$environment = $this->project->environments()->create([
'name' => $this->newName,
'uuid' => (string) new Cuid2,
]);
}
$applications = $this->environment->applications;
$databases = $this->environment->databases();
$services = $this->environment->services;
foreach ($applications as $application) {
$applicationSettings = $application->settings;
$uuid = (string) new Cuid2;
$newApplication = $application->replicate()->fill([
$url = $application->fqdn;
if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
$url = generateFqdn($this->server, $uuid);
}
$newApplication = $application->replicate([
'id',
'created_at',
'updated_at',
'additional_servers_count',
'additional_networks_count',
])->fill([
'uuid' => $uuid,
'fqdn' => generateFqdn($this->server, $uuid),
'fqdn' => $url,
'status' => 'exited',
'environment_id' => $environment->id,
// This is not correct, but we need to set it to something
'destination_id' => $this->selectedDestination,
]);
$newApplication->save();
$environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([
if ($newApplication->destination->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
$customLabels = str(implode('|coolify|', generateLabelsApplication($newApplication)))->replace('|coolify|', "\n");
$newApplication->custom_labels = base64_encode($customLabels);
$newApplication->save();
}
$newApplication->settings()->delete();
if ($applicationSettings) {
$newApplicationSettings = $applicationSettings->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'application_id' => $newApplication->id,
]);
$newEnvironmentVariable->save();
$newApplicationSettings->save();
}
$tags = $application->tags;
foreach ($tags as $tag) {
$newApplication->tags()->attach($tag->id);
}
$scheduledTasks = $application->scheduled_tasks()->get();
foreach ($scheduledTasks as $task) {
$newTask = $task->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => (string) new Cuid2,
'application_id' => $newApplication->id,
'team_id' => currentTeam()->id,
]);
$newTask->save();
}
$applicationPreviews = $application->previews()->get();
foreach ($applicationPreviews as $preview) {
$newPreview = $preview->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'application_id' => $newApplication->id,
'status' => 'exited',
]);
$newPreview->save();
}
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newPersistentVolume = $volume->replicate()->fill([
'name' => $newApplication->uuid.'-'.str($volume->name)->afterLast('-'),
$newName = '';
if (str_starts_with($volume->name, $application->uuid)) {
$newName = str($volume->name)->replace($application->uuid, $newApplication->uuid);
} else {
$newName = $newApplication->uuid.'-'.$volume->name;
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $newApplication->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopApplication::dispatch($application, false, false);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $application->destination->server;
$targetServer = $newApplication->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
queue_application_deployment(
deployment_uuid: (string) new Cuid2,
application: $application,
server: $sourceServer,
destination: $application->destination,
no_questions_asked: true
);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
$fileStorages = $application->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $newApplication->id,
]);
$newStorage->save();
}
$environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resourceable_id' => $newApplication->id,
]);
$newEnvironmentVariable->save();
}
}
foreach ($databases as $database) {
$uuid = (string) new Cuid2;
$newDatabase = $database->replicate()->fill([
$newDatabase = $database->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'status' => 'exited',
'started_at' => null,
@@ -142,51 +275,294 @@ class CloneMe extends Component
'destination_id' => $this->selectedDestination,
]);
$newDatabase->save();
$tags = $database->tags;
foreach ($tags as $tag) {
$newDatabase->tags()->attach($tag->id);
}
$newDatabase->persistentStorages()->delete();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$originalName = $volume->name;
$newName = '';
if (str_starts_with($originalName, 'postgres-data-')) {
$newName = 'postgres-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'mysql-data-')) {
$newName = 'mysql-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'redis-data-')) {
$newName = 'redis-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'clickhouse-data-')) {
$newName = 'clickhouse-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'mariadb-data-')) {
$newName = 'mariadb-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'mongodb-data-')) {
$newName = 'mongodb-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'keydb-data-')) {
$newName = 'keydb-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'dragonfly-data-')) {
$newName = 'dragonfly-data-'.$newDatabase->uuid;
} else {
if (str_starts_with($volume->name, $database->uuid)) {
$newName = str($volume->name)->replace($database->uuid, $newDatabase->uuid);
} else {
$newName = $newDatabase->uuid.'-'.$volume->name;
}
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $newDatabase->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopDatabase::dispatch($database);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $database->destination->server;
$targetServer = $newDatabase->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartDatabase::dispatch($database);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
$fileStorages = $database->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $newDatabase->id,
]);
$newStorage->save();
}
$scheduledBackups = $database->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2;
$newBackup = $backup->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'database_id' => $newDatabase->id,
'database_type' => $newDatabase->getMorphClass(),
'team_id' => currentTeam()->id,
]);
$newBackup->save();
}
$environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [];
if ($database->type() === 'standalone-postgresql') {
$payload['standalone_postgresql_id'] = $newDatabase->id;
} elseif ($database->type() === 'standalone-redis') {
$payload['standalone_redis_id'] = $newDatabase->id;
} elseif ($database->type() === 'standalone-mongodb') {
$payload['standalone_mongodb_id'] = $newDatabase->id;
} elseif ($database->type() === 'standalone-mysql') {
$payload['standalone_mysql_id'] = $newDatabase->id;
} elseif ($database->type() === 'standalone-mariadb') {
$payload['standalone_mariadb_id'] = $newDatabase->id;
}
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
$payload['resourceable_id'] = $newDatabase->id;
$payload['resourceable_type'] = $newDatabase->getMorphClass();
$newEnvironmentVariable = $environmentVarible->replicate([
'id',
'created_at',
'updated_at',
])->fill($payload);
$newEnvironmentVariable->save();
}
}
foreach ($services as $service) {
$uuid = (string) new Cuid2;
$newService = $service->replicate()->fill([
$newService = $service->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination,
]);
$newService->save();
$tags = $service->tags;
foreach ($tags as $tag) {
$newService->tags()->attach($tag->id);
}
$scheduledTasks = $service->scheduled_tasks()->get();
foreach ($scheduledTasks as $task) {
$newTask = $task->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => (string) new Cuid2,
'service_id' => $newService->id,
'team_id' => currentTeam()->id,
]);
$newTask->save();
}
$environmentVariables = $service->environment_variables()->get();
foreach ($environmentVariables as $environmentVariable) {
$newEnvironmentVariable = $environmentVariable->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resourceable_id' => $newService->id,
'resourceable_type' => $newService->getMorphClass(),
]);
$newEnvironmentVariable->save();
}
foreach ($newService->applications() as $application) {
$application->update([
'status' => 'exited',
]);
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newName = '';
if (str_starts_with($volume->name, $application->uuid)) {
$newName = str($volume->name)->replace($application->uuid, $application->uuid);
} else {
$newName = $application->uuid.'-'.$volume->name;
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $application->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopService::dispatch($application, false, false);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $application->service->destination->server;
$targetServer = $newService->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartService::dispatch($application);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
$fileStorages = $application->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $application->id,
]);
$newStorage->save();
}
}
foreach ($newService->databases() as $database) {
$database->update([
'status' => 'exited',
]);
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newName = '';
if (str_starts_with($volume->name, $database->uuid)) {
$newName = str($volume->name)->replace($database->uuid, $database->uuid);
} else {
$newName = $database->uuid.'-'.$volume->name;
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $database->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopService::dispatch($database->service, false, false);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $database->service->destination->server;
$targetServer = $newService->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartService::dispatch($database->service);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
$fileStorages = $database->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $database->id,
]);
$newStorage->save();
}
$scheduledBackups = $database->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2;
$newBackup = $backup->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'database_id' => $database->id,
'database_type' => $database->getMorphClass(),
'team_id' => currentTeam()->id,
]);
$newBackup->save();
}
}
$newService->parse();
}
return redirect()->route('project.resource.index', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
]);
} catch (\Exception $e) {
return handleError($e, $this);
handleError($e, $this);
return;
} finally {
if (! isset($e)) {
return redirect()->route('project.resource.index', [
'project_uuid' => $project->uuid,
'environment_uuid' => $environment->uuid,
]);
}
}
}
}

View File

@@ -22,7 +22,7 @@ class Execution extends Component
if (! $project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
$environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']);
if (! $environment) {
return redirect()->route('dashboard');
}

View File

@@ -14,7 +14,7 @@ class Index extends Component
if (! $project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
$environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']);
if (! $environment) {
return redirect()->route('dashboard');
}
@@ -31,7 +31,7 @@ class Index extends Component
) {
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'database_uuid' => $database->uuid,
]);
}

View File

@@ -40,8 +40,26 @@ class BackupEdit extends Component
#[Validate(['required', 'string'])]
public string $frequency = '';
#[Validate(['required', 'integer', 'min:1'])]
public int $numberOfBackupsLocally = 1;
#[Validate(['string'])]
public string $timezone = '';
#[Validate(['required', 'integer'])]
public int $databaseBackupRetentionAmountLocally = 0;
#[Validate(['required', 'integer'])]
public ?int $databaseBackupRetentionDaysLocally = 0;
#[Validate(['required', 'numeric', 'min:0'])]
public ?float $databaseBackupRetentionMaxStorageLocally = 0;
#[Validate(['required', 'integer'])]
public ?int $databaseBackupRetentionAmountS3 = 0;
#[Validate(['required', 'integer'])]
public ?int $databaseBackupRetentionDaysS3 = 0;
#[Validate(['required', 'numeric', 'min:0'])]
public ?float $databaseBackupRetentionMaxStorageS3 = 0;
#[Validate(['required', 'boolean'])]
public bool $saveS3 = false;
@@ -68,19 +86,30 @@ class BackupEdit extends Component
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->customValidate();
$this->backup->enabled = $this->backupEnabled;
$this->backup->frequency = $this->frequency;
$this->backup->number_of_backups_locally = $this->numberOfBackupsLocally;
$this->backup->database_backup_retention_amount_locally = $this->databaseBackupRetentionAmountLocally;
$this->backup->database_backup_retention_days_locally = $this->databaseBackupRetentionDaysLocally;
$this->backup->database_backup_retention_max_storage_locally = $this->databaseBackupRetentionMaxStorageLocally;
$this->backup->database_backup_retention_amount_s3 = $this->databaseBackupRetentionAmountS3;
$this->backup->database_backup_retention_days_s3 = $this->databaseBackupRetentionDaysS3;
$this->backup->database_backup_retention_max_storage_s3 = $this->databaseBackupRetentionMaxStorageS3;
$this->backup->save_s3 = $this->saveS3;
$this->backup->s3_storage_id = $this->s3StorageId;
$this->backup->databases_to_backup = $this->databasesToBackup;
$this->backup->dump_all = $this->dumpAll;
$this->customValidate();
$this->backup->save();
} else {
$this->backupEnabled = $this->backup->enabled;
$this->frequency = $this->backup->frequency;
$this->numberOfBackupsLocally = $this->backup->number_of_backups_locally;
$this->timezone = data_get($this->backup->server(), 'settings.server_timezone', 'Instance timezone');
$this->databaseBackupRetentionAmountLocally = $this->backup->database_backup_retention_amount_locally;
$this->databaseBackupRetentionDaysLocally = $this->backup->database_backup_retention_days_locally;
$this->databaseBackupRetentionMaxStorageLocally = $this->backup->database_backup_retention_max_storage_locally;
$this->databaseBackupRetentionAmountS3 = $this->backup->database_backup_retention_amount_s3;
$this->databaseBackupRetentionDaysS3 = $this->backup->database_backup_retention_days_s3;
$this->databaseBackupRetentionMaxStorageS3 = $this->backup->database_backup_retention_max_storage_s3;
$this->saveS3 = $this->backup->save_s3;
$this->s3StorageId = $this->backup->s3_storage_id;
$this->databasesToBackup = $this->backup->databases_to_backup;
@@ -99,11 +128,29 @@ class BackupEdit extends Component
}
try {
if ($this->delete_associated_backups_locally) {
$this->deleteAssociatedBackupsLocally();
$server = null;
if ($this->backup->database instanceof \App\Models\ServiceDatabase) {
$server = $this->backup->database->service->destination->server;
} elseif ($this->backup->database->destination && $this->backup->database->destination->server) {
$server = $this->backup->database->destination->server;
}
if ($this->delete_associated_backups_s3) {
$this->deleteAssociatedBackupsS3();
$filenames = $this->backup->executions()
->whereNotNull('filename')
->where('filename', '!=', '')
->where('scheduled_database_backup_id', $this->backup->id)
->pluck('filename')
->filter()
->all();
if (! empty($filenames)) {
if ($this->delete_associated_backups_locally && $server) {
deleteBackupsLocally($filenames, $server);
}
if ($this->delete_associated_backups_s3 && $this->backup->s3) {
deleteBackupsS3($filenames, $this->backup->s3);
}
}
$this->backup->delete();
@@ -119,7 +166,9 @@ class BackupEdit extends Component
} else {
return redirect()->route('project.database.backup.index', $this->parameters);
}
} catch (\Throwable $e) {
} catch (\Exception $e) {
$this->dispatch('error', 'Failed to delete backup: '.$e->getMessage());
return handleError($e, $this);
}
}
@@ -156,63 +205,12 @@ class BackupEdit extends Component
}
}
private function deleteAssociatedBackupsLocally()
{
$executions = $this->backup->executions;
$backupFolder = null;
foreach ($executions as $execution) {
if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$server = $this->backup->database->service->destination->server;
} else {
$server = $this->backup->database->destination->server;
}
if (! $backupFolder) {
$backupFolder = dirname($execution->filename);
}
delete_backup_locally($execution->filename, $server);
$execution->delete();
}
if (str($backupFolder)->isNotEmpty()) {
$this->deleteEmptyBackupFolder($backupFolder, $server);
}
}
private function deleteAssociatedBackupsS3()
{
//Add function to delete backups from S3
}
private function deleteAssociatedBackupsSftp()
{
//Add function to delete backups from SFTP
}
private function deleteEmptyBackupFolder($folderPath, $server)
{
$checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkEmpty) === 'empty') {
instant_remote_process(["rmdir '$folderPath'"], $server);
$parentFolder = dirname($folderPath);
$checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkParentEmpty) === 'empty') {
instant_remote_process(["rmdir '$parentFolder'"], $server);
}
}
}
public function render()
{
return view('livewire.project.database.backup-edit', [
'checkboxes' => [
['id' => 'delete_associated_backups_locally', 'label' => __('database.delete_backups_locally')],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
['id' => 'delete_associated_backups_s3', 'label' => 'All backups will be permanently deleted (associated with this backup job) from the selected S3 Storage.'],
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
],
]);

View File

@@ -18,9 +18,9 @@ class BackupExecutions extends Component
public $setDeletableBackup;
public $delete_backup_s3 = true;
public $delete_backup_s3 = false;
public $delete_backup_sftp = true;
public $delete_backup_sftp = false;
public function getListeners()
{
@@ -57,23 +57,25 @@ class BackupExecutions extends Component
return;
}
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
$server = $execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class
? $execution->scheduledDatabaseBackup->database->service->destination->server
: $execution->scheduledDatabaseBackup->database->destination->server;
if ($this->delete_backup_s3) {
// Add logic to delete from S3
}
try {
if ($execution->filename) {
deleteBackupsLocally($execution->filename, $server);
if ($this->delete_backup_sftp) {
// Add logic to delete from SFTP
}
if ($this->delete_backup_s3 && $execution->scheduledDatabaseBackup->s3) {
deleteBackupsS3($execution->filename, $execution->scheduledDatabaseBackup->s3);
}
}
$execution->delete();
$this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions();
$execution->delete();
$this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions();
} catch (\Exception $e) {
$this->dispatch('error', 'Failed to delete backup: '.$e->getMessage());
}
}
public function download_file($exeuctionId)
@@ -83,8 +85,10 @@ class BackupExecutions extends Component
public function refreshBackupExecutions(): void
{
if ($this->backup) {
$this->executions = $this->backup->executions()->get();
if ($this->backup && $this->backup->exists) {
$this->executions = $this->backup->executions()->get()->toArray();
} else {
$this->executions = [];
}
}
@@ -141,7 +145,7 @@ class BackupExecutions extends Component
return view('livewire.project.database.backup-executions', [
'checkboxes' => [
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
// ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
],
]);
}

View File

@@ -9,11 +9,9 @@ class BackupNow extends Component
{
public $backup;
public function backup_now()
public function backupNow()
{
dispatch(new DatabaseBackupJob(
backup: $this->backup
));
DatabaseBackupJob::dispatch($this->backup);
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
}
}

View File

@@ -6,23 +6,34 @@ use Livewire\Component;
class Configuration extends Component
{
public $currentRoute;
public $database;
public $project;
public $environment;
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (! $project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (! $environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (! $database) {
return redirect()->route('dashboard');
}
$this->currentRoute = request()->route()->getName();
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
->where('uuid', request()->route('project_uuid'))
->firstOrFail();
$environment = $project->environments()
->select('id', 'name', 'project_id', 'uuid')
->where('uuid', request()->route('environment_uuid'))
->firstOrFail();
$database = $environment->databases()
->where('uuid', request()->route('database_uuid'))
->firstOrFail();
$this->database = $database;
$this->project = $project;
$this->environment = $environment;
if (str($this->database->status)->startsWith('running') && is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged');

View File

@@ -37,6 +37,12 @@ class Import extends Component
public array $importCommands = [];
public bool $dumpAll = false;
public string $restoreCommandText = '';
public string $customLocation = '';
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
@@ -56,10 +62,62 @@ class Import extends Component
public function mount()
{
if (isDev()) {
$this->customLocation = '/data/coolify/pg-dump-all-1736245863.gz';
}
$this->parameters = get_route_parameters();
$this->getContainers();
}
public function updatedDumpAll($value)
{
switch ($this->resource->getMorphClass()) {
case \App\Models\StandaloneMariadb::class:
if ($value === true) {
$this->mariadbRestoreCommand = <<<'EOD'
for pid in $(mariadb -u root -p$MARIADB_ROOT_PASSWORD -N -e "SELECT id FROM information_schema.processlist WHERE user != 'root';"); do
mariadb -u root -p$MARIADB_ROOT_PASSWORD -e "KILL $pid" 2>/dev/null || true
done && \
mariadb -u root -p$MARIADB_ROOT_PASSWORD -N -e "SELECT CONCAT('DROP DATABASE IF EXISTS \`',schema_name,'\`;') FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','mysql','performance_schema','sys');" | mariadb -u root -p$MARIADB_ROOT_PASSWORD && \
mariadb -u root -p$MARIADB_ROOT_PASSWORD -e "CREATE DATABASE IF NOT EXISTS \`default\`;" && \
(gunzip -cf $tmpPath 2>/dev/null || cat $tmpPath) | sed -e '/^CREATE DATABASE/d' -e '/^USE \`mysql\`/d' | mariadb -u root -p$MARIADB_ROOT_PASSWORD default
EOD;
$this->restoreCommandText = $this->mariadbRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | mariadb -u root -p$MARIADB_ROOT_PASSWORD default';
} else {
$this->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
}
break;
case \App\Models\StandaloneMysql::class:
if ($value === true) {
$this->mysqlRestoreCommand = <<<'EOD'
for pid in $(mysql -u root -p$MYSQL_ROOT_PASSWORD -N -e "SELECT id FROM information_schema.processlist WHERE user != 'root';"); do
mysql -u root -p$MYSQL_ROOT_PASSWORD -e "KILL $pid" 2>/dev/null || true
done && \
mysql -u root -p$MYSQL_ROOT_PASSWORD -N -e "SELECT CONCAT('DROP DATABASE IF EXISTS \`',schema_name,'\`;') FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','mysql','performance_schema','sys');" | mysql -u root -p$MYSQL_ROOT_PASSWORD && \
mysql -u root -p$MYSQL_ROOT_PASSWORD -e "CREATE DATABASE IF NOT EXISTS \`default\`;" && \
(gunzip -cf $tmpPath 2>/dev/null || cat $tmpPath) | sed -e '/^CREATE DATABASE/d' -e '/^USE \`mysql\`/d' | mysql -u root -p$MYSQL_ROOT_PASSWORD default
EOD;
$this->restoreCommandText = $this->mysqlRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | mysql -u root -p$MYSQL_ROOT_PASSWORD default';
} else {
$this->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
}
break;
case \App\Models\StandalonePostgresql::class:
if ($value === true) {
$this->postgresqlRestoreCommand = <<<'EOD'
psql -U $POSTGRES_USER -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IS NOT NULL AND pid <> pg_backend_pid()" && \
psql -U $POSTGRES_USER -t -c "SELECT datname FROM pg_database WHERE NOT datistemplate" | xargs -I {} dropdb -U $POSTGRES_USER --if-exists {} && \
createdb -U $POSTGRES_USER postgres
EOD;
$this->restoreCommandText = $this->postgresqlRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | psql -U $POSTGRES_USER postgres';
} else {
$this->postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
}
break;
}
}
public function getContainers()
{
$this->containers = collect();
@@ -87,6 +145,24 @@ class Import extends Component
}
}
public function checkFile()
{
if (filled($this->customLocation)) {
try {
$result = instant_remote_process(["ls -l {$this->customLocation}"], $this->server, throwError: false);
if (blank($result)) {
$this->dispatch('error', 'The file does not exist or has been deleted.');
return;
}
$this->filename = $this->customLocation;
$this->dispatch('success', 'The file exists.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}
public function runImport()
{
if ($this->filename === '') {
@@ -95,46 +171,83 @@ class Import extends Component
return;
}
try {
$uploadedFilename = "upload/{$this->resource->uuid}/restore";
$path = Storage::path($uploadedFilename);
if (! Storage::exists($uploadedFilename)) {
$this->dispatch('error', 'The file does not exist or has been deleted.');
$this->importCommands = [];
if (filled($this->customLocation)) {
$backupFileName = '/tmp/restore_'.$this->resource->uuid;
$this->importCommands[] = "docker cp {$this->customLocation} {$this->container}:{$backupFileName}";
$tmpPath = $backupFileName;
} else {
$backupFileName = "upload/{$this->resource->uuid}/restore";
$path = Storage::path($backupFileName);
if (! Storage::exists($backupFileName)) {
$this->dispatch('error', 'The file does not exist or has been deleted.');
return;
return;
}
$tmpPath = '/tmp/'.basename($backupFileName).'_'.$this->resource->uuid;
instant_scp($path, $tmpPath, $this->server);
Storage::delete($backupFileName);
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
}
$tmpPath = '/tmp/'.basename($uploadedFilename);
instant_scp($path, $tmpPath, $this->server);
Storage::delete($uploadedFilename);
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
// Copy the restore command to a script file
$scriptPath = "/tmp/restore_{$this->resource->uuid}.sh";
switch ($this->resource->getMorphClass()) {
case \App\Models\StandaloneMariadb::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mariadbRestoreCommand} < {$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
$restoreCommand = $this->mariadbRestoreCommand;
if ($this->dumpAll) {
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mariadb -u root -p\$MARIADB_ROOT_PASSWORD";
} else {
$restoreCommand .= " < {$tmpPath}";
}
break;
case \App\Models\StandaloneMysql::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mysqlRestoreCommand} < {$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
$restoreCommand = $this->mysqlRestoreCommand;
if ($this->dumpAll) {
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mysql -u root -p\$MYSQL_ROOT_PASSWORD";
} else {
$restoreCommand .= " < {$tmpPath}";
}
break;
case \App\Models\StandalonePostgresql::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
$restoreCommand = $this->postgresqlRestoreCommand;
if ($this->dumpAll) {
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | psql -U \$POSTGRES_USER postgres";
} else {
$restoreCommand .= " {$tmpPath}";
}
break;
case \App\Models\StandaloneMongodb::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mongodbRestoreCommand}{$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
$restoreCommand = $this->mongodbRestoreCommand;
if ($this->dumpAll === false) {
$restoreCommand .= " {$tmpPath}";
}
break;
}
$this->importCommands[] = "docker exec {$this->container} sh -c 'rm {$tmpPath}'";
$restoreCommandBase64 = base64_encode($restoreCommand);
$this->importCommands[] = "echo \"{$restoreCommandBase64}\" | base64 -d > {$scriptPath}";
$this->importCommands[] = "chmod +x {$scriptPath}";
$this->importCommands[] = "docker cp {$scriptPath} {$this->container}:{$scriptPath}";
$this->importCommands[] = "docker exec {$this->container} sh -c '{$scriptPath}'";
$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);
$activity = remote_process($this->importCommands, $this->server, ignore_errors: true, callEventOnFinish: 'RestoreJobFinished', callEventData: [
'scriptPath' => $scriptPath,
'tmpPath' => $tmpPath,
'container' => $this->container,
'serverId' => $this->server->id,
]);
$this->dispatch('activityMonitor', $activity->id);
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->filename = null;
$this->importCommands = [];
}
}
}

View File

@@ -9,8 +9,6 @@ use App\Models\StandalonePostgresql;
use Exception;
use Livewire\Component;
use function Aws\filter;
class General extends Component
{
public StandalonePostgresql $database;
@@ -126,10 +124,52 @@ class General extends Component
public function save_init_script($script)
{
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']);
$this->database->init_scripts = array_merge($this->database->init_scripts, [$script]);
$initScripts = collect($this->database->init_scripts ?? []);
$existingScript = $initScripts->firstWhere('filename', $script['filename']);
$oldScript = $initScripts->firstWhere('index', $script['index']);
if ($existingScript && $existingScript['index'] !== $script['index']) {
$this->dispatch('error', 'A script with this filename already exists.');
return;
}
$container_name = $this->database->uuid;
$configuration_dir = database_configuration_dir().'/'.$container_name;
if ($oldScript && $oldScript['filename'] !== $script['filename']) {
$old_file_path = "$configuration_dir/docker-entrypoint-initdb.d/{$oldScript['filename']}";
$delete_command = "rm -f $old_file_path";
try {
instant_remote_process([$delete_command], $this->server);
} catch (\Exception $e) {
$this->dispatch('error', 'Failed to remove old init script from server: '.$e->getMessage());
return;
}
}
$index = $initScripts->search(function ($item) use ($script) {
return $item['index'] === $script['index'];
});
if ($index !== false) {
$initScripts[$index] = $script;
} else {
$initScripts->push($script);
}
$this->database->init_scripts = $initScripts->values()
->map(function ($item, $index) {
$item['index'] = $index;
return $item;
})
->all();
$this->database->save();
$this->dispatch('success', 'Init script saved.');
$this->dispatch('success', 'Init script saved and updated.');
}
public function delete_init_script($script)
@@ -137,12 +177,32 @@ class General extends Component
$collection = collect($this->database->init_scripts);
$found = $collection->firstWhere('filename', $script['filename']);
if ($found) {
$this->database->init_scripts = $collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray();
$container_name = $this->database->uuid;
$configuration_dir = database_configuration_dir().'/'.$container_name;
$file_path = "$configuration_dir/docker-entrypoint-initdb.d/{$script['filename']}";
$command = "rm -f $file_path";
try {
instant_remote_process([$command], $this->server);
} catch (\Exception $e) {
$this->dispatch('error', 'Failed to remove init script from server: '.$e->getMessage());
return;
}
$updatedScripts = $collection->filter(fn ($s) => $s['filename'] !== $script['filename'])
->values()
->map(function ($item, $index) {
$item['index'] = $index;
return $item;
})
->all();
$this->database->init_scripts = $updatedScripts;
$this->database->save();
$this->refresh();
$this->dispatch('success', 'Init script deleted.');
return;
$this->dispatch('success', 'Init script deleted from the database and the server.');
}
}

View File

@@ -23,11 +23,11 @@ class EnvironmentEdit extends Component
#[Validate(['nullable', 'string', 'max:255'])]
public ?string $description = null;
public function mount(string $project_uuid, string $environment_name)
public function mount(string $project_uuid, string $environment_uuid)
{
try {
$this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments()->where('name', $environment_name)->firstOrFail();
$this->environment = $this->project->environments()->where('uuid', $environment_uuid)->firstOrFail();
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -52,7 +52,10 @@ class EnvironmentEdit extends Component
{
try {
$this->syncData(true);
$this->redirectRoute('project.environment.edit', ['environment_name' => $this->environment->name, 'project_uuid' => $this->project->uuid]);
$this->redirectRoute('project.environment.edit', [
'environment_uuid' => $this->environment->uuid,
'project_uuid' => $this->project->uuid,
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -5,6 +5,7 @@ namespace App\Livewire\Project;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Support\Facades\Redirect;
use Livewire\Component;
class Index extends Component
@@ -30,4 +31,18 @@ class Index extends Component
{
return view('livewire.project.index');
}
public function navigateToProject($projectUuid)
{
$project = Project::where('uuid', $projectUuid)->first();
if ($project && $project->environments->count() === 1) {
return Redirect::route('project.resource.index', [
'project_uuid' => $projectUuid,
'environment_uuid' => $project->environments->first()->uuid,
]);
}
return Redirect::route('project.show', ['project_uuid' => $projectUuid]);
}
}

View File

@@ -59,7 +59,7 @@ class DockerCompose extends Component
}
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
@@ -87,7 +87,8 @@ class DockerCompose extends Component
'value' => $variable,
'is_build_time' => false,
'is_preview' => false,
'service_id' => $service->id,
'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
]);
}
$service->name = "service-$service->uuid";
@@ -96,7 +97,7 @@ class DockerCompose extends Component
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid,
]);
} catch (\Throwable $e) {

View File

@@ -45,7 +45,7 @@ class DockerImage extends Component
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$application = Application::create([
'name' => 'docker-image-'.new Cuid2,
'repository_project_id' => 0,
@@ -69,7 +69,7 @@ class DockerImage extends Component
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid,
]);
}

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Project\New;
use App\Models\Project;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class EmptyProject extends Component
{
@@ -12,8 +13,9 @@ class EmptyProject extends Component
$project = Project::create([
'name' => generate_random_name(),
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
]);
return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production']);
return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]);
}
}

View File

@@ -177,7 +177,7 @@ class GithubPrivateRepository extends Component
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$application = Application::create([
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
@@ -211,7 +211,7 @@ class GithubPrivateRepository extends Component
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid,
]);
} catch (\Throwable $e) {

View File

@@ -136,7 +136,7 @@ class GithubPrivateRepositoryDeployKey extends Component
$this->get_git_source();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
if ($this->git_source === 'other') {
$application_init = [
'name' => generate_random_name(),
@@ -184,7 +184,7 @@ class GithubPrivateRepositoryDeployKey extends Component
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid,
]);
} catch (\Throwable $e) {

View File

@@ -225,7 +225,7 @@ class PublicGitRepository extends Component
$this->validate();
$destination_uuid = $this->query['destination'];
$project_uuid = $this->parameters['project_uuid'];
$environment_name = $this->parameters['environment_name'];
$environment_uuid = $this->parameters['environment_uuid'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
@@ -237,7 +237,7 @@ class PublicGitRepository extends Component
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $project_uuid)->first();
$environment = $project->load(['environments'])->environments->where('name', $environment_name)->first();
$environment = $project->load(['environments'])->environments->where('uuid', $environment_uuid)->first();
if ($this->build_pack === 'dockercompose' && isDev() && $this->new_compose_services) {
$server = $destination->server;
@@ -260,7 +260,7 @@ class PublicGitRepository extends Component
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid,
]);
@@ -319,7 +319,7 @@ class PublicGitRepository extends Component
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid,
]);
} catch (\Throwable $e) {

View File

@@ -23,6 +23,8 @@ class Select extends Component
public Collection|null|Server $servers;
public bool $onlyBuildServerAvailable = false;
public ?Collection $standaloneDockers;
public ?Collection $swarmDockers;
@@ -61,7 +63,7 @@ class Select extends Component
}
$projectUuid = data_get($this->parameters, 'project_uuid');
$this->environments = Project::whereUuid($projectUuid)->first()->environments;
$this->selectedEnvironment = data_get($this->parameters, 'environment_name');
$this->selectedEnvironment = data_get($this->parameters, 'environment_uuid');
}
public function render()
@@ -73,20 +75,10 @@ class Select extends Component
{
return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->selectedEnvironment,
'environment_uuid' => $this->selectedEnvironment,
]);
}
// public function addExistingPostgresql()
// {
// try {
// instantCommand("psql {$this->existingPostgresqlUrl} -c 'SELECT 1'");
// $this->dispatch('success', 'Successfully connected to the database.');
// } catch (\Throwable $e) {
// return handleError($e, $this);
// }
// }
public function loadServices()
{
$services = get_service_templates(true);
@@ -308,7 +300,7 @@ class Select extends Component
return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'],
'environment_uuid' => $this->parameters['environment_uuid'],
'type' => $this->type,
'destination' => $this->destination_uuid,
'server_id' => $this->server_id,
@@ -323,7 +315,7 @@ class Select extends Component
} else {
return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'],
'environment_uuid' => $this->parameters['environment_uuid'],
'type' => $this->type,
'destination' => $this->destination_uuid,
'server_id' => $this->server_id,
@@ -335,5 +327,11 @@ class Select extends Component
{
$this->servers = Server::isUsable()->get()->sortBy('name');
$this->allServers = $this->servers;
if ($this->allServers && $this->allServers->isNotEmpty()) {
$this->onlyBuildServerAvailable = $this->allServers->every(function ($server) {
return $server->isBuildServer();
});
}
}
}

View File

@@ -46,7 +46,7 @@ CMD ["nginx", "-g", "daemon off;"]
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$port = get_port_from_dockerfile($this->dockerfile);
if (! $port) {
@@ -78,7 +78,7 @@ CMD ["nginx", "-g", "daemon off;"]
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid,
]);
}

View File

@@ -25,7 +25,7 @@ class Create extends Component
return redirect()->route('dashboard');
}
$this->project = $project;
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first();
$environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first();
if (! $environment) {
return redirect()->route('dashboard');
}
@@ -57,7 +57,7 @@ class Create extends Component
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'database_uuid' => $database->uuid,
]);
}
@@ -95,7 +95,8 @@ class Create extends Component
EnvironmentVariable::create([
'key' => $key,
'value' => $value,
'service_id' => $service->id,
'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
'is_build_time' => false,
'is_preview' => false,
]);
@@ -106,7 +107,7 @@ class Create extends Component
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid,
]);
}

View File

@@ -15,7 +15,7 @@ class EnvironmentSelect extends Component
public function mount()
{
$this->selectedEnvironment = request()->route('environment_name');
$this->selectedEnvironment = request()->route('environment_uuid');
$this->project_uuid = request()->route('project_uuid');
}
@@ -28,7 +28,7 @@ class EnvironmentSelect extends Component
} else {
return redirect()->route('project.resource.index', [
'project_uuid' => $this->project_uuid,
'environment_name' => $value,
'environment_uuid' => $value,
]);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Resource;
use App\Models\Environment;
use App\Models\Project;
use Illuminate\Support\Collection;
use Livewire\Component;
class Index extends Component
@@ -12,39 +13,42 @@ class Index extends Component
public Environment $environment;
public $applications = [];
public Collection $applications;
public $postgresqls = [];
public Collection $postgresqls;
public $redis = [];
public Collection $redis;
public $mongodbs = [];
public Collection $mongodbs;
public $mysqls = [];
public Collection $mysqls;
public $mariadbs = [];
public Collection $mariadbs;
public $keydbs = [];
public Collection $keydbs;
public $dragonflies = [];
public Collection $dragonflies;
public $clickhouses = [];
public Collection $clickhouses;
public $services = [];
public Collection $services;
public array $parameters;
public function mount()
{
$this->applications = $this->postgresqls = $this->redis = $this->mongodbs = $this->mysqls = $this->mariadbs = $this->keydbs = $this->dragonflies = $this->clickhouses = $this->services = collect();
$this->parameters = get_route_parameters();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (! $project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first();
if (! $environment) {
return redirect()->route('dashboard');
}
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id', 'name')
->where('uuid', request()->route('project_uuid'))
->firstOrFail();
$environment = $project->environments()
->select('id', 'uuid', 'name', 'project_id')
->where('uuid', request()->route('environment_uuid'))
->firstOrFail();
$this->project = $project;
$this->environment = $environment->loadCount([
'applications',
@@ -69,9 +73,9 @@ class Index extends Component
])->get()->sortBy('name');
$this->applications = $this->applications->map(function ($application) {
$application->hrefLink = route('project.application.configuration', [
'project_uuid' => $this->project->uuid,
'application_uuid' => $application->uuid,
'environment_name' => $this->environment->name,
'project_uuid' => data_get($application, 'environment.project.uuid'),
'environment_uuid' => data_get($application, 'environment.uuid'),
'application_uuid' => data_get($application, 'uuid'),
]);
return $application;
@@ -89,14 +93,6 @@ class Index extends Component
'clickhouses' => 'clickhouses',
];
// Load all server-related data first to prevent duplicate queries
$serverData = $this->environment->applications()
->with(['destination.server.settings'])
->get()
->pluck('destination.server')
->filter()
->unique('id');
foreach ($databaseTypes as $property => $relation) {
$this->{$property} = $this->environment->{$relation}()->with([
'tags',
@@ -106,7 +102,7 @@ class Index extends Component
$db->hrefLink = route('project.database.configuration', [
'project_uuid' => $this->project->uuid,
'database_uuid' => $db->uuid,
'environment_name' => $this->environment->name,
'environment_uuid' => data_get($this->environment, 'uuid'),
]);
return $db;
@@ -120,9 +116,9 @@ class Index extends Component
])->get()->sortBy('name');
$this->services = $this->services->map(function ($service) {
$service->hrefLink = route('project.service.configuration', [
'project_uuid' => $this->project->uuid,
'service_uuid' => $service->uuid,
'environment_name' => $this->environment->name,
'project_uuid' => data_get($service, 'environment.project.uuid'),
'environment_uuid' => data_get($service, 'environment.uuid'),
'service_uuid' => data_get($service, 'uuid'),
]);
return $service;

View File

@@ -9,16 +9,22 @@ use Livewire\Component;
class Configuration extends Component
{
public $currentRoute;
public $project;
public $environment;
public ?Service $service = null;
public $applications;
public $databases;
public array $parameters;
public array $query;
public array $parameters;
public function getListeners()
{
$userId = Auth::id();
@@ -38,11 +44,21 @@ class Configuration extends Component
public function mount()
{
$this->parameters = get_route_parameters();
$this->currentRoute = request()->route()->getName();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->first();
if (! $this->service) {
return redirect()->route('dashboard');
}
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
->where('uuid', request()->route('project_uuid'))
->firstOrFail();
$environment = $project->environments()
->select('id', 'uuid', 'name', 'project_id')
->where('uuid', request()->route('environment_uuid'))
->firstOrFail();
$this->service = $environment->services()->whereUuid(request()->route('service_uuid'))->firstOrFail();
$this->project = $project;
$this->environment = $environment;
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
}
@@ -83,7 +99,7 @@ class Configuration extends Component
$this->service->databases->each(function ($database) {
$database->refresh();
});
$this->dispatch('$refresh');
$this->dispatch('refresh');
} catch (\Exception $e) {
return handleError($e, $this);
}

View File

@@ -5,6 +5,7 @@ namespace App\Livewire\Project\Service;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Actions\Shared\PullImage;
use App\Enums\ProcessStatus;
use App\Events\ServiceStatusChanged;
use App\Models\Service;
use Illuminate\Support\Facades\Auth;
@@ -68,11 +69,9 @@ class Navbar extends Component
public function checkDeployments()
{
try {
// TODO: This is a temporary solution. We need to refactor this.
// We need to delete null bytes somehow.
$activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first();
$status = data_get($activity, 'properties.status');
if ($status === 'queued' || $status === 'in_progress') {
if ($status === ProcessStatus::QUEUED->value || $status === ProcessStatus::IN_PROGRESS->value) {
$this->isDeploymentProgress = true;
} else {
$this->isDeploymentProgress = false;
@@ -80,25 +79,46 @@ class Navbar extends Component
} catch (\Throwable) {
$this->isDeploymentProgress = false;
}
return $this->isDeploymentProgress;
}
public function start()
{
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
return;
}
$this->service->parse();
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
}
public function stop()
public function forceDeploy()
{
StopService::run($this->service, false, $this->docker_cleanup);
ServiceStatusChanged::dispatch();
try {
$activities = Activity::where('properties->type_uuid', $this->service->uuid)->where('properties->status', ProcessStatus::IN_PROGRESS->value)->orWhere('properties->status', ProcessStatus::QUEUED->value)->get();
foreach ($activities as $activity) {
$activity->properties->status = ProcessStatus::ERROR->value;
$activity->save();
}
$this->service->parse();
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
}
}
public function stop($cleanupContainers = false)
{
try {
StopService::run($this->service, false, $this->docker_cleanup);
ServiceStatusChanged::dispatch();
if ($cleanupContainers) {
$this->dispatch('success', 'Containers cleaned up.');
} else {
$this->dispatch('success', 'Service stopped.');
}
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
}
}
public function restart()

View File

@@ -20,7 +20,7 @@ class Danger extends Component
public $projectUuid;
public $environmentName;
public $environmentUuid;
public bool $delete_configurations = true;
@@ -39,7 +39,7 @@ class Danger extends Component
$parameters = get_route_parameters();
$this->modalId = new Cuid2;
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name');
$this->environmentUuid = data_get($parameters, 'environment_uuid');
if ($this->resource === null) {
if (isset($parameters['service_uuid'])) {
@@ -107,7 +107,7 @@ class Danger extends Component
return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName,
'environment_uuid' => $this->environmentUuid,
]);
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -8,6 +8,7 @@ use App\Events\ApplicationStatusChanged;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
@@ -17,7 +18,7 @@ class Destination extends Component
{
public $resource;
public $networks = [];
public Collection $networks;
public function getListeners()
{
@@ -30,6 +31,7 @@ class Destination extends Component
public function mount()
{
$this->networks = collect([]);
$this->loadData();
}
@@ -55,38 +57,46 @@ class Destination extends Component
}
}
public function stop(int $server_id)
public function stop($serverId)
{
$server = Server::find($server_id);
StopApplicationOneServer::run($this->resource, $server);
$this->refreshServers();
try {
$server = Server::ownedByCurrentTeam()->findOrFail($serverId);
StopApplicationOneServer::run($this->resource, $server);
$this->refreshServers();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function redeploy(int $network_id, int $server_id)
{
if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
try {
if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
return;
return;
}
$deployment_uuid = new Cuid2;
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
$destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail();
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->resource,
server: $server,
destination: $destination,
only_this_server: true,
no_questions_asked: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => data_get($this->resource, 'environment.project.uuid'),
'application_uuid' => data_get($this->resource, 'uuid'),
'deployment_uuid' => $deployment_uuid,
'environment_uuid' => data_get($this->resource, 'environment.uuid'),
]);
} catch (\Exception $e) {
return handleError($e, $this);
}
$deployment_uuid = new Cuid2;
$server = Server::find($server_id);
$destination = StandaloneDocker::find($network_id);
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->resource,
server: $server,
destination: $destination,
only_this_server: true,
no_questions_asked: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => data_get($this->resource, 'environment.project.uuid'),
'application_uuid' => data_get($this->resource, 'uuid'),
'deployment_uuid' => $deployment_uuid,
'environment_name' => data_get($this->resource, 'environment.name'),
]);
}
public function promote(int $network_id, int $server_id)
@@ -119,23 +129,27 @@ class Destination extends Component
public function removeServer(int $network_id, int $server_id, $password)
{
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
try {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
}
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
return;
}
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
StopApplicationOneServer::run($this->resource, $server);
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
$this->loadData();
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
} catch (\Exception $e) {
return handleError($e, $this);
}
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
return;
}
$server = Server::find($server_id);
StopApplicationOneServer::run($this->resource, $server);
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
$this->loadData();
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class All extends Component
{
@@ -14,38 +13,35 @@ class All extends Component
public bool $showPreview = false;
public ?string $modalId = null;
public ?string $variables = null;
public ?string $variablesPreview = null;
public string $view = 'normal';
public bool $is_env_sorting_enabled = false;
protected $listeners = [
'saveKey' => 'submit',
'refreshEnvs',
'environmentVariableDeleted' => 'refreshEnvs',
];
protected $rules = [
'resource.settings.is_env_sorting_enabled' => 'required|boolean',
];
public function mount()
{
$this->is_env_sorting_enabled = data_get($this->resource, 'settings.is_env_sorting_enabled', false);
$this->resourceClass = get_class($this->resource);
$resourceWithPreviews = [\App\Models\Application::class];
$simpleDockerfile = ! is_null(data_get($this->resource, 'dockerfile'));
$simpleDockerfile = filled(data_get($this->resource, 'dockerfile'));
if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) {
$this->showPreview = true;
}
$this->modalId = new Cuid2;
$this->sortEnvironmentVariables();
}
public function instantSave()
{
$this->resource->settings->is_env_sorting_enabled = $this->is_env_sorting_enabled;
$this->resource->settings->save();
$this->sortEnvironmentVariables();
$this->dispatch('success', 'Environment variable settings updated.');
@@ -53,7 +49,7 @@ class All extends Component
public function sortEnvironmentVariables()
{
if (! data_get($this->resource, 'settings.is_env_sorting_enabled')) {
if ($this->is_env_sorting_enabled === false) {
if ($this->resource->environment_variables) {
$this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values();
}
@@ -178,35 +174,12 @@ class All extends Component
$environment->is_multiline = $data['is_multiline'] ?? false;
$environment->is_literal = $data['is_literal'] ?? false;
$environment->is_preview = $data['is_preview'] ?? false;
$resourceType = $this->resource->type();
$resourceIdField = $this->getResourceIdField($resourceType);
if ($resourceIdField) {
$environment->$resourceIdField = $this->resource->id;
}
$environment->resourceable_id = $this->resource->id;
$environment->resourceable_type = $this->resource->getMorphClass();
return $environment;
}
private function getResourceIdField($resourceType)
{
$resourceTypes = [
'application' => 'application_id',
'standalone-postgresql' => 'standalone_postgresql_id',
'standalone-redis' => 'standalone_redis_id',
'standalone-mongodb' => 'standalone_mongodb_id',
'standalone-mysql' => 'standalone_mysql_id',
'standalone-mariadb' => 'standalone_mariadb_id',
'standalone-keydb' => 'standalone_keydb_id',
'standalone-dragonfly' => 'standalone_dragonfly_id',
'standalone-clickhouse' => 'standalone_clickhouse_id',
'service' => 'service_id',
];
return $resourceTypes[$resourceType] ?? null;
}
private function deleteRemovedVariables($isPreview, $variables)
{
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
@@ -231,34 +204,14 @@ class All extends Component
$environment->is_build_time = false;
$environment->is_multiline = false;
$environment->is_preview = $isPreview;
$environment->resourceable_id = $this->resource->id;
$environment->resourceable_type = $this->resource->getMorphClass();
$this->setEnvironmentResourceId($environment);
$environment->save();
}
}
}
private function setEnvironmentResourceId($environment)
{
$resourceTypes = [
'application' => 'application_id',
'standalone-postgresql' => 'standalone_postgresql_id',
'standalone-redis' => 'standalone_redis_id',
'standalone-mongodb' => 'standalone_mongodb_id',
'standalone-mysql' => 'standalone_mysql_id',
'standalone-mariadb' => 'standalone_mariadb_id',
'standalone-keydb' => 'standalone_keydb_id',
'standalone-dragonfly' => 'standalone_dragonfly_id',
'standalone-clickhouse' => 'standalone_clickhouse_id',
'service' => 'service_id',
];
$resourceType = $this->resource->type();
if (isset($resourceTypes[$resourceType])) {
$environment->{$resourceTypes[$resourceType]} = $this->resource->id;
}
}
public function refreshEnvs()
{
$this->resource->refresh();

View File

@@ -5,7 +5,6 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use App\Models\SharedEnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Show extends Component
{
@@ -13,8 +12,6 @@ class Show extends Component
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
public ?string $modalId = null;
public bool $isDisabled = false;
public bool $isLocked = false;
@@ -23,6 +20,26 @@ class Show extends Component
public string $type;
public string $key;
public ?string $value = null;
public ?string $real_value = null;
public bool $is_shared = false;
public bool $is_build_time = false;
public bool $is_multiline = false;
public bool $is_literal = false;
public bool $is_shown_once = false;
public bool $is_required = false;
public bool $is_really_required = false;
protected $listeners = [
'refreshEnvs' => 'refresh',
'refresh',
@@ -30,40 +47,59 @@ class Show extends Component
];
protected $rules = [
'env.key' => 'required|string',
'env.value' => 'nullable',
'env.is_build_time' => 'required|boolean',
'env.is_multiline' => 'required|boolean',
'env.is_literal' => 'required|boolean',
'env.is_shown_once' => 'required|boolean',
'env.real_value' => 'nullable',
'env.is_required' => 'required|boolean',
'key' => 'required|string',
'value' => 'nullable',
'is_build_time' => 'required|boolean',
'is_multiline' => 'required|boolean',
'is_literal' => 'required|boolean',
'is_shown_once' => 'required|boolean',
'real_value' => 'nullable',
'is_required' => 'required|boolean',
];
protected $validationAttributes = [
'env.key' => 'Key',
'env.value' => 'Value',
'env.is_build_time' => 'Build Time',
'env.is_multiline' => 'Multiline',
'env.is_literal' => 'Literal',
'env.is_shown_once' => 'Shown Once',
'env.is_required' => 'Required',
];
public function refresh()
{
$this->env->refresh();
$this->checkEnvs();
}
public function mount()
{
$this->syncData();
if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) {
$this->isSharedVariable = true;
}
$this->modalId = new Cuid2;
$this->parameters = get_route_parameters();
$this->checkEnvs();
}
public function refresh()
{
$this->syncData();
$this->checkEnvs();
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->env->key = $this->key;
$this->env->value = $this->value;
$this->env->is_build_time = $this->is_build_time;
$this->env->is_multiline = $this->is_multiline;
$this->env->is_literal = $this->is_literal;
$this->env->is_shown_once = $this->is_shown_once;
$this->env->is_required = $this->is_required;
$this->env->is_shared = $this->is_shared;
$this->env->save();
} else {
$this->key = $this->env->key;
$this->value = $this->env->value;
$this->is_build_time = $this->env->is_build_time ?? false;
$this->is_multiline = $this->env->is_multiline;
$this->is_literal = $this->env->is_literal;
$this->is_shown_once = $this->env->is_shown_once;
$this->is_required = $this->env->is_required ?? false;
$this->is_really_required = $this->env->is_really_required ?? false;
$this->is_shared = $this->env->is_shared ?? false;
$this->real_value = $this->env->real_value;
}
}
public function checkEnvs()
@@ -107,17 +143,17 @@ class Show extends Component
try {
if ($this->isSharedVariable) {
$this->validate([
'env.key' => 'required|string',
'env.value' => 'nullable',
'env.is_shown_once' => 'required|boolean',
'key' => 'required|string',
'value' => 'nullable',
'is_shown_once' => 'required|boolean',
]);
} else {
$this->validate();
}
if (! $this->isSharedVariable && $this->env->is_required && str($this->env->real_value)->isEmpty()) {
if (! $this->isSharedVariable && $this->is_required && str($this->value)->isEmpty()) {
$oldValue = $this->env->getOriginal('value');
$this->env->value = $oldValue;
$this->value = $oldValue;
$this->dispatch('error', 'Required environment variable cannot be empty.');
return;
@@ -126,10 +162,10 @@ class Show extends Component
$this->serialize();
if ($this->isSharedVariable) {
unset($this->env->is_required);
unset($this->is_required);
}
$this->env->save();
$this->syncData(true);
$this->dispatch('success', 'Environment variable updated.');
$this->dispatch('envsUpdated');
} catch (\Exception $e) {

View File

@@ -2,6 +2,12 @@
namespace App\Livewire\Project\Shared;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StartDatabase;
use App\Actions\Database\StopDatabase;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Jobs\VolumeCloneJob;
use App\Models\Environment;
use App\Models\Project;
use App\Models\StandaloneDocker;
@@ -15,21 +21,28 @@ class ResourceOperations extends Component
public $projectUuid;
public $environmentName;
public $environmentUuid;
public $projects;
public $servers;
public bool $cloneVolumeData = false;
public function mount()
{
$parameters = get_route_parameters();
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name');
$this->environmentUuid = data_get($parameters, 'environment_uuid');
$this->projects = Project::ownedByCurrentTeam()->get();
$this->servers = currentTeam()->servers;
}
public function toggleVolumeCloning(bool $value)
{
$this->cloneVolumeData = $value;
}
public function cloneTo($destination_id)
{
$new_destination = StandaloneDocker::find($destination_id);
@@ -41,38 +54,151 @@ class ResourceOperations extends Component
}
$uuid = (string) new Cuid2;
$server = $new_destination->server;
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
$new_resource = $this->resource->replicate()->fill([
$name = 'clone-of-'.str($this->resource->name)->limit(20).'-'.$uuid;
$applicationSettings = $this->resource->settings;
$url = $this->resource->fqdn;
if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
$url = generateFqdn($server, $uuid);
}
$new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
'additional_servers_count',
'additional_networks_count',
])->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'fqdn' => generateFqdn($server, $uuid),
'name' => $name,
'fqdn' => $url,
'status' => 'exited',
'destination_id' => $new_destination->id,
]);
$new_resource->save();
if ($new_resource->destination->server->proxyType() !== 'NONE') {
if ($new_resource->destination->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
$customLabels = str(implode('|coolify|', generateLabelsApplication($new_resource)))->replace('|coolify|', "\n");
$new_resource->custom_labels = base64_encode($customLabels);
$new_resource->save();
}
$environmentVaribles = $this->resource->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([
$new_resource->settings()->delete();
if ($applicationSettings) {
$newApplicationSettings = $applicationSettings->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'application_id' => $new_resource->id,
]);
$newEnvironmentVariable->save();
$newApplicationSettings->save();
}
$tags = $this->resource->tags;
foreach ($tags as $tag) {
$new_resource->tags()->attach($tag->id);
}
$scheduledTasks = $this->resource->scheduled_tasks()->get();
foreach ($scheduledTasks as $task) {
$newTask = $task->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => (string) new Cuid2,
'application_id' => $new_resource->id,
'team_id' => currentTeam()->id,
]);
$newTask->save();
}
$applicationPreviews = $this->resource->previews()->get();
foreach ($applicationPreviews as $preview) {
$newPreview = $preview->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'application_id' => $new_resource->id,
'status' => 'exited',
]);
$newPreview->save();
}
$persistentVolumes = $this->resource->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newPersistentVolume = $volume->replicate()->fill([
'name' => $new_resource->uuid.'-'.str($volume->name)->afterLast('-'),
$newName = '';
if (str_starts_with($volume->name, $this->resource->uuid)) {
$newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid);
} else {
$newName = $new_resource->uuid.'-'.str($volume->name)->afterLast('-');
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $new_resource->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopApplication::dispatch($this->resource, false, false);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $this->resource->destination->server;
$targetServer = $new_resource->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
queue_application_deployment(
deployment_uuid: (string) new Cuid2,
application: $this->resource,
server: $sourceServer,
destination: $this->resource->destination,
no_questions_asked: true
);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
$fileStorages = $this->resource->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $new_resource->id,
]);
$newStorage->save();
}
$environmentVaribles = $this->resource->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
$newEnvironmentVariable->save();
}
$route = route('project.application.configuration', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName,
'environment_uuid' => $this->environmentUuid,
'application_uuid' => $new_resource->uuid,
]).'#resource-operations';
@@ -88,7 +214,11 @@ class ResourceOperations extends Component
$this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
) {
$uuid = (string) new Cuid2;
$new_resource = $this->resource->replicate()->fill([
$new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'status' => 'exited',
@@ -96,52 +226,255 @@ class ResourceOperations extends Component
'destination_id' => $new_destination->id,
]);
$new_resource->save();
$tags = $this->resource->tags;
foreach ($tags as $tag) {
$new_resource->tags()->attach($tag->id);
}
$new_resource->persistentStorages()->delete();
$persistentVolumes = $this->resource->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$originalName = $volume->name;
$newName = '';
if (str_starts_with($originalName, 'postgres-data-')) {
$newName = 'postgres-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mysql-data-')) {
$newName = 'mysql-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'redis-data-')) {
$newName = 'redis-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'clickhouse-data-')) {
$newName = 'clickhouse-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mariadb-data-')) {
$newName = 'mariadb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mongodb-data-')) {
$newName = 'mongodb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'keydb-data-')) {
$newName = 'keydb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'dragonfly-data-')) {
$newName = 'dragonfly-data-'.$new_resource->uuid;
} else {
if (str_starts_with($volume->name, $this->resource->uuid)) {
$newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid);
} else {
$newName = $new_resource->uuid.'-'.$volume->name;
}
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $new_resource->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopDatabase::dispatch($this->resource);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $this->resource->destination->server;
$targetServer = $new_resource->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartDatabase::dispatch($this->resource);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
$fileStorages = $this->resource->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $new_resource->id,
]);
$newStorage->save();
}
$scheduledBackups = $this->resource->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2;
$newBackup = $backup->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'database_id' => $new_resource->id,
'database_type' => $new_resource->getMorphClass(),
'team_id' => currentTeam()->id,
]);
$newBackup->save();
}
$environmentVaribles = $this->resource->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [];
if ($this->resource->type() === 'standalone-postgresql') {
$payload['standalone_postgresql_id'] = $new_resource->id;
} elseif ($this->resource->type() === 'standalone-redis') {
$payload['standalone_redis_id'] = $new_resource->id;
} elseif ($this->resource->type() === 'standalone-mongodb') {
$payload['standalone_mongodb_id'] = $new_resource->id;
} elseif ($this->resource->type() === 'standalone-mysql') {
$payload['standalone_mysql_id'] = $new_resource->id;
} elseif ($this->resource->type() === 'standalone-mariadb') {
$payload['standalone_mariadb_id'] = $new_resource->id;
}
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
$payload = [
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
];
$newEnvironmentVariable = $environmentVarible->replicate([
'id',
'created_at',
'updated_at',
])->fill($payload);
$newEnvironmentVariable->save();
}
$route = route('project.database.configuration', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName,
'environment_uuid' => $this->environmentUuid,
'database_uuid' => $new_resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
} elseif ($this->resource->type() === 'service') {
$uuid = (string) new Cuid2;
$new_resource = $this->resource->replicate()->fill([
$new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'destination_id' => $new_destination->id,
'destination_type' => $new_destination->getMorphClass(),
'server_id' => $new_destination->server_id, // server_id is probably not needed anymore because of the new polymorphic relationships (here it is needed for clone to a different server to work - but maybe we can drop the column)
]);
$new_resource->save();
$tags = $this->resource->tags;
foreach ($tags as $tag) {
$new_resource->tags()->attach($tag->id);
}
$scheduledTasks = $this->resource->scheduled_tasks()->get();
foreach ($scheduledTasks as $task) {
$newTask = $task->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => (string) new Cuid2,
'service_id' => $new_resource->id,
'team_id' => currentTeam()->id,
]);
$newTask->save();
}
$environmentVariables = $this->resource->environment_variables()->get();
foreach ($environmentVariables as $environmentVariable) {
$newEnvironmentVariable = $environmentVariable->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
$newEnvironmentVariable->save();
}
foreach ($new_resource->applications() as $application) {
$application->update([
'status' => 'exited',
]);
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newName = '';
if (str_starts_with($volume->name, $volume->resource->uuid)) {
$newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid);
} else {
$newName = $application->uuid.'-'.str($volume->name)->afterLast('-');
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $application->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopService::dispatch($application, false, false);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $application->service->destination->server;
$targetServer = $new_resource->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartService::dispatch($application);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
}
foreach ($new_resource->databases() as $database) {
$database->update([
'status' => 'exited',
]);
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newName = '';
if (str_starts_with($volume->name, $volume->resource->uuid)) {
$newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid);
} else {
$newName = $database->uuid.'-'.str($volume->name)->afterLast('-');
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $database->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopService::dispatch($database->service, false, false);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $database->service->destination->server;
$targetServer = $new_resource->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartService::dispatch($database->service);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
}
$new_resource->parse();
$route = route('project.service.configuration', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName,
'environment_uuid' => $this->environmentUuid,
'service_uuid' => $new_resource->uuid,
]).'#resource-operations';
@@ -159,7 +492,7 @@ class ResourceOperations extends Component
if ($this->resource->type() === 'application') {
$route = route('project.application.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_name' => $new_environment->name,
'environment_uuid' => $new_environment->uuid,
'application_uuid' => $this->resource->uuid,
]).'#resource-operations';
@@ -167,7 +500,7 @@ class ResourceOperations extends Component
} elseif (str($this->resource->type())->startsWith('standalone-')) {
$route = route('project.database.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_name' => $new_environment->name,
'environment_uuid' => $new_environment->uuid,
'database_uuid' => $this->resource->uuid,
]).'#resource-operations';
@@ -175,7 +508,7 @@ class ResourceOperations extends Component
} elseif ($this->resource->type() === 'service') {
$route = route('project.service.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_name' => $new_environment->name,
'environment_uuid' => $new_environment->uuid,
'service_uuid' => $this->resource->uuid,
]).'#resource-operations';

View File

@@ -46,7 +46,7 @@ class Show extends Component
#[Locked]
public string $task_uuid;
public function mount(string $task_uuid, string $project_uuid, string $environment_name, ?string $application_uuid = null, ?string $service_uuid = null)
public function mount(string $task_uuid, string $project_uuid, string $environment_uuid, ?string $application_uuid = null, ?string $service_uuid = null)
{
try {
$this->task_uuid = $task_uuid;
@@ -60,7 +60,7 @@ class Show extends Component
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $service_uuid)->firstOrFail();
}
$this->parameters = [
'environment_name' => $environment_name,
'environment_uuid' => $environment_uuid,
'project_uuid' => $project_uuid,
'application_uuid' => $application_uuid,
'service_uuid' => $service_uuid,

View File

@@ -81,11 +81,18 @@ class Add extends Component
'file_storage_path' => 'string',
'file_storage_content' => 'nullable|string',
]);
$this->file_storage_path = trim($this->file_storage_path);
$this->file_storage_path = str($this->file_storage_path)->start('/')->value();
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
$fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path;
} elseif (str($this->resource->getMorphClass())->contains('Standalone')) {
$fs_path = database_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path;
} else {
throw new \Exception('No valid resource type for file mount storage type!');
}
LocalFileVolume::create(
[
'fs_path' => $fs_path,
@@ -109,10 +116,12 @@ class Add extends Component
'file_storage_directory_source' => 'string',
'file_storage_directory_destination' => 'string',
]);
$this->file_storage_directory_source = trim($this->file_storage_directory_source);
$this->file_storage_directory_source = str($this->file_storage_directory_source)->start('/')->value();
$this->file_storage_directory_destination = trim($this->file_storage_directory_destination);
$this->file_storage_directory_destination = str($this->file_storage_directory_destination)->start('/')->value();
LocalFileVolume::create(
[
'fs_path' => $this->file_storage_directory_source,

View File

@@ -6,6 +6,7 @@ use App\Models\Environment;
use App\Models\Project;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Show extends Component
{
@@ -33,17 +34,26 @@ class Show extends Component
$environment = Environment::create([
'name' => $this->name,
'project_id' => $this->project->id,
'uuid' => (string) new Cuid2,
]);
return redirect()->route('project.resource.index', [
'project_uuid' => $this->project->uuid,
'environment_name' => $environment->name,
'environment_uuid' => $environment->uuid,
]);
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function navigateToEnvironment($projectUuid, $environmentUuid)
{
return redirect()->route('project.resource.index', [
'project_uuid' => $projectUuid,
'environment_uuid' => $environmentUuid,
]);
}
public function render()
{
return view('livewire.project.show');

View File

@@ -13,14 +13,11 @@ class Advanced extends Component
public array $parameters = [];
#[Validate(['integer', 'min:1'])]
public int $concurrentBuilds = 1;
#[Validate(['string'])]
public string $serverDiskUsageCheckFrequency = '0 23 * * *';
#[Validate(['integer', 'min:1'])]
public int $dynamicTimeout = 1;
#[Validate('boolean')]
public bool $forceDockerCleanup = false;
#[Validate(['integer', 'min:1', 'max:99'])]
public int $serverDiskUsageNotificationThreshold = 50;
#[Validate(['string', 'required'])]
public string $dockerCleanupFrequency = '*/10 * * * *';
@@ -28,8 +25,8 @@ class Advanced extends Component
#[Validate(['integer', 'min:1', 'max:99'])]
public int $dockerCleanupThreshold = 10;
#[Validate(['integer', 'min:1', 'max:99'])]
public int $serverDiskUsageNotificationThreshold = 50;
#[Validate('boolean')]
public bool $forceDockerCleanup = false;
#[Validate('boolean')]
public bool $deleteUnusedVolumes = false;
@@ -37,6 +34,12 @@ class Advanced extends Component
#[Validate('boolean')]
public bool $deleteUnusedNetworks = false;
#[Validate(['integer', 'min:1'])]
public int $concurrentBuilds = 1;
#[Validate(['integer', 'min:1'])]
public int $dynamicTimeout = 1;
public function mount(string $server_uuid)
{
try {
@@ -60,6 +63,7 @@ class Advanced extends Component
$this->server->settings->server_disk_usage_notification_threshold = $this->serverDiskUsageNotificationThreshold;
$this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes;
$this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks;
$this->server->settings->server_disk_usage_check_frequency = $this->serverDiskUsageCheckFrequency;
$this->server->settings->save();
} else {
$this->concurrentBuilds = $this->server->settings->concurrent_builds;
@@ -70,6 +74,7 @@ class Advanced extends Component
$this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold;
$this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes;
$this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks;
$this->serverDiskUsageCheckFrequency = $this->server->settings->server_disk_usage_check_frequency;
}
}
@@ -100,6 +105,10 @@ class Advanced extends Component
$this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency');
throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.');
}
if (! validate_cron_expression($this->serverDiskUsageCheckFrequency)) {
$this->serverDiskUsageCheckFrequency = $this->server->settings->getOriginal('server_disk_usage_check_frequency');
throw new \Exception('Invalid Cron / Human expression for Disk Usage Check Frequency.');
}
$this->syncData(true);
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {

View File

@@ -226,16 +226,18 @@ class Index extends Component
}
}
public function toggleTwoStepConfirmation($password)
public function toggleTwoStepConfirmation($password): bool
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
return false;
}
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true;
$this->settings->save();
$this->dispatch('success', 'Two step confirmation has been disabled.');
return true;
}
}

View File

@@ -42,8 +42,8 @@ class Show extends Component
public function mount()
{
$this->parameters = get_route_parameters();
$this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->first();
$this->environment = $this->project->environments()->where('name', request()->route('environment_name'))->first();
$this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->firstOrFail();
$this->environment = $this->project->environments()->where('uuid', request()->route('environment_uuid'))->firstOrFail();
}
public function render()

View File

@@ -130,14 +130,14 @@ class Change extends Component
} else {
$parameters = data_get(session('from'), 'parameters');
$back = data_get(session('from'), 'back');
$environment_name = data_get($parameters, 'environment_name');
$environment_uuid = data_get($parameters, 'environment_uuid');
$project_uuid = data_get($parameters, 'project_uuid');
$type = data_get($parameters, 'type');
$destination = data_get($parameters, 'destination');
session()->forget('from');
return redirect()->route($back, [
'environment_name' => $environment_name,
'environment_uuid' => $environment_uuid,
'project_uuid' => $project_uuid,
'type' => $type,
'destination' => $destination,

View File

@@ -325,7 +325,7 @@ class Application extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.application.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'application_uuid' => data_get($this, 'uuid'),
]);
}
@@ -338,7 +338,7 @@ class Application extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
$route = route('project.application.scheduled-tasks', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid,
]);
@@ -610,7 +610,7 @@ class Application extends BaseModel
},
get: function ($value) {
if ($this->additional_servers->count() === 0) {
//running (healthy)
// running (healthy)
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
@@ -695,46 +695,62 @@ class Application extends BaseModel
return $this->settings->is_static ? [80] : $this->ports_exposes_array;
}
public function environment_variables(): HasMany
public function environment_variables()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', false)
->orderBy('key', 'asc');
}
public function runtime_environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->where('key', 'not like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', false)
->where('key', 'not like', 'NIXPACKS_%');
}
// Preview Deployments
public function build_environment_variables(): HasMany
public function build_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->where('is_build_time', true)->where('key', 'not like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', false)
->where('is_build_time', true)
->where('key', 'not like', 'NIXPACKS_%');
}
public function nixpacks_environment_variables(): HasMany
public function nixpacks_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->where('key', 'like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', false)
->where('key', 'like', 'NIXPACKS_%');
}
public function environment_variables_preview(): HasMany
public function environment_variables_preview()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', true)
->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
public function runtime_environment_variables_preview(): HasMany
public function runtime_environment_variables_preview()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('key', 'not like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', true)
->where('key', 'not like', 'NIXPACKS_%');
}
public function build_environment_variables_preview(): HasMany
public function build_environment_variables_preview()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('is_build_time', true)->where('key', 'not like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', true)
->where('is_build_time', true)
->where('key', 'not like', 'NIXPACKS_%');
}
public function nixpacks_environment_variables_preview(): HasMany
public function nixpacks_environment_variables_preview()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('key', 'like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', true)
->where('key', 'like', 'NIXPACKS_%');
}
public function scheduled_tasks(): HasMany

View File

@@ -70,6 +70,11 @@ class ApplicationDeploymentQueue extends Model
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
}
public function getHorizonJobStatus()
{
return getJobStatus($this->horizon_job_id);
}
public function commitMessage()
{
if (empty($this->commit_message) || is_null($this->commit_message)) {

View File

@@ -3,7 +3,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
#[OA\Schema(
@@ -18,7 +17,7 @@ use OpenApi\Attributes as OA;
'description' => ['type' => 'string'],
]
)]
class Environment extends Model
class Environment extends BaseModel
{
protected $guarded = [];

View File

@@ -14,9 +14,8 @@ use Visus\Cuid2\Cuid2;
properties: [
'id' => ['type' => 'integer'],
'uuid' => ['type' => 'string'],
'application_id' => ['type' => 'integer'],
'service_id' => ['type' => 'integer'],
'database_id' => ['type' => 'integer'],
'resourceable_type' => ['type' => 'string'],
'resourceable_id' => ['type' => 'integer'],
'is_build_time' => ['type' => 'boolean'],
'is_literal' => ['type' => 'boolean'],
'is_multiline' => ['type' => 'boolean'],
@@ -42,6 +41,8 @@ class EnvironmentVariable extends Model
'is_multiline' => 'boolean',
'is_preview' => 'boolean',
'version' => 'string',
'resourceable_type' => 'string',
'resourceable_id' => 'integer',
];
protected $appends = ['real_value', 'is_shared', 'is_really_required'];
@@ -53,18 +54,25 @@ class EnvironmentVariable extends Model
$model->uuid = (string) new Cuid2;
}
});
static::created(function (EnvironmentVariable $environment_variable) {
if ($environment_variable->application_id && ! $environment_variable->is_preview) {
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first();
if ($environment_variable->resourceable_type === Application::class && ! $environment_variable->is_preview) {
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)
->where('resourceable_type', Application::class)
->where('resourceable_id', $environment_variable->resourceable_id)
->where('is_preview', true)
->first();
if (! $found) {
$application = Application::find($environment_variable->application_id);
if ($application->build_pack !== 'dockerfile') {
$application = Application::find($environment_variable->resourceable_id);
if ($application && $application->build_pack !== 'dockerfile') {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'is_multiline' => $environment_variable->is_multiline ?? false,
'application_id' => $environment_variable->application_id,
'resourceable_type' => Application::class,
'resourceable_id' => $environment_variable->resourceable_id,
'is_preview' => true,
]);
}
@@ -74,6 +82,7 @@ class EnvironmentVariable extends Model
'version' => config('constants.coolify.version'),
]);
});
static::saving(function (EnvironmentVariable $environmentVariable) {
$environmentVariable->updateIsShared();
});
@@ -92,43 +101,32 @@ class EnvironmentVariable extends Model
);
}
/**
* Get the parent resourceable model.
*/
public function resourceable()
{
return $this->morphTo();
}
public function resource()
{
$resource = null;
if ($this->application_id) {
$resource = Application::find($this->application_id);
} elseif ($this->service_id) {
$resource = Service::find($this->service_id);
} elseif ($this->standalone_postgresql_id) {
$resource = StandalonePostgresql::find($this->standalone_postgresql_id);
} elseif ($this->standalone_redis_id) {
$resource = StandaloneRedis::find($this->standalone_redis_id);
} elseif ($this->standalone_mongodb_id) {
$resource = StandaloneMongodb::find($this->standalone_mongodb_id);
} elseif ($this->standalone_mysql_id) {
$resource = StandaloneMysql::find($this->standalone_mysql_id);
} elseif ($this->standalone_mariadb_id) {
$resource = StandaloneMariadb::find($this->standalone_mariadb_id);
} elseif ($this->standalone_keydb_id) {
$resource = StandaloneKeydb::find($this->standalone_keydb_id);
} elseif ($this->standalone_dragonfly_id) {
$resource = StandaloneDragonfly::find($this->standalone_dragonfly_id);
} elseif ($this->standalone_clickhouse_id) {
$resource = StandaloneClickhouse::find($this->standalone_clickhouse_id);
}
return $resource;
return $this->resourceable;
}
public function realValue(): Attribute
{
$resource = $this->resource();
return Attribute::make(
get: function () use ($resource) {
$env = $this->get_real_environment_variables($this->value, $resource);
get: function () {
if (! $this->relationLoaded('resourceable')) {
$this->load('resourceable');
}
$resource = $this->resourceable;
if (! $resource) {
return null;
}
return data_get($env, 'value', $env);
return $this->get_real_environment_variables($this->value, $resource);
}
);
}
@@ -164,7 +162,6 @@ class EnvironmentVariable extends Model
if ($sharedEnvsFound->isEmpty()) {
return $environment_variable;
}
foreach ($sharedEnvsFound as $sharedEnv) {
$type = str($sharedEnv)->match('/(.*?)\./');
if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
description: 'Project model',
@@ -24,8 +25,6 @@ class Project extends BaseModel
{
protected $guarded = [];
protected $appends = ['default_environment'];
public static function ownedByCurrentTeam()
{
return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)');
@@ -40,6 +39,7 @@ class Project extends BaseModel
Environment::create([
'name' => 'production',
'project_id' => $project->id,
'uuid' => (string) new Cuid2,
]);
});
static::deleting(function ($project) {
@@ -140,18 +140,4 @@ class Project extends BaseModel
{
return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
}
public function getDefaultEnvironmentAttribute()
{
$default = $this->environments()->where('name', 'production')->first();
if ($default) {
return $default->name;
}
$default = $this->environments()->get();
if ($default->count() > 0) {
return $default->sortBy('created_at')->first()->name;
}
return null;
}
}

View File

@@ -54,6 +54,8 @@ class Server extends BaseModel
public static $batch_counter = 0;
protected $appends = ['is_coolify_host'];
protected static function booted()
{
static::saving(function ($server) {
@@ -156,6 +158,15 @@ class Server extends BaseModel
return 'server';
}
protected function isCoolifyHost(): Attribute
{
return Attribute::make(
get: function () {
return $this->id === 0;
}
);
}
public static function isReachable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
@@ -656,9 +667,9 @@ $schema://$host {
$containers = collect([]);
$containerReplicates = collect([]);
if ($this->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false);
$containers = instant_remote_process_with_timeout(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false);
$containers = format_docker_command_output_to_json($containers);
$containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this, false);
$containerReplicates = instant_remote_process_with_timeout(["docker service ls --format '{{json .}}'"], $this, false);
if ($containerReplicates) {
$containerReplicates = format_docker_command_output_to_json($containerReplicates);
foreach ($containerReplicates as $containerReplica) {
@@ -682,7 +693,7 @@ $schema://$host {
}
}
} else {
$containers = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false);
$containers = instant_remote_process_with_timeout(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false);
$containers = format_docker_command_output_to_json($containers);
$containerReplicates = collect([]);
}

View File

@@ -1050,10 +1050,11 @@ class Service extends BaseModel
$fields->put('MySQL', $data->toArray());
break;
case $image->contains('mariadb'):
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD'];
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA', 'MYSQL_DATABASE'];
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
@@ -1102,6 +1103,23 @@ class Service extends BaseModel
break;
}
}
$fields = collect($fields)->map(function ($extraFields) {
if (is_array($extraFields)) {
$extraFields = collect($extraFields)->map(function ($field) {
if (filled($field['value']) && str($field['value'])->startsWith('$SERVICE_')) {
$searchValue = str($field['value'])->after('$')->value;
$newValue = $this->environment_variables()->where('key', $searchValue)->first();
if ($newValue) {
$field['value'] = $newValue->value;
}
}
return $field;
});
}
return $extraFields;
});
return $fields;
}
@@ -1120,7 +1138,8 @@ class Service extends BaseModel
'key' => $key,
'value' => $value,
'is_build_time' => false,
'service_id' => $this->id,
'resourceable_id' => $this->id,
'resourceable_type' => $this->getMorphClass(),
'is_preview' => false,
]);
}
@@ -1132,7 +1151,7 @@ class Service extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.service.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'service_uuid' => data_get($this, 'uuid'),
]);
}
@@ -1145,7 +1164,7 @@ class Service extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
$route = route('project.service.scheduled-tasks', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'service_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid,
]);
@@ -1232,14 +1251,17 @@ class Service extends BaseModel
return $this->hasMany(ScheduledTask::class)->orderBy('name', 'asc');
}
public function environment_variables(): HasMany
public function environment_variables()
{
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
public function environment_variables_preview(): HasMany
public function environment_variables_preview()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', true)
->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
public function workdir()

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneClickhouse extends BaseModel
@@ -169,7 +168,7 @@ class StandaloneClickhouse extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -251,14 +250,15 @@ class StandaloneClickhouse extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
public function runtime_environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()

View File

@@ -6,6 +6,19 @@ class StandaloneDocker extends BaseModel
{
protected $guarded = [];
protected static function boot()
{
parent::boot();
static::created(function ($newStandaloneDocker) {
$server = $newStandaloneDocker->server;
instant_remote_process([
"docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null",
], $server, false);
$connectProxyToDockerNetworks = connectProxyToNetworks($server);
instant_remote_process($connectProxyToDockerNetworks, $server, false);
});
}
public function applications()
{
return $this->morphMany(Application::class, 'destination');

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneDragonfly extends BaseModel
@@ -174,7 +173,7 @@ class StandaloneDragonfly extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -251,14 +250,9 @@ class StandaloneDragonfly extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -319,4 +313,10 @@ class StandaloneDragonfly extends BaseModel
{
return false;
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneKeydb extends BaseModel
@@ -174,7 +173,7 @@ class StandaloneKeydb extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -251,14 +250,9 @@ class StandaloneKeydb extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -319,4 +313,10 @@ class StandaloneKeydb extends BaseModel
{
return false;
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMariadb extends BaseModel
@@ -174,7 +173,7 @@ class StandaloneMariadb extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -251,14 +250,15 @@ class StandaloneMariadb extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
public function runtime_environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMongodb extends BaseModel
@@ -183,7 +182,7 @@ class StandaloneMongodb extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -271,14 +270,9 @@ class StandaloneMongodb extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -339,4 +333,10 @@ class StandaloneMongodb extends BaseModel
{
return true;
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMysql extends BaseModel
@@ -175,7 +174,7 @@ class StandaloneMysql extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -252,14 +251,9 @@ class StandaloneMysql extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -320,4 +314,10 @@ class StandaloneMysql extends BaseModel
{
return true;
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandalonePostgresql extends BaseModel
@@ -170,7 +169,7 @@ class StandalonePostgresql extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -252,14 +251,9 @@ class StandalonePostgresql extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -320,4 +314,10 @@ class StandalonePostgresql extends BaseModel
return $parsedCollection->toArray();
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneRedis extends BaseModel
@@ -170,7 +169,7 @@ class StandaloneRedis extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -262,14 +261,9 @@ class StandaloneRedis extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -359,4 +353,10 @@ class StandaloneRedis extends BaseModel
}
);
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -120,22 +120,10 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
return Attribute::make(
get: function () {
if (config('constants.coolify.self_hosted') || $this->id === 0) {
$subscription = 'self-hosted';
} else {
$subscription = data_get($this, 'subscription');
if (is_null($subscription)) {
$subscription = 'zero';
} else {
$subscription = $subscription->type();
}
}
if ($this->custom_server_limit) {
$serverLimit = $this->custom_server_limit;
} else {
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
return 999999999999;
}
return $serverLimit ?? 2;
return $this->custom_server_limit ?? 2;
}
);
}
@@ -259,8 +247,17 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
public function sources()
{
$sources = collect([]);
$github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get();
$gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get();
$github_apps = GithubApp::where(function ($query) {
$query->where('team_id', $this->id)
->Where('is_public', false)
->orWhere('is_system_wide', true);
})->get();
$gitlab_apps = GitlabApp::where(function ($query) {
$query->where('team_id', $this->id)
->Where('is_public', false)
->orWhere('is_system_wide', true);
})->get();
return $sources->merge($github_apps)->merge($gitlab_apps);
}

View File

@@ -22,6 +22,8 @@ class DeploymentFailed extends CustomEmailNotification
public string $project_uuid;
public string $environment_uuid;
public string $environment_name;
public ?string $deployment_url = null;
@@ -36,12 +38,13 @@ class DeploymentFailed extends CustomEmailNotification
$this->preview = $preview;
$this->application_name = data_get($application, 'name');
$this->project_uuid = data_get($application, 'environment.project.uuid');
$this->environment_uuid = data_get($application, 'environment.uuid');
$this->environment_name = data_get($application, 'environment.name');
$this->fqdn = data_get($application, 'fqdn');
if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
$this->deployment_url = base_url()."/project/{$this->project_uuid}/environments/{$this->environment_uuid}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
}
public function via(object $notifiable): array

View File

@@ -22,6 +22,8 @@ class DeploymentSuccess extends CustomEmailNotification
public string $project_uuid;
public string $environment_uuid;
public string $environment_name;
public ?string $deployment_url = null;
@@ -36,12 +38,13 @@ class DeploymentSuccess extends CustomEmailNotification
$this->preview = $preview;
$this->application_name = data_get($application, 'name');
$this->project_uuid = data_get($application, 'environment.project.uuid');
$this->environment_uuid = data_get($application, 'environment.uuid');
$this->environment_name = data_get($application, 'environment.name');
$this->fqdn = data_get($application, 'fqdn');
if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
$this->deployment_url = base_url()."/project/{$this->project_uuid}/environments/{$this->environment_uuid}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
}
public function via(object $notifiable): array
@@ -144,7 +147,7 @@ class DeploymentSuccess extends CustomEmailNotification
{
if ($this->preview) {
$title = "Pull request #{$this->preview->pull_request_id} successfully deployed";
$message = 'New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '';
$message = 'New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.'';
if ($this->preview->fqdn) {
$buttons[] = [
'text' => 'Open Application',
@@ -153,7 +156,7 @@ class DeploymentSuccess extends CustomEmailNotification
}
} else {
$title = 'New version successfully deployed';
$message = 'New version successfully deployed of ' . $this->application_name . '';
$message = 'New version successfully deployed of '.$this->application_name.'';
if ($this->fqdn) {
$buttons[] = [
'text' => 'Open Application',

View File

@@ -15,6 +15,8 @@ class StatusChanged extends CustomEmailNotification
public string $project_uuid;
public string $environment_uuid;
public string $environment_name;
public ?string $resource_url = null;
@@ -26,12 +28,13 @@ class StatusChanged extends CustomEmailNotification
$this->onQueue('high');
$this->resource_name = data_get($resource, 'name');
$this->project_uuid = data_get($resource, 'environment.project.uuid');
$this->environment_uuid = data_get($resource, 'environment.uuid');
$this->environment_name = data_get($resource, 'environment.name');
$this->fqdn = data_get($resource, 'fqdn', null);
if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->resource_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->resource->uuid}";
$this->resource_url = base_url()."/project/{$this->project_uuid}/environments/{$this->environment_uuid}/application/{$this->resource->uuid}";
}
public function via(object $notifiable): array
@@ -80,7 +83,7 @@ class StatusChanged extends CustomEmailNotification
public function toPushover(): PushoverMessage
{
$message = $this->resource_name . ' has been stopped.';
$message = $this->resource_name.' has been stopped.';
return new PushoverMessage(
title: 'Application stopped',

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