Merge branch 'next' into docker-network-aliases
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
@@ -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',
|
||||
|
@@ -22,70 +22,27 @@ class StartDatabaseProxy
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
|
||||
{
|
||||
$internalPort = null;
|
||||
$type = $database->getMorphClass();
|
||||
$databaseType = $database->database_type;
|
||||
$network = data_get($database, 'destination.network');
|
||||
$server = data_get($database, 'destination.server');
|
||||
$containerName = data_get($database, 'uuid');
|
||||
$proxyContainerName = "{$database->uuid}-proxy";
|
||||
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
|
||||
$databaseType = $database->databaseType();
|
||||
// $connectPredefined = data_get($database, 'service.connect_to_docker_network');
|
||||
$network = $database->service->uuid;
|
||||
$server = data_get($database, 'service.destination.server');
|
||||
$proxyContainerName = "{$database->service->uuid}-proxy";
|
||||
switch ($databaseType) {
|
||||
case 'standalone-mariadb':
|
||||
$type = \App\Models\StandaloneMariadb::class;
|
||||
$containerName = "mariadb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
$type = \App\Models\StandaloneMongodb::class;
|
||||
$containerName = "mongodb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
$type = \App\Models\StandaloneMysql::class;
|
||||
$containerName = "mysql-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
$type = \App\Models\StandalonePostgresql::class;
|
||||
$containerName = "postgresql-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
$type = \App\Models\StandaloneRedis::class;
|
||||
$containerName = "redis-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-keydb':
|
||||
$type = \App\Models\StandaloneKeydb::class;
|
||||
$containerName = "keydb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-dragonfly':
|
||||
$type = \App\Models\StandaloneDragonfly::class;
|
||||
$containerName = "dragonfly-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-clickhouse':
|
||||
$type = \App\Models\StandaloneClickhouse::class;
|
||||
$containerName = "clickhouse-{$database->service->uuid}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($type === \App\Models\StandaloneRedis::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === \App\Models\StandalonePostgresql::class) {
|
||||
$internalPort = 5432;
|
||||
} elseif ($type === \App\Models\StandaloneMongodb::class) {
|
||||
$internalPort = 27017;
|
||||
} elseif ($type === \App\Models\StandaloneMysql::class) {
|
||||
$internalPort = 3306;
|
||||
} elseif ($type === \App\Models\StandaloneMariadb::class) {
|
||||
$internalPort = 3306;
|
||||
} elseif ($type === \App\Models\StandaloneKeydb::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === \App\Models\StandaloneDragonfly::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === \App\Models\StandaloneClickhouse::class) {
|
||||
$internalPort = 9000;
|
||||
$containerName = "{$database->name}-{$database->service->uuid}";
|
||||
}
|
||||
$internalPort = match ($databaseType) {
|
||||
'standalone-mariadb', 'standalone-mysql' => 3306,
|
||||
'standalone-postgresql', 'standalone-supabase/postgres' => 5432,
|
||||
'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6379,
|
||||
'standalone-clickhouse' => 9000,
|
||||
'standalone-mongodb' => 27017,
|
||||
default => throw new \Exception("Unsupported database type: $databaseType"),
|
||||
};
|
||||
|
||||
$configuration_dir = database_proxy_dir($database->uuid);
|
||||
$nginxconf = <<<EOF
|
||||
user nginx;
|
||||
|
@@ -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',
|
||||
|
@@ -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',
|
||||
|
@@ -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',
|
||||
|
@@ -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',
|
||||
|
@@ -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',
|
||||
|
@@ -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";
|
||||
}
|
||||
}
|
||||
|
@@ -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',
|
||||
|
@@ -30,7 +30,6 @@ class StopDatabaseProxy
|
||||
}
|
||||
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
|
||||
|
||||
$database->is_public = false;
|
||||
$database->save();
|
||||
|
||||
DatabaseProxyStopped::dispatch();
|
||||
|
@@ -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 {
|
||||
@@ -208,7 +208,6 @@ class GetContainersStatus
|
||||
$foundServices[] = "$service->id-$service->name";
|
||||
$statusFromDb = $service->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
// ray('Updating status: ' . $containerStatus);
|
||||
$service->update(['status' => $containerStatus]);
|
||||
} else {
|
||||
$service->update(['last_online_at' => now()]);
|
||||
|
@@ -28,13 +28,13 @@ class StartProxy
|
||||
$docker_compose_yml_base64 = base64_encode($configuration);
|
||||
$server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
|
||||
$server->save();
|
||||
if ($server->isSwarm()) {
|
||||
if ($server->isSwarmManager()) {
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
"cd $proxy_path",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker stack deploy -c docker-compose.yml coolify-proxy',
|
||||
'docker stack deploy --detach=true -c docker-compose.yml coolify-proxy',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
} else {
|
||||
@@ -57,7 +57,7 @@ class StartProxy
|
||||
" echo 'Successfully stopped and removed existing coolify-proxy.'",
|
||||
'fi',
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d --remove-orphans',
|
||||
'docker compose up -d',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
$commands = $commands->merge(connectProxyToNetworks($server));
|
||||
|
@@ -25,17 +25,25 @@ class CleanupDocker
|
||||
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
|
||||
];
|
||||
|
||||
$serverSettings = $server->settings;
|
||||
if ($serverSettings->delete_unused_volumes) {
|
||||
if ($server->settings->delete_unused_volumes) {
|
||||
$commands[] = 'docker volume prune -af';
|
||||
}
|
||||
|
||||
if ($serverSettings->delete_unused_networks) {
|
||||
if ($server->settings->delete_unused_networks) {
|
||||
$commands[] = 'docker network prune -f';
|
||||
}
|
||||
|
||||
$cleanupLog = [];
|
||||
foreach ($commands as $command) {
|
||||
instant_remote_process([$command], $server, false);
|
||||
$commandOutput = instant_remote_process([$command], $server, false);
|
||||
if ($commandOutput !== null) {
|
||||
$cleanupLog[] = [
|
||||
'command' => $command,
|
||||
'output' => $commandOutput,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $cleanupLog;
|
||||
}
|
||||
}
|
||||
|
@@ -12,19 +12,24 @@ class StartService
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Service $service)
|
||||
public function handle(Service $service, bool $pullLatestImages = false, bool $stopBeforeStart = false)
|
||||
{
|
||||
$service->parse();
|
||||
if ($stopBeforeStart) {
|
||||
StopService::run(service: $service, dockerCleanup: false);
|
||||
}
|
||||
$service->saveComposeConfigs();
|
||||
$commands[] = 'cd '.$service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
if ($pullLatestImages) {
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = 'docker compose pull';
|
||||
}
|
||||
if ($service->networks()->count() > 0) {
|
||||
$commands[] = "echo 'Creating Docker network.'";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
||||
}
|
||||
$commands[] = 'echo Starting service.';
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = 'docker compose pull';
|
||||
$commands[] = "echo 'Starting containers.'";
|
||||
$commands[] = 'docker compose up -d --remove-orphans --force-recreate --build';
|
||||
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
|
||||
if (data_get($service, 'connect_to_docker_network')) {
|
||||
|
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Shared;
|
||||
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class PullImage
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $resource)
|
||||
{
|
||||
$resource->saveComposeConfigs();
|
||||
|
||||
$commands[] = 'cd '.$resource->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$resource->workdir()}.'";
|
||||
$commands[] = 'docker compose pull';
|
||||
|
||||
$server = data_get($resource, 'server');
|
||||
|
||||
if (! $server) {
|
||||
return;
|
||||
}
|
||||
|
||||
instant_remote_process($commands, $resource->server);
|
||||
}
|
||||
}
|
@@ -57,6 +57,14 @@ class CleanupDatabase extends Command
|
||||
$application_deployment_queues->delete();
|
||||
}
|
||||
|
||||
// Cleanup scheduled_task_executions table
|
||||
$scheduled_task_executions = DB::table('scheduled_task_executions')->where('created_at', '<', now()->subDays($keep_days))->orderBy('created_at', 'desc');
|
||||
$count = $scheduled_task_executions->count();
|
||||
echo "Delete $count entries from scheduled_task_executions.\n";
|
||||
if ($this->option('yes')) {
|
||||
$scheduled_task_executions->delete();
|
||||
}
|
||||
|
||||
// Cleanup webhooks table
|
||||
$webhooks = DB::table('webhooks')->where('created_at', '<', now()->subDays($keep_days));
|
||||
$count = $webhooks->count();
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -50,7 +50,7 @@ class CloudCleanupSubscriptions extends Command
|
||||
} else {
|
||||
$subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []);
|
||||
$status = data_get($subscription, 'status');
|
||||
if ($status === 'active' || $status === 'past_due') {
|
||||
if ($status === 'active') {
|
||||
$team->subscription->update([
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_trial_already_ended' => false,
|
||||
|
@@ -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':
|
||||
|
178
app/Console/Commands/HorizonManage.php
Normal file
178
app/Console/Commands/HorizonManage.php
Normal 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);
|
||||
}
|
||||
}
|
@@ -35,8 +35,7 @@ class Init extends Command
|
||||
}
|
||||
|
||||
$this->servers = Server::all();
|
||||
if (isCloud()) {
|
||||
} else {
|
||||
if (! isCloud()) {
|
||||
$this->send_alive_signal();
|
||||
get_public_ips();
|
||||
}
|
||||
@@ -88,8 +87,10 @@ class Init extends Command
|
||||
$settings = instanceSettings();
|
||||
if (! is_null(config('constants.coolify.autoupdate', null))) {
|
||||
if (config('constants.coolify.autoupdate') == true) {
|
||||
echo "Enabling auto-update\n";
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
echo "Disabling auto-update\n";
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
}
|
||||
}
|
||||
@@ -119,7 +120,9 @@ class Init extends Command
|
||||
private function update_user_emails()
|
||||
{
|
||||
try {
|
||||
User::whereRaw('email ~ \'[A-Z]\'')->get()->each(fn (User $user) => $user->update(['email' => strtolower($user->email)]));
|
||||
User::whereRaw('email ~ \'[A-Z]\'')->get()->each(function (User $user) {
|
||||
$user->update(['email' => strtolower($user->email)]);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in updating user emails: {$e->getMessage()}\n";
|
||||
}
|
||||
@@ -200,7 +203,6 @@ class Init extends Command
|
||||
try {
|
||||
$database = StandalonePostgresql::withTrashed()->find(0);
|
||||
if ($database && $database->trashed()) {
|
||||
echo "Restoring coolify db backup\n";
|
||||
$database->restore();
|
||||
$scheduledBackup = ScheduledDatabaseBackup::find(0);
|
||||
if (! $scheduledBackup) {
|
||||
|
@@ -6,13 +6,11 @@ use App\Jobs\CheckAndStartSentinelJob;
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\CleanupStaleMultiplexedConnections;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\ServerCheckJob;
|
||||
use App\Jobs\ServerCleanupMux;
|
||||
use App\Jobs\ServerStorageCheckJob;
|
||||
use App\Jobs\UpdateCoolifyJob;
|
||||
use App\Models\InstanceSettings;
|
||||
@@ -23,6 +21,7 @@ use App\Models\Team;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
@@ -91,12 +90,22 @@ 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) {
|
||||
CheckAndStartSentinelJob::dispatch($server);
|
||||
})->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
try {
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$this->scheduleInstance->job(function () use ($server) {
|
||||
CheckAndStartSentinelJob::dispatch($server);
|
||||
})->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error pulling images: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
$this->scheduleInstance->job(new CheckHelperImageJob)
|
||||
@@ -124,7 +133,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 {
|
||||
@@ -132,39 +141,47 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
|
||||
foreach ($servers as $server) {
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
|
||||
// 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
|
||||
try {
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
if (isCloud()) {
|
||||
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
|
||||
} else {
|
||||
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer();
|
||||
|
||||
// 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 (isCloud()) {
|
||||
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
|
||||
} else {
|
||||
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer();
|
||||
}
|
||||
// $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer();
|
||||
|
||||
$serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) {
|
||||
$serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency];
|
||||
}
|
||||
$this->scheduleInstance->job(new ServerStorageCheckJob($server))->cron($serverDiskUsageCheckFrequency)->timezone($serverTimezone)->onOneServer();
|
||||
}
|
||||
// $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();
|
||||
}
|
||||
$dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) {
|
||||
$dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency];
|
||||
}
|
||||
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($dockerCleanupFrequency)->timezone($serverTimezone)->onOneServer();
|
||||
|
||||
// Cleanup multiplexed connections every hour
|
||||
// $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
|
||||
// Cleanup multiplexed connections every hour
|
||||
// $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
|
||||
|
||||
// Temporary solution until we have better memory management for Sentinel
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$this->scheduleInstance->job(function () use ($server) {
|
||||
$server->restartContainer('coolify-sentinel');
|
||||
})->daily()->onOneServer();
|
||||
// Temporary solution until we have better memory management for Sentinel
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$this->scheduleInstance->job(function () use ($server) {
|
||||
$server->restartContainer('coolify-sentinel');
|
||||
})->daily()->onOneServer();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error checking resources: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,25 +192,51 @@ 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();
|
||||
|
||||
if (is_null($server)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
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) {
|
||||
try {
|
||||
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 (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($serverTimezone)->onOneServer();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error scheduling backup: '.$e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
}
|
||||
$this->scheduleInstance->job(new DatabaseBackupJob(
|
||||
backup: $scheduled_backup
|
||||
))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,37 +246,60 @@ 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 (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
|
||||
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
|
||||
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) {
|
||||
try {
|
||||
$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($serverTimezone)->onOneServer();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error scheduling task: '.$e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
}
|
||||
$this->scheduleInstance->job(new ScheduledTaskJob(
|
||||
task: $scheduled_task
|
||||
))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
|
19
app/Contracts/CustomJobRepositoryInterface.php
Normal file
19
app/Contracts/CustomJobRepositoryInterface.php
Normal 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;
|
||||
}
|
24
app/Events/DockerCleanupDone.php
Normal file
24
app/Events/DockerCleanupDone.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\DockerCleanupExecution;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class DockerCleanupDone implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public function __construct(public DockerCleanupExecution $execution) {}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PrivateChannel('team.'.$this->execution->server->team->id),
|
||||
];
|
||||
}
|
||||
}
|
34
app/Events/RestoreJobFinished.php
Normal file
34
app/Events/RestoreJobFinished.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -18,6 +18,7 @@ use App\Models\Service;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -27,6 +28,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 +118,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 +190,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 +234,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 +306,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 +350,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 +422,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 +466,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 +522,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 +566,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 +619,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 +663,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 +682,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 +711,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 +725,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 +746,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 +783,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 +803,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 +812,12 @@ class ApplicationsController extends Controller
|
||||
'docker_compose_raw' => 'string|nullable',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
];
|
||||
$validationRules = array_merge($validationRules, sharedDataApplications());
|
||||
// ports_exposes is not required for dockercompose
|
||||
if ($request->build_pack === 'dockercompose') {
|
||||
$validationRules['ports_exposes'] = 'string';
|
||||
$request->offsetSet('ports_exposes', '80');
|
||||
}
|
||||
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
||||
$validator = customApiValidator($request->all(), $validationRules);
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
@@ -753,7 +825,9 @@ class ApplicationsController extends Controller
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (! $request->has('name')) {
|
||||
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
|
||||
}
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
@@ -776,7 +850,13 @@ class ApplicationsController extends Controller
|
||||
if ($dockerComposeDomainsJson->count() > 0) {
|
||||
$application->docker_compose_domains = $dockerComposeDomainsJson;
|
||||
}
|
||||
|
||||
$repository_url_parsed = Url::fromString($request->git_repository);
|
||||
$git_host = $repository_url_parsed->getHost();
|
||||
if ($git_host === 'github.com') {
|
||||
$application->source_type = GithubApp::class;
|
||||
$application->source_id = GithubApp::find(0)->id;
|
||||
}
|
||||
$application->git_repository = $repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2);
|
||||
$application->fqdn = $fqdn;
|
||||
$application->destination_id = $destination->id;
|
||||
$application->destination_type = $destination->getMorphClass();
|
||||
@@ -791,7 +871,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 +895,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 +907,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 +916,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 +966,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 +996,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 +1010,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 +1019,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 +1063,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 +1093,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 +1107,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 +1149,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 +1178,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 +1194,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 +1213,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 +1242,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 +1266,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()) {
|
||||
@@ -1216,11 +1299,6 @@ class ApplicationsController extends Controller
|
||||
$dockerCompose = base64_decode($request->docker_compose_raw);
|
||||
$dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
|
||||
|
||||
// $isValid = validateComposeFile($dockerComposeRaw, $server_id);
|
||||
// if ($isValid !== 'OK') {
|
||||
// return $this->dispatch('error', "Invalid docker-compose file.\n$isValid");
|
||||
// }
|
||||
|
||||
$service = new Service;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
$service->fill($request->all());
|
||||
@@ -1241,7 +1319,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);
|
||||
@@ -1313,6 +1391,108 @@ class ApplicationsController extends Controller
|
||||
return response()->json($this->removeSensitiveData($application));
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Get application logs.',
|
||||
description: 'Get application logs by UUID.',
|
||||
path: '/applications/{uuid}/logs',
|
||||
operationId: 'get-application-logs-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'lines',
|
||||
in: 'query',
|
||||
description: 'Number of lines to show from the end of the logs.',
|
||||
required: false,
|
||||
schema: new OA\Schema(
|
||||
type: 'integer',
|
||||
format: 'int32',
|
||||
default: 100,
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get application logs by UUID.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'logs' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function logs_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$uuid = $request->route('uuid');
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 400);
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id);
|
||||
|
||||
if ($containers->count() == 0) {
|
||||
return response()->json([
|
||||
'message' => 'Application is not running.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$container = $containers->first();
|
||||
|
||||
$status = getContainerStatus($application->destination->server, $container['Names']);
|
||||
if ($status !== 'running') {
|
||||
return response()->json([
|
||||
'message' => 'Application is not running.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$lines = $request->query->get('lines', 100) ?: 100;
|
||||
$logs = getContainerLogs($application->destination->server, $container['ID'], $lines);
|
||||
|
||||
return response()->json([
|
||||
'logs' => $logs,
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete',
|
||||
description: 'Delete application by UUID.',
|
||||
@@ -1551,7 +1731,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
|
||||
@@ -1606,7 +1786,8 @@ class ApplicationsController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$domains = $request->domains;
|
||||
if ($request->has('domains') && $server->isProxyShouldRun()) {
|
||||
$requestHasDomains = $request->has('domains');
|
||||
if ($requestHasDomains && $server->isProxyShouldRun()) {
|
||||
$uuid = $request->uuid;
|
||||
$fqdn = $request->domains;
|
||||
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
|
||||
@@ -1668,7 +1849,10 @@ class ApplicationsController extends Controller
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
|
||||
$data = $request->all();
|
||||
data_set($data, 'fqdn', $domains);
|
||||
if ($requestHasDomains && $server->isProxyShouldRun()) {
|
||||
data_set($data, 'fqdn', $domains);
|
||||
}
|
||||
|
||||
if ($dockerComposeDomainsJson->count() > 0) {
|
||||
data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson));
|
||||
}
|
||||
@@ -1893,8 +2077,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 +2106,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 +2249,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 +2271,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 +2298,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 +2328,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 +2449,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 +2466,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 +2475,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 +2489,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 +2578,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.',
|
||||
|
@@ -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',
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -195,6 +195,31 @@ class SecurityController extends Controller
|
||||
if (! $request->description) {
|
||||
$request->offsetSet('description', 'Created by Coolify via API');
|
||||
}
|
||||
|
||||
$isPrivateKeyString = str_starts_with($request->private_key, '-----BEGIN');
|
||||
if (! $isPrivateKeyString) {
|
||||
try {
|
||||
$base64PrivateKey = base64_decode($request->private_key);
|
||||
$request->offsetSet('private_key', $base64PrivateKey);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'message' => 'Invalid private key.',
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
$isPrivateKeyValid = PrivateKey::validatePrivateKey($request->private_key);
|
||||
if (! $isPrivateKeyValid) {
|
||||
return response()->json([
|
||||
'message' => 'Invalid private key.',
|
||||
], 422);
|
||||
}
|
||||
$fingerPrint = PrivateKey::generateFingerprint($request->private_key);
|
||||
$isFingerPrintExists = PrivateKey::fingerprintExists($fingerPrint);
|
||||
if ($isFingerPrintExists) {
|
||||
return response()->json([
|
||||
'message' => 'Private key already exists.',
|
||||
], 422);
|
||||
}
|
||||
$key = PrivateKey::create([
|
||||
'team_id' => $teamId,
|
||||
'name' => $request->name,
|
||||
|
@@ -530,11 +530,11 @@ class ServersController extends Controller
|
||||
'user' => $request->user,
|
||||
'private_key_id' => $privateKey->id,
|
||||
'team_id' => $teamId,
|
||||
'proxy' => [
|
||||
'type' => $proxyType,
|
||||
'status' => ProxyStatus::EXITED->value,
|
||||
],
|
||||
]);
|
||||
$server->proxy->set('type', $proxyType);
|
||||
$server->proxy->set('status', ProxyStatus::EXITED->value);
|
||||
$server->save();
|
||||
|
||||
$server->settings()->update([
|
||||
'is_build_server' => $request->is_build_server,
|
||||
]);
|
||||
@@ -742,6 +742,9 @@ class ServersController extends Controller
|
||||
if ($server->definedResources()->count() > 0) {
|
||||
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
|
||||
}
|
||||
if ($server->isLocalhost()) {
|
||||
return response()->json(['message' => 'Local server cannot be deleted.'], 400);
|
||||
}
|
||||
$server->delete();
|
||||
DeleteServer::dispatch($server);
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -54,7 +54,7 @@ class Controller extends BaseController
|
||||
'email' => Str::lower($arrayOfRequest['email']),
|
||||
]);
|
||||
$type = set_transanctional_email_settings();
|
||||
if (! $type) {
|
||||
if (blank($type)) {
|
||||
return response()->json(['message' => 'Transactional emails are not active'], 400);
|
||||
}
|
||||
$request->validate([Fortify::email() => 'required|email']);
|
||||
|
@@ -37,7 +37,7 @@ class Bitbucket extends Controller
|
||||
$headers = $request->headers->all();
|
||||
$x_bitbucket_token = data_get($headers, 'x-hub-signature.0', '');
|
||||
$x_bitbucket_event = data_get($headers, 'x-event-key.0', '');
|
||||
$handled_events = collect(['repo:push', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']);
|
||||
$handled_events = collect(['repo:push', 'pullrequest:updated', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']);
|
||||
if (! $handled_events->contains($x_bitbucket_event)) {
|
||||
return response([
|
||||
'status' => 'failed',
|
||||
@@ -48,6 +48,7 @@ class Bitbucket extends Controller
|
||||
$branch = data_get($payload, 'push.changes.0.new.name');
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
$commit = data_get($payload, 'push.changes.0.new.target.hash');
|
||||
|
||||
if (! $branch) {
|
||||
return response([
|
||||
'status' => 'failed',
|
||||
@@ -55,7 +56,7 @@ class Bitbucket extends Controller
|
||||
]);
|
||||
}
|
||||
}
|
||||
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
|
||||
if ($x_bitbucket_event === 'pullrequest:updated' || $x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
|
||||
$branch = data_get($payload, 'pullrequest.destination.branch.name');
|
||||
$base_branch = data_get($payload, 'pullrequest.source.branch.name');
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
@@ -119,7 +120,7 @@ class Bitbucket extends Controller
|
||||
]);
|
||||
}
|
||||
}
|
||||
if ($x_bitbucket_event === 'pullrequest:created') {
|
||||
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:updated') {
|
||||
if ($application->isPRDeployable()) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
|
@@ -152,7 +152,7 @@ class Gitea extends Controller
|
||||
}
|
||||
}
|
||||
if ($x_gitea_event === 'pull_request') {
|
||||
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
|
||||
if ($action === 'opened' || $action === 'synchronized' || $action === 'reopened') {
|
||||
if ($application->isPRDeployable()) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
@@ -202,7 +202,6 @@ class Gitea extends Controller
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
// ray('Stopping container: ' . $container_name);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
|
@@ -208,7 +208,6 @@ class Github extends Controller
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
// ray('Stopping container: ' . $container_name);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
|
@@ -227,7 +227,6 @@ class Gitlab extends Controller
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
// ray('Stopping container: ' . $container_name);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
|
@@ -18,6 +18,7 @@ use App\Models\SwarmDocker;
|
||||
use App\Notifications\Application\DeploymentFailed;
|
||||
use App\Notifications\Application\DeploymentSuccess;
|
||||
use App\Traits\ExecuteRemoteCommand;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
@@ -39,12 +40,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 +127,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 +167,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 +241,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.');
|
||||
@@ -250,6 +254,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Make sure the private key is stored in the filesystem
|
||||
$this->server->privateKey->storeInFileSystem();
|
||||
|
||||
// Generate custom host<->ip mapping
|
||||
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
||||
|
||||
@@ -313,6 +320,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->fail($e);
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->application_deployment_queue->update([
|
||||
'finished_at' => Carbon::now()->toImmutable(),
|
||||
]);
|
||||
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->build_server;
|
||||
} else {
|
||||
@@ -916,8 +927,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
|
||||
$envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}");
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
|
||||
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,8 +989,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
|
||||
$envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}");
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
|
||||
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1115,7 +1132,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
|
||||
$nixpacks_php_fallback_path->value = '/index.php';
|
||||
$nixpacks_php_fallback_path->is_build_time = false;
|
||||
$nixpacks_php_fallback_path->application_id = $this->application->id;
|
||||
$nixpacks_php_fallback_path->resourceable_id = $this->application->id;
|
||||
$nixpacks_php_fallback_path->resourceable_type = 'App\Models\Application';
|
||||
$nixpacks_php_fallback_path->save();
|
||||
}
|
||||
if (! $nixpacks_php_root_dir) {
|
||||
@@ -1123,7 +1141,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR';
|
||||
$nixpacks_php_root_dir->value = '/app/public';
|
||||
$nixpacks_php_root_dir->is_build_time = false;
|
||||
$nixpacks_php_root_dir->application_id = $this->application->id;
|
||||
$nixpacks_php_root_dir->resourceable_id = $this->application->id;
|
||||
$nixpacks_php_root_dir->resourceable_type = 'App\Models\Application';
|
||||
$nixpacks_php_root_dir->save();
|
||||
}
|
||||
|
||||
@@ -1136,7 +1155,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->application_deployment_queue->addLogEntry('Rolling update started.');
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}"),
|
||||
executeInDocker($this->deployment_uuid, "docker stack deploy --detach=true --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}"),
|
||||
],
|
||||
);
|
||||
$this->application_deployment_queue->addLogEntry('Rolling update completed.');
|
||||
@@ -1189,7 +1208,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->application->custom_healthcheck_found) {
|
||||
$this->application_deployment_queue->addLogEntry('Custom healthcheck found, skipping default healthcheck.');
|
||||
}
|
||||
// ray('New container name: ', $this->container_name);
|
||||
if ($this->container_name) {
|
||||
$counter = 1;
|
||||
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
|
||||
@@ -1392,7 +1410,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
continue;
|
||||
}
|
||||
// ray('Deploying to additional destination: ', $server->name);
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
deployment_uuid: $deployment_uuid,
|
||||
@@ -1405,7 +1422,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'),
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -1494,7 +1511,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
]
|
||||
);
|
||||
if ($this->saved_outputs->get('commit_message')) {
|
||||
$commit_message = str($this->saved_outputs->get('commit_message'))->limit(47);
|
||||
$commit_message = str($this->saved_outputs->get('commit_message'));
|
||||
$this->application_deployment_queue->commit_message = $commit_message->value();
|
||||
ApplicationDeploymentQueue::whereCommit($this->commit)->whereApplicationId($this->application->id)->update(
|
||||
['commit_message' => $commit_message->value()]
|
||||
@@ -1545,7 +1562,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 +1587,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 +1696,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 +1708,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) {
|
||||
@@ -2005,6 +2023,8 @@ LABEL coolify.deploymentId={$this->deployment_uuid}
|
||||
COPY . .
|
||||
RUN rm -f /usr/share/nginx/html/nginx.conf
|
||||
RUN rm -f /usr/share/nginx/html/Dockerfile
|
||||
RUN rm -f /usr/share/nginx/html/docker-compose.yaml
|
||||
RUN rm -f /usr/share/nginx/html/.env
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
|
||||
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
|
||||
@@ -2266,7 +2286,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
} else {
|
||||
if ($this->use_build_server) {
|
||||
$this->execute_remote_command(
|
||||
["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true],
|
||||
["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --pull always --build -d", 'hidden' => true],
|
||||
);
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
@@ -2279,18 +2299,18 @@ 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}");
|
||||
}
|
||||
if ($this->application->build_pack === 'nixpacks') {
|
||||
$variables = collect($this->nixpacks_plan_json->get('variables'));
|
||||
} else {
|
||||
foreach ($this->application->build_environment_variables_preview as $env) {
|
||||
$value = escapeshellarg($env->real_value);
|
||||
$this->build_args->push("--build-arg {$env->key}={$value}");
|
||||
}
|
||||
$this->generate_env_variables();
|
||||
$variables = collect([])->merge($this->env_args);
|
||||
}
|
||||
|
||||
$this->build_args = $variables->map(function ($value, $key) {
|
||||
$value = escapeshellarg($value);
|
||||
|
||||
return "--build-arg {$key}={$value}";
|
||||
});
|
||||
}
|
||||
|
||||
private function add_build_env_variables_to_dockerfile()
|
||||
@@ -2395,7 +2415,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,
|
||||
|
@@ -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';
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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,12 +322,20 @@ 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 {
|
||||
if ($this->team) {
|
||||
BackupCreated::dispatch($this->team->id);
|
||||
}
|
||||
if ($this->backup_log) {
|
||||
$this->backup_log->update([
|
||||
'finished_at' => Carbon::now()->toImmutable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,9 +346,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($databaseWithCollections === 'all') {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
}
|
||||
} else {
|
||||
if (str($databaseWithCollections)->contains(':')) {
|
||||
@@ -357,15 +361,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if ($collectionsToExclude->count() === 0) {
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $databaseName --gzip --archive > $this->backup_location";
|
||||
}
|
||||
} else {
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,9 +415,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
try {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if ($this->backup->dump_all) {
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" $database > $this->backup_location";
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
@@ -431,9 +435,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
try {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if ($this->backup->dump_all) {
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" $database > $this->backup_location";
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
@@ -460,19 +464,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 {
|
||||
@@ -504,12 +495,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
|
||||
}
|
||||
if ($this->s3->isHetzner()) {
|
||||
$endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value();
|
||||
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret";
|
||||
} else {
|
||||
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
|
||||
}
|
||||
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key \"$secret\"";
|
||||
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
|
||||
instant_remote_process($commands, $this->server);
|
||||
|
||||
|
@@ -3,9 +3,12 @@
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Events\DockerCleanupDone;
|
||||
use App\Models\DockerCleanupExecution;
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Server\DockerCleanupFailed;
|
||||
use App\Notifications\Server\DockerCleanupSuccess;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
@@ -24,6 +27,8 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public ?string $usageBefore = null;
|
||||
|
||||
public ?DockerCleanupExecution $execution_log = null;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
|
||||
@@ -38,37 +43,89 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
$this->execution_log = DockerCleanupExecution::create([
|
||||
'server_id' => $this->server->id,
|
||||
]);
|
||||
|
||||
$this->usageBefore = $this->server->getDiskUsage();
|
||||
|
||||
if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
|
||||
CleanupDocker::run(server: $this->server);
|
||||
$cleanup_log = CleanupDocker::run(server: $this->server);
|
||||
$usageAfter = $this->server->getDiskUsage();
|
||||
$this->server->team?->notify(new DockerCleanupSuccess($this->server, ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
|
||||
$message = ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.';
|
||||
|
||||
$this->execution_log->update([
|
||||
'status' => 'success',
|
||||
'message' => $message,
|
||||
'cleanup_log' => $cleanup_log,
|
||||
]);
|
||||
|
||||
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
|
||||
event(new DockerCleanupDone($this->execution_log));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
|
||||
CleanupDocker::run(server: $this->server);
|
||||
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk usage could be determined.'));
|
||||
$cleanup_log = CleanupDocker::run(server: $this->server);
|
||||
$message = 'Docker cleanup job executed successfully, but no disk usage could be determined.';
|
||||
|
||||
$this->execution_log->update([
|
||||
'status' => 'success',
|
||||
'message' => $message,
|
||||
'cleanup_log' => $cleanup_log,
|
||||
]);
|
||||
|
||||
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
|
||||
event(new DockerCleanupDone($this->execution_log));
|
||||
}
|
||||
|
||||
if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
|
||||
CleanupDocker::run(server: $this->server);
|
||||
$cleanup_log = CleanupDocker::run(server: $this->server);
|
||||
$usageAfter = $this->server->getDiskUsage();
|
||||
$diskSaved = $this->usageBefore - $usageAfter;
|
||||
|
||||
if ($diskSaved > 0) {
|
||||
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Saved '.$diskSaved.'% disk space. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
|
||||
$message = 'Saved '.$diskSaved.'% disk space. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.';
|
||||
} else {
|
||||
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk space was saved. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
|
||||
$message = 'Docker cleanup job executed successfully, but no disk space was saved. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.';
|
||||
}
|
||||
|
||||
$this->execution_log->update([
|
||||
'status' => 'success',
|
||||
'message' => $message,
|
||||
'cleanup_log' => $cleanup_log,
|
||||
]);
|
||||
|
||||
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
|
||||
event(new DockerCleanupDone($this->execution_log));
|
||||
} else {
|
||||
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'No cleanup needed for '.$this->server->name));
|
||||
$message = 'No cleanup needed for '.$this->server->name;
|
||||
|
||||
$this->execution_log->update([
|
||||
'status' => 'success',
|
||||
'message' => $message,
|
||||
]);
|
||||
|
||||
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
|
||||
event(new DockerCleanupDone($this->execution_log));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->execution_log) {
|
||||
$this->execution_log->update([
|
||||
'status' => 'failed',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
event(new DockerCleanupDone($this->execution_log));
|
||||
}
|
||||
$this->server->team?->notify(new DockerCleanupFailed($this->server, 'Docker cleanup job failed with the following error: '.$e->getMessage()));
|
||||
throw $e;
|
||||
} finally {
|
||||
if ($this->execution_log) {
|
||||
$this->execution_log->update([
|
||||
'finished_at' => Carbon::now()->toImmutable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -27,19 +27,28 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$github_access_token = generate_github_jwt_token($this->github_app);
|
||||
$github_access_token = generateGithubJwt($this->github_app);
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => "Bearer $github_access_token",
|
||||
'Accept' => 'application/vnd.github+json',
|
||||
])->get("{$this->github_app->api_url}/app");
|
||||
|
||||
if (! $response->successful()) {
|
||||
throw new \RuntimeException('Failed to fetch GitHub app permissions: '.$response->body());
|
||||
}
|
||||
|
||||
$response = $response->json();
|
||||
$permissions = data_get($response, 'permissions');
|
||||
|
||||
$this->github_app->contents = data_get($permissions, 'contents');
|
||||
$this->github_app->metadata = data_get($permissions, 'metadata');
|
||||
$this->github_app->pull_requests = data_get($permissions, 'pull_requests');
|
||||
$this->github_app->administration = data_get($permissions, 'administration');
|
||||
|
||||
$this->github_app->save();
|
||||
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('GithubAppPermissionJob failed with: '.$e->getMessage());
|
||||
throw $e;
|
||||
|
@@ -25,7 +25,7 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
if (isDev() || isCloud()) {
|
||||
if (isDev()) {
|
||||
return;
|
||||
}
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
|
@@ -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;
|
||||
|
@@ -11,6 +11,7 @@ use App\Models\Service;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\ScheduledTask\TaskFailed;
|
||||
use App\Notifications\ScheduledTask\TaskSuccess;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -131,6 +132,11 @@ class ScheduledTaskJob implements ShouldQueue
|
||||
throw $e;
|
||||
} finally {
|
||||
ScheduledTaskDone::dispatch($this->team->id);
|
||||
if ($this->task_log) {
|
||||
$this->task_log->update([
|
||||
'finished_at' => Carbon::now()->toImmutable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ class SendMessageToSlackJob implements ShouldQueue
|
||||
public function handle(): void
|
||||
{
|
||||
Http::post($this->webhookUrl, [
|
||||
'text' => $this->message->title,
|
||||
'blocks' => [
|
||||
[
|
||||
'type' => 'section',
|
||||
|
@@ -73,19 +73,21 @@ class StripeProcessJob implements ShouldQueue
|
||||
}
|
||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||
if ($subscription) {
|
||||
send_internal_notification('Old subscription activated for team: '.$teamId);
|
||||
// send_internal_notification('Old subscription activated for team: '.$teamId);
|
||||
$subscription->update([
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_past_due' => false,
|
||||
]);
|
||||
} else {
|
||||
send_internal_notification('New subscription for team: '.$teamId);
|
||||
// send_internal_notification('New subscription for team: '.$teamId);
|
||||
Subscription::create([
|
||||
'team_id' => $teamId,
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_past_due' => false,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
@@ -100,6 +102,7 @@ class StripeProcessJob implements ShouldQueue
|
||||
if ($subscription) {
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_past_due' => false,
|
||||
]);
|
||||
} else {
|
||||
throw new \RuntimeException("No subscription found for customer: {$customerId}");
|
||||
@@ -119,9 +122,7 @@ class StripeProcessJob implements ShouldQueue
|
||||
}
|
||||
if (! $subscription->stripe_invoice_paid) {
|
||||
SubscriptionInvoiceFailedJob::dispatch($team);
|
||||
send_internal_notification('Invoice payment failed: '.$customerId);
|
||||
} else {
|
||||
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
|
||||
// send_internal_notification('Invoice payment failed: '.$customerId);
|
||||
}
|
||||
break;
|
||||
case 'payment_intent.payment_failed':
|
||||
@@ -136,7 +137,7 @@ class StripeProcessJob implements ShouldQueue
|
||||
|
||||
return;
|
||||
}
|
||||
send_internal_notification('Subscription payment failed for customer: '.$customerId);
|
||||
// send_internal_notification('Subscription payment failed for customer: '.$customerId);
|
||||
break;
|
||||
case 'customer.subscription.created':
|
||||
$customerId = data_get($data, 'customer');
|
||||
@@ -158,7 +159,7 @@ class StripeProcessJob implements ShouldQueue
|
||||
}
|
||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||
if ($subscription) {
|
||||
send_internal_notification("Subscription already exists for team: {$teamId}");
|
||||
// send_internal_notification("Subscription already exists for team: {$teamId}");
|
||||
throw new \RuntimeException("Subscription already exists for team: {$teamId}");
|
||||
} else {
|
||||
Subscription::create([
|
||||
@@ -182,7 +183,7 @@ class StripeProcessJob implements ShouldQueue
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
if ($status === 'incomplete_expired') {
|
||||
send_internal_notification('Subscription incomplete expired');
|
||||
// send_internal_notification('Subscription incomplete expired');
|
||||
throw new \RuntimeException('Subscription incomplete expired');
|
||||
}
|
||||
if ($teamId) {
|
||||
@@ -224,9 +225,33 @@ class StripeProcessJob implements ShouldQueue
|
||||
]);
|
||||
}
|
||||
}
|
||||
if ($status === 'past_due') {
|
||||
if ($subscription->stripe_subscription_id === $subscriptionId) {
|
||||
$subscription->update([
|
||||
'stripe_past_due' => true,
|
||||
]);
|
||||
send_internal_notification('Past Due: '.$customerId.'Subscription ID: '.$subscriptionId);
|
||||
}
|
||||
}
|
||||
if ($status === 'unpaid') {
|
||||
if ($subscription->stripe_subscription_id === $subscriptionId) {
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
send_internal_notification('Unpaid: '.$customerId.'Subscription ID: '.$subscriptionId);
|
||||
}
|
||||
$team = data_get($subscription, 'team');
|
||||
if ($team) {
|
||||
$team->subscriptionEnded();
|
||||
} else {
|
||||
send_internal_notification('Subscription unpaid but no team found in Coolify for customer: '.$customerId);
|
||||
throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
|
||||
}
|
||||
}
|
||||
if ($status === 'active') {
|
||||
if ($subscription->stripe_subscription_id === $subscriptionId) {
|
||||
$subscription->update([
|
||||
'stripe_past_due' => false,
|
||||
'stripe_invoice_paid' => true,
|
||||
]);
|
||||
}
|
||||
|
104
app/Jobs/VolumeCloneJob.php
Normal file
104
app/Jobs/VolumeCloneJob.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
// }
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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,
|
||||
]
|
||||
);
|
||||
|
@@ -49,6 +49,11 @@ class Dashboard extends Component
|
||||
])->sortBy('id')->groupBy('server_name')->toArray();
|
||||
}
|
||||
|
||||
public function navigateToProject($projectUuid)
|
||||
{
|
||||
return $this->redirect(collect($this->projects)->firstWhere('uuid', $projectUuid)->navigateTo(), true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard');
|
||||
|
@@ -35,10 +35,18 @@ class Docker extends Component
|
||||
$this->network = new Cuid2;
|
||||
$this->servers = Server::isUsable()->get();
|
||||
if ($server_id) {
|
||||
$this->selectedServer = $this->servers->find($server_id) ?: $this->servers->first();
|
||||
$foundServer = $this->servers->find($server_id) ?: $this->servers->first();
|
||||
if (! $foundServer) {
|
||||
throw new \Exception('Server not found.');
|
||||
}
|
||||
$this->selectedServer = $foundServer;
|
||||
$this->serverId = $this->selectedServer->id;
|
||||
} else {
|
||||
$this->selectedServer = $this->servers->first();
|
||||
$foundServer = $this->servers->first();
|
||||
if (! $foundServer) {
|
||||
throw new \Exception('Server not found.');
|
||||
}
|
||||
$this->selectedServer = $foundServer;
|
||||
$this->serverId = $this->selectedServer->id;
|
||||
}
|
||||
$this->generateName();
|
||||
@@ -83,9 +91,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);
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ class Help extends Component
|
||||
$type = set_transanctional_email_settings($settings);
|
||||
|
||||
// Sending feedback through Cloud API
|
||||
if ($type === false) {
|
||||
if (blank($type)) {
|
||||
$url = 'https://app.coolify.io/api/feedback';
|
||||
Http::post($url, [
|
||||
'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`',
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
//use Livewire\Component;
|
||||
// use Livewire\Component;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -124,9 +124,20 @@ class Advanced extends Component
|
||||
}
|
||||
}
|
||||
|
||||
private function resetDefaultLabels()
|
||||
{
|
||||
if ($this->application->settings->is_container_label_readonly_enabled === false) {
|
||||
return;
|
||||
}
|
||||
$customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
||||
$this->application->custom_labels = base64_encode($customLabels);
|
||||
$this->application->save();
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$reset = false;
|
||||
if ($this->isLogDrainEnabled) {
|
||||
if (! $this->application->destination->server->isLogDrainEnabled()) {
|
||||
$this->isLogDrainEnabled = false;
|
||||
@@ -140,7 +151,7 @@ class Advanced extends Component
|
||||
$this->application->isGzipEnabled() !== $this->isGzipEnabled ||
|
||||
$this->application->isStripprefixEnabled() !== $this->isStripprefixEnabled
|
||||
) {
|
||||
$this->dispatch('resetDefaultLabels', false);
|
||||
$reset = true;
|
||||
}
|
||||
|
||||
if ($this->application->settings->is_raw_compose_deployment_enabled) {
|
||||
@@ -149,6 +160,11 @@ class Advanced extends Component
|
||||
$this->application->parse();
|
||||
}
|
||||
$this->syncData(true);
|
||||
|
||||
if ($reset) {
|
||||
$this->resetDefaultLabels();
|
||||
}
|
||||
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
|
@@ -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()
|
||||
|
@@ -18,7 +18,7 @@ class Index extends Component
|
||||
|
||||
public int $skip = 0;
|
||||
|
||||
public int $default_take = 40;
|
||||
public int $default_take = 10;
|
||||
|
||||
public bool $show_next = false;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class Index extends Component
|
||||
if (! $application) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 40);
|
||||
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->default_take);
|
||||
$this->application = $application;
|
||||
$this->deployments = $deployments;
|
||||
$this->deployments_count = $count;
|
||||
|
@@ -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()
|
||||
|
@@ -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;
|
||||
}
|
||||
@@ -53,13 +53,13 @@ class DeploymentNavbar extends Component
|
||||
public function cancel()
|
||||
{
|
||||
$kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
|
||||
$build_server_id = $this->application_deployment_queue->build_server_id;
|
||||
$build_server_id = $this->application_deployment_queue->build_server_id ?? $this->application->destination->server_id;
|
||||
$server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
|
||||
try {
|
||||
if ($this->application->settings->is_build_server_enabled) {
|
||||
$server = Server::find($build_server_id);
|
||||
$server = Server::ownedByCurrentTeam()->find($build_server_id);
|
||||
} else {
|
||||
$server = Server::find($server_id);
|
||||
$server = Server::ownedByCurrentTeam()->find($server_id);
|
||||
}
|
||||
if ($this->application_deployment_queue->logs) {
|
||||
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
|
@@ -155,7 +155,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();
|
||||
@@ -189,6 +189,9 @@ class General extends Component
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($this->application->settings->is_container_label_readonly_enabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadComposeFile($isInit = false)
|
||||
@@ -296,7 +299,7 @@ class General extends Component
|
||||
public function resetDefaultLabels($manualReset = false)
|
||||
{
|
||||
try {
|
||||
if ($this->application->settings->is_container_label_readonly_enabled && ! $manualReset) {
|
||||
if (! $this->application->settings->is_container_label_readonly_enabled && ! $manualReset) {
|
||||
return;
|
||||
}
|
||||
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
||||
@@ -326,10 +329,11 @@ class General extends Component
|
||||
}
|
||||
check_domain_usage(resource: $this->application);
|
||||
$this->application->fqdn = $domains->implode(',');
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
}
|
||||
|
||||
public function set_redirect()
|
||||
public function setRedirect()
|
||||
{
|
||||
try {
|
||||
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
|
||||
@@ -362,10 +366,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();
|
||||
@@ -444,6 +448,7 @@ class General extends Component
|
||||
{
|
||||
$config = GenerateConfig::run($this->application, true);
|
||||
$fileName = str($this->application->name)->slug()->append('_config.json');
|
||||
dd($config);
|
||||
|
||||
return response()->streamDownload(function () use ($config) {
|
||||
echo $config;
|
||||
|
@@ -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();
|
||||
@@ -90,12 +90,12 @@ class Heading extends Component
|
||||
force_rebuild: $force_rebuild,
|
||||
);
|
||||
|
||||
return redirect()->route('project.application.deployment.show', [
|
||||
return $this->redirectRoute('project.application.deployment.show', [
|
||||
'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'],
|
||||
], navigate: true);
|
||||
}
|
||||
|
||||
protected function setDeploymentUuid()
|
||||
@@ -132,12 +132,12 @@ class Heading extends Component
|
||||
restart_only: true,
|
||||
);
|
||||
|
||||
return redirect()->route('project.application.deployment.show', [
|
||||
return $this->redirectRoute('project.application.deployment.show', [
|
||||
'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'],
|
||||
], navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
@@ -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);
|
||||
|
@@ -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'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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');
|
||||
}
|
||||
|
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
@@ -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.']
|
||||
],
|
||||
]);
|
||||
|
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,35 +117,12 @@ class BackupExecutions extends Component
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getServerTimezone()
|
||||
{
|
||||
$server = $this->server();
|
||||
if (! $server) {
|
||||
return 'UTC';
|
||||
}
|
||||
|
||||
return $server->settings->server_timezone;
|
||||
}
|
||||
|
||||
public function formatDateInServerTimezone($date)
|
||||
{
|
||||
$serverTimezone = $this->getServerTimezone();
|
||||
$dateObj = new \DateTime($date);
|
||||
try {
|
||||
$dateObj->setTimezone(new \DateTimeZone($serverTimezone));
|
||||
} catch (\Exception) {
|
||||
$dateObj->setTimezone(new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
return $dateObj->format('Y-m-d H:i:s T');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
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'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
@@ -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');
|
||||
|
@@ -43,8 +43,10 @@ class Heading extends Component
|
||||
|
||||
public function check_status($showNotification = false)
|
||||
{
|
||||
GetContainersStatus::run($this->database->destination->server);
|
||||
$this->database->refresh();
|
||||
if ($this->database->destination->server->isFunctional()) {
|
||||
GetContainersStatus::dispatch($this->database->destination->server);
|
||||
}
|
||||
|
||||
if ($showNotification) {
|
||||
$this->dispatch('success', 'Database status updated.');
|
||||
}
|
||||
|
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -88,12 +88,12 @@ class General extends Component
|
||||
if (version_compare($this->redis_version, '6.0', '>=')) {
|
||||
$this->database->runtime_environment_variables()->updateOrCreate(
|
||||
['key' => 'REDIS_USERNAME'],
|
||||
['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id]
|
||||
['value' => $this->redis_username, 'resourceable_id' => $this->database->id]
|
||||
);
|
||||
}
|
||||
$this->database->runtime_environment_variables()->updateOrCreate(
|
||||
['key' => 'REDIS_PASSWORD'],
|
||||
['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id]
|
||||
['value' => $this->redis_password, 'resourceable_id' => $this->database->id]
|
||||
);
|
||||
|
||||
$this->database->save();
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -30,4 +30,11 @@ class Index extends Component
|
||||
{
|
||||
return view('livewire.project.index');
|
||||
}
|
||||
|
||||
public function navigateToProject($projectUuid)
|
||||
{
|
||||
$project = collect($this->projects)->firstWhere('uuid', $projectUuid);
|
||||
|
||||
return $this->redirect($project->navigateTo(), true);
|
||||
}
|
||||
}
|
||||
|
@@ -52,14 +52,8 @@ class DockerCompose extends Component
|
||||
'dockerComposeRaw' => 'required',
|
||||
]);
|
||||
$this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
|
||||
|
||||
$isValid = validateComposeFile($this->dockerComposeRaw, $server_id);
|
||||
if ($isValid !== 'OK') {
|
||||
return $this->dispatch('error', "Invalid docker-compose file.\n$isValid");
|
||||
}
|
||||
|
||||
$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 +81,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 +91,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) {
|
||||
|
@@ -6,6 +6,7 @@ use App\Models\Application;
|
||||
use App\Models\Project;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Services\DockerImageParser;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -28,12 +29,10 @@ class DockerImage extends Component
|
||||
$this->validate([
|
||||
'dockerImage' => 'required',
|
||||
]);
|
||||
$image = str($this->dockerImage)->before(':');
|
||||
if (str($this->dockerImage)->contains(':')) {
|
||||
$tag = str($this->dockerImage)->after(':');
|
||||
} else {
|
||||
$tag = 'latest';
|
||||
}
|
||||
|
||||
$parser = new DockerImageParser;
|
||||
$parser->parse($this->dockerImage);
|
||||
|
||||
$destination_uuid = $this->query['destination'];
|
||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
|
||||
if (! $destination) {
|
||||
@@ -45,7 +44,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,
|
||||
@@ -53,8 +52,8 @@ class DockerImage extends Component
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'dockerimage',
|
||||
'ports_exposes' => 80,
|
||||
'docker_registry_image_name' => $image,
|
||||
'docker_registry_image_tag' => $tag,
|
||||
'docker_registry_image_name' => $parser->getFullImageNameWithoutTag(),
|
||||
'docker_registry_image_tag' => $parser->getTag(),
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination_class,
|
||||
@@ -69,7 +68,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,
|
||||
]);
|
||||
}
|
||||
|
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
@@ -105,7 +105,7 @@ class GithubPrivateRepository extends Component
|
||||
$this->page = 1;
|
||||
$this->selected_github_app_id = $github_app_id;
|
||||
$this->github_app = GithubApp::where('id', $github_app_id)->first();
|
||||
$this->token = generate_github_installation_token($this->github_app);
|
||||
$this->token = generateGithubInstallationToken($this->github_app);
|
||||
$this->loadRepositoryByPage();
|
||||
if ($this->repositories->count() < $this->total_repositories_count) {
|
||||
while ($this->repositories->count() < $this->total_repositories_count) {
|
||||
@@ -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) {
|
||||
|
@@ -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) {
|
||||
|
@@ -188,11 +188,22 @@ class PublicGitRepository extends Component
|
||||
|
||||
private function getGitSource()
|
||||
{
|
||||
$this->git_branch = 'main';
|
||||
$this->base_directory = '/';
|
||||
|
||||
$this->repository_url_parsed = Url::fromString($this->repository_url);
|
||||
$this->git_host = $this->repository_url_parsed->getHost();
|
||||
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
|
||||
|
||||
if ($this->repository_url_parsed->getSegment(3) === 'tree') {
|
||||
$this->git_branch = str($this->repository_url_parsed->getPath())->after('tree/')->value();
|
||||
$path = str($this->repository_url_parsed->getPath())->trim('/');
|
||||
$this->git_branch = str($path)->after('tree/')->before('/')->value();
|
||||
$this->base_directory = str($path)->after($this->git_branch)->after('/')->value();
|
||||
if (filled($this->base_directory)) {
|
||||
$this->base_directory = '/'.$this->base_directory;
|
||||
} else {
|
||||
$this->base_directory = '/';
|
||||
}
|
||||
} else {
|
||||
$this->git_branch = 'main';
|
||||
}
|
||||
@@ -225,7 +236,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 +248,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 +271,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 +330,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) {
|
||||
|
@@ -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,23 +75,13 @@ 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);
|
||||
$services = get_service_templates();
|
||||
$services = collect($services)->map(function ($service, $key) {
|
||||
$default_logo = 'images/default.webp';
|
||||
$logo = data_get($service, 'logo', $default_logo);
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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();
|
||||
@@ -26,7 +32,7 @@ class Configuration extends Component
|
||||
return [
|
||||
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
|
||||
'check_status',
|
||||
'refresh' => '$refresh',
|
||||
'refreshStatus' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -76,14 +92,16 @@ class Configuration extends Component
|
||||
public function check_status()
|
||||
{
|
||||
try {
|
||||
GetContainersStatus::run($this->service->server);
|
||||
if ($this->service->server->isFunctional()) {
|
||||
GetContainersStatus::dispatch($this->service->server);
|
||||
}
|
||||
$this->service->applications->each(function ($application) {
|
||||
$application->refresh();
|
||||
});
|
||||
$this->service->databases->each(function ($database) {
|
||||
$database->refresh();
|
||||
});
|
||||
$this->dispatch('$refresh');
|
||||
$this->dispatch('refreshStatus');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
@@ -4,7 +4,10 @@ namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Database extends Component
|
||||
@@ -15,6 +18,8 @@ class Database extends Component
|
||||
|
||||
public $fileStorages;
|
||||
|
||||
public $parameters;
|
||||
|
||||
protected $listeners = ['refreshFileStorages'];
|
||||
|
||||
protected $rules = [
|
||||
@@ -34,12 +39,33 @@ class Database extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
if ($this->database->is_public) {
|
||||
$this->db_url_public = $this->database->getServiceDatabaseUrl();
|
||||
}
|
||||
$this->refreshFileStorages();
|
||||
}
|
||||
|
||||
public function delete($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.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->database->delete();
|
||||
$this->dispatch('success', 'Database deleted.');
|
||||
|
||||
return redirect()->route('project.service.configuration', $this->parameters);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveExclude()
|
||||
{
|
||||
$this->submit();
|
||||
|
@@ -31,12 +31,22 @@ class EditCompose extends Component
|
||||
|
||||
public function refreshEnvs()
|
||||
{
|
||||
$this->service = Service::find($this->serviceId);
|
||||
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->service = Service::find($this->serviceId);
|
||||
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
|
||||
}
|
||||
|
||||
public function validateCompose()
|
||||
{
|
||||
$isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server_id);
|
||||
if ($isValid !== 'OK') {
|
||||
$this->dispatch('error', "Invalid docker-compose file.\n$isValid");
|
||||
} else {
|
||||
$this->dispatch('success', 'Docker compose is valid.');
|
||||
}
|
||||
}
|
||||
|
||||
public function saveEditedCompose()
|
||||
|
@@ -43,12 +43,11 @@ class EditDomain extends Component
|
||||
updateCompose($this->application);
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
} else {
|
||||
! $warning && $this->dispatch('success', 'Service saved.');
|
||||
}
|
||||
$this->application->service->parse();
|
||||
$this->dispatch('refresh');
|
||||
$this->dispatch('configurationChanged');
|
||||
$this->dispatch('refreshStatus');
|
||||
} catch (\Throwable $e) {
|
||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
|
@@ -4,7 +4,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;
|
||||
@@ -40,6 +40,7 @@ class Navbar extends Component
|
||||
return [
|
||||
"echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted',
|
||||
'envsUpdated' => '$refresh',
|
||||
'refreshStatus' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,44 @@ 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);
|
||||
$activity = StartService::run($this->service, pullLatestImages: true);
|
||||
$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();
|
||||
}
|
||||
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
|
||||
$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()
|
||||
@@ -109,10 +127,7 @@ class Navbar extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
StopService::run(service: $this->service, dockerCleanup: false);
|
||||
$this->service->parse();
|
||||
$this->dispatch('imagePulled');
|
||||
$activity = StartService::run($this->service);
|
||||
$activity = StartService::run($this->service, stopBeforeStart: true);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
@@ -124,11 +139,7 @@ class Navbar extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
PullImage::run($this->service);
|
||||
StopService::run(service: $this->service, dockerCleanup: false);
|
||||
$this->service->parse();
|
||||
$this->dispatch('imagePulled');
|
||||
$activity = StartService::run($this->service);
|
||||
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
|
@@ -63,7 +63,7 @@ class StackForm extends Component
|
||||
public function saveCompose($raw)
|
||||
{
|
||||
$this->service->docker_compose_raw = $raw;
|
||||
$this->submit(notify: false);
|
||||
$this->submit(notify: true);
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
@@ -76,10 +76,6 @@ class StackForm extends Component
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server->id);
|
||||
if ($isValid !== 'OK') {
|
||||
throw new \Exception("Invalid docker-compose file.\n$isValid");
|
||||
}
|
||||
$this->service->save();
|
||||
$this->service->saveExtraFields($this->fields);
|
||||
$this->service->parse();
|
||||
|
@@ -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);
|
||||
|
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
@@ -142,6 +138,7 @@ class All extends Component
|
||||
private function handleBulkSubmit()
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
|
||||
$this->deleteRemovedVariables(false, $variables);
|
||||
$this->updateOrCreateVariables(false, $variables);
|
||||
|
||||
@@ -178,35 +175,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';
|
||||
@@ -216,6 +190,9 @@ class All extends Component
|
||||
private function updateOrCreateVariables($isPreview, $variables)
|
||||
{
|
||||
foreach ($variables as $key => $value) {
|
||||
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) {
|
||||
continue;
|
||||
}
|
||||
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
|
||||
$found = $this->resource->$method()->where('key', $key)->first();
|
||||
|
||||
@@ -231,34 +208,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();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user