Merge branch 'next' into feature/authentik-provider
This commit is contained in:
@@ -11,7 +11,6 @@ class GenerateConfig
|
||||
|
||||
public function handle(Application $application, bool $is_json = false)
|
||||
{
|
||||
ray()->clearAll();
|
||||
return $application->generateConfig(is_json: $is_json);
|
||||
}
|
||||
}
|
||||
|
||||
37
app/Actions/Application/IsHorizonQueueEmpty.php
Normal file
37
app/Actions/Application/IsHorizonQueueEmpty.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Application;
|
||||
|
||||
use Laravel\Horizon\Contracts\JobRepository;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class IsHorizonQueueEmpty
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$hostname = gethostname();
|
||||
$recent = app(JobRepository::class)->getRecent();
|
||||
if ($recent) {
|
||||
$running = $recent->filter(function ($job) use ($hostname) {
|
||||
$payload = json_decode($job->payload);
|
||||
$tags = data_get($payload, 'tags');
|
||||
|
||||
return $job->status != 'completed' &&
|
||||
$job->status != 'failed' &&
|
||||
isset($tags) &&
|
||||
is_array($tags) &&
|
||||
in_array('server:'.$hostname, $tags);
|
||||
});
|
||||
if ($running->count() > 0) {
|
||||
echo 'false';
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
echo 'true';
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ class StopApplication
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
@@ -17,7 +19,6 @@ class StopApplication
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
ray('Stopping application: '.$application->name);
|
||||
|
||||
if ($server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
|
||||
@@ -36,8 +37,6 @@ class StopApplication
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,6 @@ class StopApplicationOneServer
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Actions\CoolifyTask;
|
||||
|
||||
use App\Data\CoolifyTaskArgs;
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Jobs\CoolifyTask;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
@@ -47,12 +46,7 @@ class PrepareCoolifyTask
|
||||
call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish,
|
||||
call_event_data: $this->remoteProcessArgs->call_event_data,
|
||||
);
|
||||
if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) {
|
||||
ray('Dispatching a high priority job');
|
||||
dispatch($job)->onQueue('high');
|
||||
} else {
|
||||
dispatch($job);
|
||||
}
|
||||
dispatch($job);
|
||||
$this->activity->refresh();
|
||||
|
||||
return $this->activity;
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Process\ProcessResult;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
@@ -39,7 +40,6 @@ class RunRemoteProcess
|
||||
*/
|
||||
public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null, $call_event_data = null)
|
||||
{
|
||||
|
||||
if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value && $activity->getExtraProperty('type') !== ActivityTypes::COMMAND->value) {
|
||||
throw new \RuntimeException('Incompatible Activity to run a remote command.');
|
||||
}
|
||||
@@ -125,7 +125,7 @@ class RunRemoteProcess
|
||||
]));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
Log::error('Error calling event: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class StartClickhouse
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"echo 'Starting database.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
];
|
||||
|
||||
@@ -51,6 +51,8 @@ class StartClickhouse
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
'coolify.type' => 'database',
|
||||
'coolify.databaseId' => $this->database->id,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
|
||||
@@ -97,8 +99,8 @@ class StartClickhouse
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
|
||||
@@ -16,6 +16,8 @@ class StartDatabase
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
@@ -23,28 +25,28 @@ class StartDatabase
|
||||
return 'Server is not functional';
|
||||
}
|
||||
switch ($database->getMorphClass()) {
|
||||
case 'App\Models\StandalonePostgresql':
|
||||
case \App\Models\StandalonePostgresql::class:
|
||||
$activity = StartPostgresql::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneRedis':
|
||||
case \App\Models\StandaloneRedis::class:
|
||||
$activity = StartRedis::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneMongodb':
|
||||
case \App\Models\StandaloneMongodb::class:
|
||||
$activity = StartMongodb::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneMysql':
|
||||
case \App\Models\StandaloneMysql::class:
|
||||
$activity = StartMysql::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneMariadb':
|
||||
case \App\Models\StandaloneMariadb::class:
|
||||
$activity = StartMariadb::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneKeydb':
|
||||
case \App\Models\StandaloneKeydb::class:
|
||||
$activity = StartKeydb::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneDragonfly':
|
||||
case \App\Models\StandaloneDragonfly::class:
|
||||
$activity = StartDragonfly::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneClickhouse':
|
||||
case \App\Models\StandaloneClickhouse::class:
|
||||
$activity = StartClickhouse::run($database);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ class StartDatabaseProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
|
||||
{
|
||||
$internalPort = null;
|
||||
@@ -26,7 +28,7 @@ class StartDatabaseProxy
|
||||
$server = data_get($database, 'destination.server');
|
||||
$containerName = data_get($database, 'uuid');
|
||||
$proxyContainerName = "{$database->uuid}-proxy";
|
||||
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
|
||||
$databaseType = $database->databaseType();
|
||||
// $connectPredefined = data_get($database, 'service.connect_to_docker_network');
|
||||
$network = $database->service->uuid;
|
||||
@@ -34,54 +36,54 @@ class StartDatabaseProxy
|
||||
$proxyContainerName = "{$database->service->uuid}-proxy";
|
||||
switch ($databaseType) {
|
||||
case 'standalone-mariadb':
|
||||
$type = 'App\Models\StandaloneMariadb';
|
||||
$type = \App\Models\StandaloneMariadb::class;
|
||||
$containerName = "mariadb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
$type = 'App\Models\StandaloneMongodb';
|
||||
$type = \App\Models\StandaloneMongodb::class;
|
||||
$containerName = "mongodb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
$type = 'App\Models\StandaloneMysql';
|
||||
$type = \App\Models\StandaloneMysql::class;
|
||||
$containerName = "mysql-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
$type = 'App\Models\StandalonePostgresql';
|
||||
$type = \App\Models\StandalonePostgresql::class;
|
||||
$containerName = "postgresql-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
$type = 'App\Models\StandaloneRedis';
|
||||
$type = \App\Models\StandaloneRedis::class;
|
||||
$containerName = "redis-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-keydb':
|
||||
$type = 'App\Models\StandaloneKeydb';
|
||||
$type = \App\Models\StandaloneKeydb::class;
|
||||
$containerName = "keydb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-dragonfly':
|
||||
$type = 'App\Models\StandaloneDragonfly';
|
||||
$type = \App\Models\StandaloneDragonfly::class;
|
||||
$containerName = "dragonfly-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-clickhouse':
|
||||
$type = 'App\Models\StandaloneClickhouse';
|
||||
$type = \App\Models\StandaloneClickhouse::class;
|
||||
$containerName = "clickhouse-{$database->service->uuid}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($type === 'App\Models\StandaloneRedis') {
|
||||
if ($type === \App\Models\StandaloneRedis::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === 'App\Models\StandalonePostgresql') {
|
||||
} elseif ($type === \App\Models\StandalonePostgresql::class) {
|
||||
$internalPort = 5432;
|
||||
} elseif ($type === 'App\Models\StandaloneMongodb') {
|
||||
} elseif ($type === \App\Models\StandaloneMongodb::class) {
|
||||
$internalPort = 27017;
|
||||
} elseif ($type === 'App\Models\StandaloneMysql') {
|
||||
} elseif ($type === \App\Models\StandaloneMysql::class) {
|
||||
$internalPort = 3306;
|
||||
} elseif ($type === 'App\Models\StandaloneMariadb') {
|
||||
} elseif ($type === \App\Models\StandaloneMariadb::class) {
|
||||
$internalPort = 3306;
|
||||
} elseif ($type === 'App\Models\StandaloneKeydb') {
|
||||
} elseif ($type === \App\Models\StandaloneKeydb::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === 'App\Models\StandaloneDragonfly') {
|
||||
} elseif ($type === \App\Models\StandaloneDragonfly::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === 'App\Models\StandaloneClickhouse') {
|
||||
} elseif ($type === \App\Models\StandaloneClickhouse::class) {
|
||||
$internalPort = 9000;
|
||||
}
|
||||
$configuration_dir = database_proxy_dir($database->uuid);
|
||||
|
||||
@@ -26,7 +26,7 @@ class StartDragonfly
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"echo 'Starting database.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
];
|
||||
|
||||
@@ -48,6 +48,8 @@ class StartDragonfly
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
'coolify.type' => 'database',
|
||||
'coolify.databaseId' => $this->database->id,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => "redis-cli -a {$this->database->dragonfly_password} ping",
|
||||
@@ -94,8 +96,8 @@ class StartDragonfly
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
|
||||
@@ -27,7 +27,7 @@ class StartKeydb
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"echo 'Starting database.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
];
|
||||
|
||||
@@ -50,6 +50,8 @@ class StartKeydb
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
'coolify.type' => 'database',
|
||||
'coolify.databaseId' => $this->database->id,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => "keydb-cli --pass {$this->database->keydb_password} ping",
|
||||
@@ -105,8 +107,8 @@ class StartKeydb
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
|
||||
@@ -24,7 +24,7 @@ class StartMariadb
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"echo 'Starting database.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
];
|
||||
|
||||
@@ -45,6 +45,8 @@ class StartMariadb
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
'coolify.type' => 'database',
|
||||
'coolify.databaseId' => $this->database->id,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'],
|
||||
@@ -99,8 +101,8 @@ class StartMariadb
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
|
||||
@@ -25,8 +25,12 @@ class StartMongodb
|
||||
$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->name}.'",
|
||||
"echo 'Starting database.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
];
|
||||
|
||||
@@ -49,6 +53,8 @@ class StartMongodb
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
'coolify.type' => 'database',
|
||||
'coolify.databaseId' => $this->database->id,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
@@ -115,8 +121,8 @@ class StartMongodb
|
||||
];
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
|
||||
@@ -24,7 +24,7 @@ class StartMysql
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"echo 'Starting database.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
];
|
||||
|
||||
@@ -45,6 +45,8 @@ class StartMysql
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
'coolify.type' => 'database',
|
||||
'coolify.databaseId' => $this->database->id,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"],
|
||||
@@ -99,8 +101,8 @@ class StartMysql
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
|
||||
@@ -25,7 +25,7 @@ class StartPostgresql
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"echo 'Starting database.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
|
||||
];
|
||||
@@ -49,6 +49,8 @@ class StartPostgresql
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
'coolify.type' => 'database',
|
||||
'coolify.databaseId' => $this->database->id,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
@@ -120,8 +122,8 @@ class StartPostgresql
|
||||
];
|
||||
}
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
|
||||
@@ -21,13 +21,11 @@ class StartRedis
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"echo 'Starting database.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
];
|
||||
|
||||
@@ -37,6 +35,8 @@ class StartRedis
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->add_custom_redis();
|
||||
|
||||
$startCommand = $this->buildStartCommand();
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
$container_name => [
|
||||
@@ -50,6 +50,8 @@ class StartRedis
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
'coolify.type' => 'database',
|
||||
'coolify.databaseId' => $this->database->id,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
@@ -105,12 +107,11 @@ class StartRedis
|
||||
'target' => '/usr/local/etc/redis/redis.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
@@ -160,12 +161,26 @@ class StartRedis
|
||||
private function generate_environment_variables()
|
||||
{
|
||||
$environment_variables = collect();
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
if ($env->is_shared) {
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
|
||||
if ($env->key === 'REDIS_PASSWORD') {
|
||||
$this->database->update(['redis_password' => $env->real_value]);
|
||||
}
|
||||
|
||||
if ($env->key === 'REDIS_USERNAME') {
|
||||
$this->database->update(['redis_username' => $env->real_value]);
|
||||
}
|
||||
} else {
|
||||
if ($env->key === 'REDIS_PASSWORD') {
|
||||
$env->update(['value' => $this->database->redis_password]);
|
||||
} elseif ($env->key === 'REDIS_USERNAME') {
|
||||
$env->update(['value' => $this->database->redis_username]);
|
||||
}
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
||||
@@ -173,6 +188,27 @@ class StartRedis
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
private function buildStartCommand(): string
|
||||
{
|
||||
$hasRedisConf = ! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf);
|
||||
$redisConfPath = '/usr/local/etc/redis/redis.conf';
|
||||
|
||||
if ($hasRedisConf) {
|
||||
$confContent = $this->database->redis_conf;
|
||||
$hasRequirePass = str_contains($confContent, 'requirepass');
|
||||
|
||||
if ($hasRequirePass) {
|
||||
$command = "redis-server $redisConfPath";
|
||||
} else {
|
||||
$command = "redis-server $redisConfPath --requirepass {$this->database->redis_password}";
|
||||
}
|
||||
} else {
|
||||
$command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
}
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
private function add_custom_redis()
|
||||
{
|
||||
if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Events\DatabaseStatusChanged;
|
||||
use App\Events\DatabaseProxyStopped;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
@@ -18,16 +18,22 @@ class StopDatabaseProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database)
|
||||
{
|
||||
$server = data_get($database, 'destination.server');
|
||||
$uuid = $database->uuid;
|
||||
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
|
||||
$uuid = $database->service->uuid;
|
||||
$server = data_get($database, 'service.server');
|
||||
}
|
||||
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
|
||||
|
||||
$database->is_public = false;
|
||||
$database->save();
|
||||
DatabaseStatusChanged::dispatch();
|
||||
|
||||
DatabaseProxyStopped::dispatch();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
namespace App\Actions\Docker;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Shared\ComplexStatusCheck;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Notifications\Container\ContainerRestarted;
|
||||
use App\Notifications\Container\ContainerStopped;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -19,6 +15,8 @@ class GetContainersStatus
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public $applications;
|
||||
|
||||
public ?Collection $containers;
|
||||
@@ -33,7 +31,7 @@ class GetContainersStatus
|
||||
$this->containerReplicates = $containerReplicates;
|
||||
$this->server = $server;
|
||||
if (! $this->server->isFunctional()) {
|
||||
return 'Server is not ready.';
|
||||
return 'Server is not functional.';
|
||||
}
|
||||
$this->applications = $this->server->applications();
|
||||
$skip_these_applications = collect([]);
|
||||
@@ -49,323 +47,8 @@ class GetContainersStatus
|
||||
$this->applications = $this->applications->filter(function ($value, $key) use ($skip_these_applications) {
|
||||
return ! $skip_these_applications->pluck('id')->contains($value->id);
|
||||
});
|
||||
$this->old_way();
|
||||
// if ($this->server->isSwarm()) {
|
||||
// $this->old_way();
|
||||
// } else {
|
||||
// if (!$this->server->is_metrics_enabled) {
|
||||
// $this->old_way();
|
||||
// return;
|
||||
// }
|
||||
// $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this->server, false);
|
||||
// $sentinel_found = json_decode($sentinel_found, true);
|
||||
// $status = data_get($sentinel_found, '0.State.Status', 'exited');
|
||||
// if ($status === 'running') {
|
||||
// ray('Checking with Sentinel');
|
||||
// $this->sentinel();
|
||||
// } else {
|
||||
// ray('Checking the Old way');
|
||||
// $this->old_way();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// private function sentinel()
|
||||
// {
|
||||
// try {
|
||||
// $this->containers = $this->server->getContainersWithSentinel();
|
||||
// if ($this->containers->count() === 0) {
|
||||
// return;
|
||||
// }
|
||||
// $databases = $this->server->databases();
|
||||
// $services = $this->server->services()->get();
|
||||
// $previews = $this->server->previews();
|
||||
// $foundApplications = [];
|
||||
// $foundApplicationPreviews = [];
|
||||
// $foundDatabases = [];
|
||||
// $foundServices = [];
|
||||
|
||||
// foreach ($this->containers as $container) {
|
||||
// $labels = Arr::undot(data_get($container, 'labels'));
|
||||
// $containerStatus = data_get($container, 'state');
|
||||
// $containerHealth = data_get($container, 'health_status', 'unhealthy');
|
||||
// $containerStatus = "$containerStatus ($containerHealth)";
|
||||
// $applicationId = data_get($labels, 'coolify.applicationId');
|
||||
// if ($applicationId) {
|
||||
// $pullRequestId = data_get($labels, 'coolify.pullRequestId');
|
||||
// if ($pullRequestId) {
|
||||
// if (str($applicationId)->contains('-')) {
|
||||
// $applicationId = str($applicationId)->before('-');
|
||||
// }
|
||||
// $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
|
||||
// if ($preview) {
|
||||
// $foundApplicationPreviews[] = $preview->id;
|
||||
// $statusFromDb = $preview->status;
|
||||
// if ($statusFromDb !== $containerStatus) {
|
||||
// $preview->update(['status' => $containerStatus]);
|
||||
// }
|
||||
// } else {
|
||||
// //Notify user that this container should not be there.
|
||||
// }
|
||||
// } else {
|
||||
// $application = $this->applications->where('id', $applicationId)->first();
|
||||
// if ($application) {
|
||||
// $foundApplications[] = $application->id;
|
||||
// $statusFromDb = $application->status;
|
||||
// if ($statusFromDb !== $containerStatus) {
|
||||
// $application->update(['status' => $containerStatus]);
|
||||
// }
|
||||
// } else {
|
||||
// //Notify user that this container should not be there.
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// $uuid = data_get($labels, 'com.docker.compose.service');
|
||||
// $type = data_get($labels, 'coolify.type');
|
||||
// if ($uuid) {
|
||||
// if ($type === 'service') {
|
||||
// $database_id = data_get($labels, 'coolify.service.subId');
|
||||
// if ($database_id) {
|
||||
// $service_db = ServiceDatabase::where('id', $database_id)->first();
|
||||
// if ($service_db) {
|
||||
// $uuid = $service_db->service->uuid;
|
||||
// $isPublic = data_get($service_db, 'is_public');
|
||||
// if ($isPublic) {
|
||||
// $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
|
||||
// if ($this->server->isSwarm()) {
|
||||
// // TODO: fix this with sentinel
|
||||
// return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||
// } else {
|
||||
// return data_get($value, 'name') === "$uuid-proxy";
|
||||
// }
|
||||
// })->first();
|
||||
// if (! $foundTcpProxy) {
|
||||
// StartDatabaseProxy::run($service_db);
|
||||
// // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// $database = $databases->where('uuid', $uuid)->first();
|
||||
// if ($database) {
|
||||
// $isPublic = data_get($database, 'is_public');
|
||||
// $foundDatabases[] = $database->id;
|
||||
// $statusFromDb = $database->status;
|
||||
// if ($statusFromDb !== $containerStatus) {
|
||||
// $database->update(['status' => $containerStatus]);
|
||||
// }
|
||||
// if ($isPublic) {
|
||||
// $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
|
||||
// if ($this->server->isSwarm()) {
|
||||
// // TODO: fix this with sentinel
|
||||
// return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||
// } else {
|
||||
// return data_get($value, 'name') === "$uuid-proxy";
|
||||
// }
|
||||
// })->first();
|
||||
// if (! $foundTcpProxy) {
|
||||
// StartDatabaseProxy::run($database);
|
||||
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // Notify user that this container should not be there.
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if (data_get($container, 'name') === 'coolify-db') {
|
||||
// $foundDatabases[] = 0;
|
||||
// }
|
||||
// }
|
||||
// $serviceLabelId = data_get($labels, 'coolify.serviceId');
|
||||
// if ($serviceLabelId) {
|
||||
// $subType = data_get($labels, 'coolify.service.subType');
|
||||
// $subId = data_get($labels, 'coolify.service.subId');
|
||||
// $service = $services->where('id', $serviceLabelId)->first();
|
||||
// if (! $service) {
|
||||
// continue;
|
||||
// }
|
||||
// if ($subType === 'application') {
|
||||
// $service = $service->applications()->where('id', $subId)->first();
|
||||
// } else {
|
||||
// $service = $service->databases()->where('id', $subId)->first();
|
||||
// }
|
||||
// if ($service) {
|
||||
// $foundServices[] = "$service->id-$service->name";
|
||||
// $statusFromDb = $service->status;
|
||||
// if ($statusFromDb !== $containerStatus) {
|
||||
// // ray('Updating status: ' . $containerStatus);
|
||||
// $service->update(['status' => $containerStatus]);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// $exitedServices = collect([]);
|
||||
// foreach ($services as $service) {
|
||||
// $apps = $service->applications()->get();
|
||||
// $dbs = $service->databases()->get();
|
||||
// foreach ($apps as $app) {
|
||||
// if (in_array("$app->id-$app->name", $foundServices)) {
|
||||
// continue;
|
||||
// } else {
|
||||
// $exitedServices->push($app);
|
||||
// }
|
||||
// }
|
||||
// foreach ($dbs as $db) {
|
||||
// if (in_array("$db->id-$db->name", $foundServices)) {
|
||||
// continue;
|
||||
// } else {
|
||||
// $exitedServices->push($db);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// $exitedServices = $exitedServices->unique('id');
|
||||
// foreach ($exitedServices as $exitedService) {
|
||||
// if (str($exitedService->status)->startsWith('exited')) {
|
||||
// continue;
|
||||
// }
|
||||
// $name = data_get($exitedService, 'name');
|
||||
// $fqdn = data_get($exitedService, 'fqdn');
|
||||
// if ($name) {
|
||||
// if ($fqdn) {
|
||||
// $containerName = "$name, available at $fqdn";
|
||||
// } else {
|
||||
// $containerName = $name;
|
||||
// }
|
||||
// } else {
|
||||
// if ($fqdn) {
|
||||
// $containerName = $fqdn;
|
||||
// } else {
|
||||
// $containerName = null;
|
||||
// }
|
||||
// }
|
||||
// $projectUuid = data_get($service, 'environment.project.uuid');
|
||||
// $serviceUuid = data_get($service, 'uuid');
|
||||
// $environmentName = data_get($service, 'environment.name');
|
||||
|
||||
// if ($projectUuid && $serviceUuid && $environmentName) {
|
||||
// $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid;
|
||||
// } else {
|
||||
// $url = null;
|
||||
// }
|
||||
// // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
// $exitedService->update(['status' => 'exited']);
|
||||
// }
|
||||
|
||||
// $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
|
||||
// foreach ($notRunningApplications as $applicationId) {
|
||||
// $application = $this->applications->where('id', $applicationId)->first();
|
||||
// if (str($application->status)->startsWith('exited')) {
|
||||
// continue;
|
||||
// }
|
||||
// $application->update(['status' => 'exited']);
|
||||
|
||||
// $name = data_get($application, 'name');
|
||||
// $fqdn = data_get($application, 'fqdn');
|
||||
|
||||
// $containerName = $name ? "$name ($fqdn)" : $fqdn;
|
||||
|
||||
// $projectUuid = data_get($application, 'environment.project.uuid');
|
||||
// $applicationUuid = data_get($application, 'uuid');
|
||||
// $environment = data_get($application, 'environment.name');
|
||||
|
||||
// if ($projectUuid && $applicationUuid && $environment) {
|
||||
// $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid;
|
||||
// } else {
|
||||
// $url = null;
|
||||
// }
|
||||
|
||||
// // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
// }
|
||||
// $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
|
||||
// foreach ($notRunningApplicationPreviews as $previewId) {
|
||||
// $preview = $previews->where('id', $previewId)->first();
|
||||
// if (str($preview->status)->startsWith('exited')) {
|
||||
// continue;
|
||||
// }
|
||||
// $preview->update(['status' => 'exited']);
|
||||
|
||||
// $name = data_get($preview, 'name');
|
||||
// $fqdn = data_get($preview, 'fqdn');
|
||||
|
||||
// $containerName = $name ? "$name ($fqdn)" : $fqdn;
|
||||
|
||||
// $projectUuid = data_get($preview, 'application.environment.project.uuid');
|
||||
// $environmentName = data_get($preview, 'application.environment.name');
|
||||
// $applicationUuid = data_get($preview, 'application.uuid');
|
||||
|
||||
// if ($projectUuid && $applicationUuid && $environmentName) {
|
||||
// $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid;
|
||||
// } else {
|
||||
// $url = null;
|
||||
// }
|
||||
|
||||
// // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
// }
|
||||
// $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
|
||||
// foreach ($notRunningDatabases as $database) {
|
||||
// $database = $databases->where('id', $database)->first();
|
||||
// if (str($database->status)->startsWith('exited')) {
|
||||
// continue;
|
||||
// }
|
||||
// $database->update(['status' => 'exited']);
|
||||
|
||||
// $name = data_get($database, 'name');
|
||||
// $fqdn = data_get($database, 'fqdn');
|
||||
|
||||
// $containerName = $name;
|
||||
|
||||
// $projectUuid = data_get($database, 'environment.project.uuid');
|
||||
// $environmentName = data_get($database, 'environment.name');
|
||||
// $databaseUuid = data_get($database, 'uuid');
|
||||
|
||||
// if ($projectUuid && $databaseUuid && $environmentName) {
|
||||
// $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid;
|
||||
// } else {
|
||||
// $url = null;
|
||||
// }
|
||||
// // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
// }
|
||||
|
||||
// // Check if proxy is running
|
||||
// $this->server->proxyType();
|
||||
// $foundProxyContainer = $this->containers->filter(function ($value, $key) {
|
||||
// if ($this->server->isSwarm()) {
|
||||
// // TODO: fix this with sentinel
|
||||
// return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
// } else {
|
||||
// return data_get($value, 'name') === 'coolify-proxy';
|
||||
// }
|
||||
// })->first();
|
||||
// if (! $foundProxyContainer) {
|
||||
// try {
|
||||
// $shouldStart = CheckProxy::run($this->server);
|
||||
// if ($shouldStart) {
|
||||
// StartProxy::run($this->server, false);
|
||||
// $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
// }
|
||||
// } catch (\Throwable $e) {
|
||||
// ray($e);
|
||||
// }
|
||||
// } else {
|
||||
// $this->server->proxy->status = data_get($foundProxyContainer, 'state');
|
||||
// $this->server->save();
|
||||
// $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
// instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
// }
|
||||
// } catch (\Exception $e) {
|
||||
// // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
|
||||
// ray($e->getMessage());
|
||||
|
||||
// return handleError($e);
|
||||
// }
|
||||
// }
|
||||
|
||||
private function old_way()
|
||||
{
|
||||
if ($this->containers === null) {
|
||||
['containers' => $this->containers,'containerReplicates' => $this->containerReplicates] = $this->server->getContainers();
|
||||
['containers' => $this->containers, 'containerReplicates' => $this->containerReplicates] = $this->server->getContainers();
|
||||
}
|
||||
|
||||
if (is_null($this->containers)) {
|
||||
@@ -425,6 +108,8 @@ class GetContainersStatus
|
||||
$statusFromDb = $preview->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$preview->update(['status' => $containerStatus]);
|
||||
} else {
|
||||
$preview->update(['last_online_at' => now()]);
|
||||
}
|
||||
} else {
|
||||
//Notify user that this container should not be there.
|
||||
@@ -436,6 +121,8 @@ class GetContainersStatus
|
||||
$statusFromDb = $application->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$application->update(['status' => $containerStatus]);
|
||||
} else {
|
||||
$application->update(['last_online_at' => now()]);
|
||||
}
|
||||
} else {
|
||||
//Notify user that this container should not be there.
|
||||
@@ -478,7 +165,10 @@ class GetContainersStatus
|
||||
$statusFromDb = $database->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$database->update(['status' => $containerStatus]);
|
||||
} else {
|
||||
$database->update(['last_online_at' => now()]);
|
||||
}
|
||||
|
||||
if ($isPublic) {
|
||||
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
|
||||
if ($this->server->isSwarm()) {
|
||||
@@ -489,7 +179,7 @@ class GetContainersStatus
|
||||
})->first();
|
||||
if (! $foundTcpProxy) {
|
||||
StartDatabaseProxy::run($database);
|
||||
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
|
||||
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -520,6 +210,8 @@ class GetContainersStatus
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
// ray('Updating status: ' . $containerStatus);
|
||||
$service->update(['status' => $containerStatus]);
|
||||
} else {
|
||||
$service->update(['last_online_at' => now()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -650,32 +342,5 @@ class GetContainersStatus
|
||||
}
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
|
||||
if (! $this->server->proxySet() || $this->server->proxy->force_stop) {
|
||||
return;
|
||||
}
|
||||
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
}
|
||||
} else {
|
||||
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
||||
$this->server->save();
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,11 @@ use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*
|
||||
@@ -32,7 +31,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
'max:255',
|
||||
Rule::unique(User::class),
|
||||
],
|
||||
'password' => $this->passwordRules(),
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
])->validate();
|
||||
|
||||
if (User::count() == 0) {
|
||||
@@ -41,7 +40,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
$user = User::create([
|
||||
'id' => 0,
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email' => strtolower($input['email']),
|
||||
'password' => Hash::make($input['password']),
|
||||
]);
|
||||
$team = $user->teams()->first();
|
||||
@@ -53,7 +52,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
} else {
|
||||
$user = User::create([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email' => strtolower($input['email']),
|
||||
'password' => Hash::make($input['password']),
|
||||
]);
|
||||
$team = $user->teams()->first();
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use Laravel\Fortify\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', new Password, 'confirmed'];
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,11 @@ namespace App\Actions\Fortify;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
@@ -19,7 +18,7 @@ class ResetUserPassword implements ResetsUserPasswords
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
|
||||
@@ -5,12 +5,11 @@ namespace App\Actions\Fortify;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
|
||||
class UpdateUserPassword implements UpdatesUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and update the user's password.
|
||||
*
|
||||
@@ -20,7 +19,7 @@ class UpdateUserPassword implements UpdatesUserPasswords
|
||||
{
|
||||
Validator::make($input, [
|
||||
'current_password' => ['required', 'string', 'current_password:web'],
|
||||
'password' => $this->passwordRules(),
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
], [
|
||||
'current_password.current_password' => __('The provided password does not match your current password.'),
|
||||
])->validateWithBag('updatePassword');
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\License;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class CheckResaleLicense
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$settings = instanceSettings();
|
||||
if (isDev()) {
|
||||
$settings->update([
|
||||
'is_resale_license_active' => true,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
// if (!$settings->resale_license) {
|
||||
// return;
|
||||
// }
|
||||
$base_url = config('coolify.license_url');
|
||||
$instance_id = config('app.id');
|
||||
|
||||
ray("Checking license key against $base_url/lemon/validate");
|
||||
$data = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
])->get("$base_url/lemon/validate", [
|
||||
'license_key' => $settings->resale_license,
|
||||
'instance_id' => $instance_id,
|
||||
])->json();
|
||||
if (data_get($data, 'valid') === true && data_get($data, 'license_key.status') === 'active') {
|
||||
ray('Valid & active license key');
|
||||
$settings->update([
|
||||
'is_resale_license_active' => true,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
$data = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
])->get("$base_url/lemon/activate", [
|
||||
'license_key' => $settings->resale_license,
|
||||
'instance_id' => $instance_id,
|
||||
])->json();
|
||||
if (data_get($data, 'activated') === true) {
|
||||
ray('Activated license key');
|
||||
$settings->update([
|
||||
'is_resale_license_active' => true,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
if (data_get($data, 'license_key.status') === 'active') {
|
||||
throw new \Exception('Invalid license key.');
|
||||
}
|
||||
throw new \Exception('Cannot activate license key.');
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
$settings->update([
|
||||
'resale_license' => null,
|
||||
'is_resale_license_active' => false,
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Actions\Proxy;
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
@@ -29,7 +30,7 @@ class CheckProxy
|
||||
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
|
||||
return false;
|
||||
}
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
|
||||
if (! $uptime) {
|
||||
throw new \Exception($error);
|
||||
}
|
||||
@@ -88,7 +89,7 @@ class CheckProxy
|
||||
$portsToCheck = [];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
Log::error('Error checking proxy: '.$e->getMessage());
|
||||
}
|
||||
if (count($portsToCheck) === 0) {
|
||||
return false;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStarted;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -13,67 +14,65 @@ class StartProxy
|
||||
|
||||
public function handle(Server $server, bool $async = true, bool $force = false): string|Activity
|
||||
{
|
||||
try {
|
||||
$proxyType = $server->proxyType();
|
||||
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) {
|
||||
return 'OK';
|
||||
$proxyType = $server->proxyType();
|
||||
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) {
|
||||
return 'OK';
|
||||
}
|
||||
$commands = collect([]);
|
||||
$proxy_path = $server->proxyPath();
|
||||
$configuration = CheckConfiguration::run($server);
|
||||
if (! $configuration) {
|
||||
throw new \Exception('Configuration is not synced');
|
||||
}
|
||||
SaveConfiguration::run($server, $configuration);
|
||||
$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()) {
|
||||
$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',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
} else {
|
||||
if (isDev()) {
|
||||
if ($proxyType === ProxyTypes::CADDY->value) {
|
||||
$proxy_path = '/data/coolify/proxy/caddy';
|
||||
}
|
||||
}
|
||||
$commands = collect([]);
|
||||
$proxy_path = $server->proxyPath();
|
||||
$configuration = CheckConfiguration::run($server);
|
||||
if (! $configuration) {
|
||||
throw new \Exception('Configuration is not synced');
|
||||
}
|
||||
SaveConfiguration::run($server, $configuration);
|
||||
$docker_compose_yml_base64 = base64_encode($configuration);
|
||||
$server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
|
||||
$caddyfile = 'import /dynamic/*.caddy';
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
"cd $proxy_path",
|
||||
"echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Pulling docker image.'",
|
||||
'docker compose pull',
|
||||
'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
|
||||
" echo 'Stopping and removing existing coolify-proxy.'",
|
||||
' docker rm -f coolify-proxy || true',
|
||||
" echo 'Successfully stopped and removed existing coolify-proxy.'",
|
||||
'fi',
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d --remove-orphans',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
$commands = $commands->merge(connectProxyToNetworks($server));
|
||||
}
|
||||
|
||||
if ($async) {
|
||||
return remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server);
|
||||
} else {
|
||||
instant_remote_process($commands, $server);
|
||||
$server->proxy->set('status', 'running');
|
||||
$server->proxy->set('type', $proxyType);
|
||||
$server->save();
|
||||
if ($server->isSwarm()) {
|
||||
$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',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
} else {
|
||||
$caddfile = 'import /dynamic/*.caddy';
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
"cd $proxy_path",
|
||||
"echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Pulling docker image.'",
|
||||
'docker compose pull',
|
||||
'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
|
||||
" echo 'Stopping and removing existing coolify-proxy.'",
|
||||
' docker rm -f coolify-proxy || true',
|
||||
" echo 'Successfully stopped and removed existing coolify-proxy.'",
|
||||
'fi',
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d --remove-orphans',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
$commands = $commands->merge(connectProxyToNetworks($server));
|
||||
}
|
||||
ProxyStarted::dispatch($server);
|
||||
|
||||
if ($async) {
|
||||
$activity = remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server);
|
||||
|
||||
return $activity;
|
||||
} else {
|
||||
instant_remote_process($commands, $server);
|
||||
$server->proxy->set('status', 'running');
|
||||
$server->proxy->set('type', $proxyType);
|
||||
$server->save();
|
||||
ProxyStarted::dispatch($server);
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
throw $e;
|
||||
return 'OK';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ class CleanupDocker
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
$helperImageVersion = data_get($settings, 'helper_version');
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$helperImage = config('constants.coolify.helper_image');
|
||||
$helperImageWithVersion = "$helperImage:$helperImageVersion";
|
||||
|
||||
$commands = [
|
||||
|
||||
@@ -40,7 +40,6 @@ class ConfigureCloudflared
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
$server->settings->is_cloudflare_tunnel = false;
|
||||
$server->settings->save();
|
||||
throw $e;
|
||||
@@ -51,7 +50,6 @@ class ConfigureCloudflared
|
||||
'rm -fr /tmp/cloudflared',
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
app/Actions/Server/DeleteServer.php
Normal file
17
app/Actions/Server/DeleteServer.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class DeleteServer
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
StopSentinel::run($server);
|
||||
$server->forceDelete();
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,11 @@ class InstallDocker
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$dockerVersion = config('constants.docker.minimum_required_version');
|
||||
$supported_os_type = $server->validateOS();
|
||||
if (! $supported_os_type) {
|
||||
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');
|
||||
}
|
||||
ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type);
|
||||
$dockerVersion = '24.0';
|
||||
$config = base64_encode('{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
|
||||
41
app/Actions/Server/ResourcesCheck.php
Normal file
41
app/Actions/Server/ResourcesCheck.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class ResourcesCheck
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$seconds = 60;
|
||||
try {
|
||||
Application::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
ServiceApplication::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
ServiceDatabase::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
StandalonePostgresql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
StandaloneRedis::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
StandaloneMongodb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
StandaloneMysql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
StandaloneMariadb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
StandaloneKeydb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
StandaloneDragonfly::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
StandaloneClickhouse::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
app/Actions/Server/RestartContainer.php
Normal file
16
app/Actions/Server/RestartContainer.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class RestartContainer
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, string $containerName)
|
||||
{
|
||||
$server->restartContainer($containerName);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,6 @@ class RunCommand
|
||||
|
||||
public function handle(Server $server, $command)
|
||||
{
|
||||
$activity = remote_process(command: [$command], server: $server, ignore_errors: true, type: ActivityTypes::COMMAND->value);
|
||||
|
||||
return $activity;
|
||||
return remote_process(command: [$command], server: $server, ignore_errors: true, type: ActivityTypes::COMMAND->value);
|
||||
}
|
||||
}
|
||||
|
||||
267
app/Actions/Server/ServerCheck.php
Normal file
267
app/Actions/Server/ServerCheck.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Jobs\CheckAndStartSentinelJob;
|
||||
use App\Jobs\ServerStorageCheckJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Notifications\Container\ContainerRestarted;
|
||||
use Illuminate\Support\Arr;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class ServerCheck
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public Server $server;
|
||||
|
||||
public bool $isSentinel = false;
|
||||
|
||||
public $containers;
|
||||
|
||||
public $databases;
|
||||
|
||||
public function handle(Server $server, $data = null)
|
||||
{
|
||||
$this->server = $server;
|
||||
try {
|
||||
if ($this->server->isFunctional() === false) {
|
||||
return 'Server is not functional.';
|
||||
}
|
||||
|
||||
if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) {
|
||||
|
||||
if (isset($data)) {
|
||||
$data = collect($data);
|
||||
|
||||
$this->server->sentinelHeartbeat();
|
||||
|
||||
$this->containers = collect(data_get($data, 'containers'));
|
||||
|
||||
$filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage');
|
||||
ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot);
|
||||
|
||||
$containerReplicates = null;
|
||||
$this->isSentinel = true;
|
||||
} else {
|
||||
['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
|
||||
// ServerStorageCheckJob::dispatch($this->server);
|
||||
}
|
||||
|
||||
if (is_null($this->containers)) {
|
||||
return 'No containers found.';
|
||||
}
|
||||
|
||||
if (isset($containerReplicates)) {
|
||||
foreach ($containerReplicates as $containerReplica) {
|
||||
$name = data_get($containerReplica, 'Name');
|
||||
$this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) {
|
||||
if (data_get($container, 'Spec.Name') === $name) {
|
||||
$replicas = data_get($containerReplica, 'Replicas');
|
||||
$running = str($replicas)->explode('/')[0];
|
||||
$total = str($replicas)->explode('/')[1];
|
||||
if ($running === $total) {
|
||||
data_set($container, 'State.Status', 'running');
|
||||
data_set($container, 'State.Health.Status', 'healthy');
|
||||
} else {
|
||||
data_set($container, 'State.Status', 'starting');
|
||||
data_set($container, 'State.Health.Status', 'unhealthy');
|
||||
}
|
||||
}
|
||||
|
||||
return $container;
|
||||
});
|
||||
}
|
||||
}
|
||||
$this->checkContainers();
|
||||
|
||||
if ($this->server->isSentinelEnabled() && $this->isSentinel === false) {
|
||||
CheckAndStartSentinelJob::dispatch($this->server);
|
||||
}
|
||||
|
||||
if ($this->server->isLogDrainEnabled()) {
|
||||
$this->checkLogDrainContainer();
|
||||
}
|
||||
|
||||
if ($this->server->proxySet() && ! $this->server->proxy->force_stop) {
|
||||
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
} else {
|
||||
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
||||
$this->server->save();
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkLogDrainContainer()
|
||||
{
|
||||
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
|
||||
return data_get($value, 'Name') === '/coolify-log-drain';
|
||||
})->first();
|
||||
if ($foundLogDrainContainer) {
|
||||
$status = data_get($foundLogDrainContainer, 'State.Status');
|
||||
if ($status !== 'running') {
|
||||
StartLogDrain::dispatch($this->server);
|
||||
}
|
||||
} else {
|
||||
StartLogDrain::dispatch($this->server);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkContainers()
|
||||
{
|
||||
foreach ($this->containers as $container) {
|
||||
if ($this->isSentinel) {
|
||||
$labels = Arr::undot(data_get($container, 'labels'));
|
||||
} else {
|
||||
if ($this->server->isSwarm()) {
|
||||
$labels = Arr::undot(data_get($container, 'Spec.Labels'));
|
||||
} else {
|
||||
$labels = Arr::undot(data_get($container, 'Config.Labels'));
|
||||
}
|
||||
}
|
||||
$managed = data_get($labels, 'coolify.managed');
|
||||
if (! $managed) {
|
||||
continue;
|
||||
}
|
||||
$uuid = data_get($labels, 'coolify.name');
|
||||
if (! $uuid) {
|
||||
$uuid = data_get($labels, 'com.docker.compose.service');
|
||||
}
|
||||
|
||||
if ($this->isSentinel) {
|
||||
$containerStatus = data_get($container, 'state');
|
||||
$containerHealth = data_get($container, 'health_status');
|
||||
} else {
|
||||
$containerStatus = data_get($container, 'State.Status');
|
||||
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
|
||||
}
|
||||
$containerStatus = "$containerStatus ($containerHealth)";
|
||||
|
||||
$applicationId = data_get($labels, 'coolify.applicationId');
|
||||
$serviceId = data_get($labels, 'coolify.serviceId');
|
||||
$databaseId = data_get($labels, 'coolify.databaseId');
|
||||
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
|
||||
|
||||
if ($applicationId) {
|
||||
// Application
|
||||
if ($pullRequestId != 0) {
|
||||
if (str($applicationId)->contains('-')) {
|
||||
$applicationId = str($applicationId)->before('-');
|
||||
}
|
||||
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
|
||||
if ($preview) {
|
||||
$preview->update(['status' => $containerStatus]);
|
||||
}
|
||||
} else {
|
||||
$application = Application::where('id', $applicationId)->first();
|
||||
if ($application) {
|
||||
$application->update([
|
||||
'status' => $containerStatus,
|
||||
'last_online_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
} elseif (isset($serviceId)) {
|
||||
// Service
|
||||
$subType = data_get($labels, 'coolify.service.subType');
|
||||
$subId = data_get($labels, 'coolify.service.subId');
|
||||
$service = Service::where('id', $serviceId)->first();
|
||||
if (! $service) {
|
||||
continue;
|
||||
}
|
||||
if ($subType === 'application') {
|
||||
$service = ServiceApplication::where('id', $subId)->first();
|
||||
} else {
|
||||
$service = ServiceDatabase::where('id', $subId)->first();
|
||||
}
|
||||
if ($service) {
|
||||
$service->update([
|
||||
'status' => $containerStatus,
|
||||
'last_online_at' => now(),
|
||||
]);
|
||||
if ($subType === 'database') {
|
||||
$isPublic = data_get($service, 'is_public');
|
||||
if ($isPublic) {
|
||||
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
|
||||
if ($this->isSentinel) {
|
||||
return data_get($value, 'name') === $uuid.'-proxy';
|
||||
} else {
|
||||
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||
} else {
|
||||
return data_get($value, 'Name') === "/$uuid-proxy";
|
||||
}
|
||||
}
|
||||
})->first();
|
||||
if (! $foundTcpProxy) {
|
||||
StartDatabaseProxy::run($service);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Database
|
||||
if (is_null($this->databases)) {
|
||||
$this->databases = $this->server->databases();
|
||||
}
|
||||
$database = $this->databases->where('uuid', $uuid)->first();
|
||||
if ($database) {
|
||||
$database->update([
|
||||
'status' => $containerStatus,
|
||||
'last_online_at' => now(),
|
||||
]);
|
||||
|
||||
$isPublic = data_get($database, 'is_public');
|
||||
if ($isPublic) {
|
||||
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
|
||||
if ($this->isSentinel) {
|
||||
return data_get($value, 'name') === $uuid.'-proxy';
|
||||
} else {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||
} else {
|
||||
|
||||
return data_get($value, 'Name') === "/$uuid-proxy";
|
||||
}
|
||||
}
|
||||
})->first();
|
||||
if (! $foundTcpProxy) {
|
||||
StartDatabaseProxy::run($database);
|
||||
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,20 +5,26 @@ namespace App\Actions\Server;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class InstallLogDrain
|
||||
class StartLogDrain
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
if ($server->settings->is_logdrain_newrelic_enabled) {
|
||||
$type = 'newrelic';
|
||||
StopLogDrain::run($server);
|
||||
} elseif ($server->settings->is_logdrain_highlight_enabled) {
|
||||
$type = 'highlight';
|
||||
StopLogDrain::run($server);
|
||||
} elseif ($server->settings->is_logdrain_axiom_enabled) {
|
||||
$type = 'axiom';
|
||||
StopLogDrain::run($server);
|
||||
} elseif ($server->settings->is_logdrain_custom_enabled) {
|
||||
$type = 'custom';
|
||||
StopLogDrain::run($server);
|
||||
} else {
|
||||
$type = 'none';
|
||||
}
|
||||
@@ -151,6 +157,8 @@ services:
|
||||
- ./parsers.conf:/parsers.conf
|
||||
ports:
|
||||
- 127.0.0.1:24224:24224
|
||||
labels:
|
||||
- coolify.managed=true
|
||||
restart: unless-stopped
|
||||
');
|
||||
$readme = base64_encode('# New Relic Log Drain
|
||||
@@ -163,7 +171,7 @@ Files:
|
||||
');
|
||||
$license_key = $server->settings->logdrain_newrelic_license_key;
|
||||
$base_uri = $server->settings->logdrain_newrelic_base_uri;
|
||||
$base_path = config('coolify.base_config_path');
|
||||
$base_path = config('constants.coolify.base_config_path');
|
||||
|
||||
$config_path = $base_path.'/log-drains';
|
||||
$fluent_bit_config = $config_path.'/fluent-bit.conf';
|
||||
@@ -202,10 +210,8 @@ Files:
|
||||
throw new \Exception('Unknown log drain type.');
|
||||
}
|
||||
$restart_command = [
|
||||
"echo 'Stopping old Fluent Bit'",
|
||||
"cd $config_path && docker compose down --remove-orphans || true",
|
||||
"echo 'Starting Fluent Bit'",
|
||||
"cd $config_path && docker compose up -d --remove-orphans",
|
||||
"cd $config_path && docker compose up -d",
|
||||
];
|
||||
$command = array_merge($command, $add_envs_command, $restart_command);
|
||||
|
||||
@@ -9,18 +9,57 @@ class StartSentinel
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, $version = 'latest', bool $restart = false)
|
||||
public function handle(Server $server, bool $restart = false, ?string $latestVersion = null)
|
||||
{
|
||||
if ($server->isSwarm() || $server->isBuildServer()) {
|
||||
return;
|
||||
}
|
||||
if ($restart) {
|
||||
StopSentinel::run($server);
|
||||
}
|
||||
$metrics_history = $server->settings->metrics_history_days;
|
||||
$refresh_rate = $server->settings->metrics_refresh_rate_seconds;
|
||||
$token = $server->settings->metrics_token;
|
||||
$version = $latestVersion ?? get_latest_sentinel_version();
|
||||
$metricsHistory = data_get($server, 'settings.sentinel_metrics_history_days');
|
||||
$refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
|
||||
$pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds');
|
||||
$token = data_get($server, 'settings.sentinel_token');
|
||||
$endpoint = data_get($server, 'settings.sentinel_custom_url');
|
||||
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
|
||||
$mountDir = '/data/coolify/sentinel';
|
||||
$image = "ghcr.io/coollabsio/sentinel:$version";
|
||||
if (! $endpoint) {
|
||||
throw new \Exception('You should set FQDN in Instance Settings.');
|
||||
}
|
||||
$environments = [
|
||||
'TOKEN' => $token,
|
||||
'DEBUG' => $debug ? 'true' : 'false',
|
||||
'PUSH_ENDPOINT' => $endpoint,
|
||||
'PUSH_INTERVAL_SECONDS' => $pushInterval,
|
||||
'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false',
|
||||
'COLLECTOR_REFRESH_RATE_SECONDS' => $refreshRate,
|
||||
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metricsHistory,
|
||||
];
|
||||
$labels = [
|
||||
'coolify.managed' => 'true',
|
||||
];
|
||||
if (isDev()) {
|
||||
// data_set($environments, 'DEBUG', 'true');
|
||||
// $image = 'sentinel';
|
||||
$mountDir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel';
|
||||
}
|
||||
$dockerEnvironments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"';
|
||||
$dockerLabels = implode(' ', array_map(fn ($key, $value) => "$key=$value", array_keys($labels), $labels));
|
||||
$dockerCommand = "docker run -d $dockerEnvironments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mountDir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway --label $dockerLabels $image";
|
||||
|
||||
instant_remote_process([
|
||||
"docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version",
|
||||
'chown -R 9999:root /data/coolify/metrics /data/coolify/logs',
|
||||
'chmod -R 700 /data/coolify/metrics /data/coolify/logs',
|
||||
], $server, true);
|
||||
'docker rm -f coolify-sentinel || true',
|
||||
"mkdir -p $mountDir",
|
||||
$dockerCommand,
|
||||
"chown -R 9999:root $mountDir",
|
||||
"chmod -R 700 $mountDir",
|
||||
], $server);
|
||||
|
||||
$server->settings->is_sentinel_enabled = true;
|
||||
$server->settings->save();
|
||||
$server->sentinelHeartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class StopLogDrain
|
||||
public function handle(Server $server)
|
||||
{
|
||||
try {
|
||||
return instant_remote_process(['docker rm -f coolify-log-drain || true'], $server);
|
||||
return instant_remote_process(['docker rm -f coolify-log-drain'], $server, false);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ class StopSentinel
|
||||
public function handle(Server $server)
|
||||
{
|
||||
instant_remote_process(['docker rm -f coolify-sentinel'], $server, false);
|
||||
$server->sentinelHeartbeat(isReset: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Actions\Server;
|
||||
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class UpdateCoolify
|
||||
@@ -18,49 +19,38 @@ class UpdateCoolify
|
||||
|
||||
public function handle($manual_update = false)
|
||||
{
|
||||
try {
|
||||
$settings = instanceSettings();
|
||||
$this->server = Server::find(0);
|
||||
if (! $this->server) {
|
||||
if (isDev()) {
|
||||
Sleep::for(10)->seconds();
|
||||
|
||||
return;
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
$this->server = Server::find(0);
|
||||
if (! $this->server) {
|
||||
return;
|
||||
}
|
||||
CleanupDocker::dispatch($this->server);
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
if (! $manual_update) {
|
||||
if (! $settings->is_auto_update_enabled) {
|
||||
return;
|
||||
}
|
||||
CleanupDocker::dispatch($this->server)->onQueue('high');
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('version');
|
||||
if (! $manual_update) {
|
||||
if (! $settings->is_auto_update_enabled) {
|
||||
return;
|
||||
}
|
||||
if ($this->latestVersion === $this->currentVersion) {
|
||||
return;
|
||||
}
|
||||
if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
|
||||
return;
|
||||
}
|
||||
if ($this->latestVersion === $this->currentVersion) {
|
||||
return;
|
||||
}
|
||||
if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
|
||||
return;
|
||||
}
|
||||
$this->update();
|
||||
$settings->new_version_available = false;
|
||||
$settings->save();
|
||||
} catch (\Throwable $e) {
|
||||
throw $e;
|
||||
}
|
||||
$this->update();
|
||||
$settings->new_version_available = false;
|
||||
$settings->save();
|
||||
}
|
||||
|
||||
private function update()
|
||||
{
|
||||
if (isDev()) {
|
||||
remote_process([
|
||||
'sleep 10',
|
||||
], $this->server);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$all_servers = Server::all();
|
||||
$servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
|
||||
foreach ($servers as $server) {
|
||||
PullHelperImageJob::dispatch($server);
|
||||
}
|
||||
PullHelperImageJob::dispatch($this->server);
|
||||
|
||||
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ class ValidateServer
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public ?string $uptime = null;
|
||||
|
||||
public ?string $error = null;
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Actions\Service;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class DeleteService
|
||||
@@ -39,8 +40,8 @@ class DeleteService
|
||||
if (! empty($commands)) {
|
||||
foreach ($commands as $command) {
|
||||
$result = instant_remote_process([$command], $server, false);
|
||||
if ($result !== 0) {
|
||||
ray("Failed to execute: $command");
|
||||
if ($result !== null && $result !== 0) {
|
||||
Log::error('Error deleting volumes: '.$result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ class RestartService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Service $service)
|
||||
{
|
||||
StopService::run($service);
|
||||
|
||||
@@ -10,9 +10,10 @@ class StartService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Service $service)
|
||||
{
|
||||
ray('Starting service: '.$service->name);
|
||||
$service->saveComposeConfigs();
|
||||
$commands[] = 'cd '.$service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
@@ -34,8 +35,7 @@ class StartService
|
||||
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
|
||||
}
|
||||
}
|
||||
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
|
||||
|
||||
return $activity;
|
||||
return remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ class StopService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
@@ -28,8 +30,6 @@ class StopService
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CleanupDatabase extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:database {--yes}';
|
||||
protected $signature = 'cleanup:database {--yes} {--keep-days=}';
|
||||
|
||||
protected $description = 'Cleanup database';
|
||||
|
||||
@@ -20,9 +20,9 @@ class CleanupDatabase extends Command
|
||||
}
|
||||
if (isCloud()) {
|
||||
// Later on we can increase this to 180 days or dynamically set
|
||||
$keep_days = 60;
|
||||
$keep_days = $this->option('keep-days') ?? 60;
|
||||
} else {
|
||||
$keep_days = 60;
|
||||
$keep_days = $this->option('keep-days') ?? 60;
|
||||
}
|
||||
echo "Keep days: $keep_days\n";
|
||||
// Cleanup failed jobs table
|
||||
@@ -64,6 +64,5 @@ class CleanupDatabase extends Command
|
||||
if ($this->option('yes')) {
|
||||
$webhooks->delete();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ class CleanupRedis extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Cleanup Redis keys.\n";
|
||||
$prefix = config('database.redis.options.prefix');
|
||||
|
||||
$keys = Redis::connection()->keys('*:laravel*');
|
||||
@@ -26,6 +25,5 @@ class CleanupRedis extends Command
|
||||
collect($queueOverlaps)->each(function ($key) {
|
||||
Redis::connection()->del($key);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,14 +30,11 @@ class CleanupStuckedResources extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
ray('Running cleanup stucked resources.');
|
||||
echo "Running cleanup stucked resources.\n";
|
||||
$this->cleanup_stucked_resources();
|
||||
}
|
||||
|
||||
private function cleanup_stucked_resources()
|
||||
{
|
||||
|
||||
try {
|
||||
$servers = Server::all()->filter(function ($server) {
|
||||
return $server->isFunctional();
|
||||
|
||||
@@ -18,7 +18,6 @@ class CleanupUnreachableServers extends Command
|
||||
if ($servers->count() > 0) {
|
||||
foreach ($servers as $server) {
|
||||
echo "Cleanup unreachable server ($server->id) with name $server->name";
|
||||
// send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
|
||||
$server->update([
|
||||
'ip' => '1.2.3.4',
|
||||
]);
|
||||
|
||||
49
app/Console/Commands/CloudCheckSubscription.php
Normal file
49
app/Console/Commands/CloudCheckSubscription.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CloudCheckSubscription extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'cloud:check-subscription';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check Cloud subscriptions';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
||||
$activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get();
|
||||
foreach ($activeSubscribers as $team) {
|
||||
$stripeSubscriptionId = $team->subscription->stripe_subscription_id;
|
||||
$stripeInvoicePaid = $team->subscription->stripe_invoice_paid;
|
||||
$stripeCustomerId = $team->subscription->stripe_customer_id;
|
||||
if (! $stripeSubscriptionId) {
|
||||
echo "Team {$team->id} has no subscription, but invoice status is: {$stripeInvoicePaid}\n";
|
||||
echo "Link on Stripe: https://dashboard.stripe.com/customers/{$stripeCustomerId}\n";
|
||||
|
||||
continue;
|
||||
}
|
||||
$subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId);
|
||||
if ($subscription->status === 'active') {
|
||||
continue;
|
||||
}
|
||||
echo "Subscription {$stripeSubscriptionId} is not active ({$subscription->status})\n";
|
||||
echo "Link on Stripe: https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ class CloudCleanupSubscriptions extends Command
|
||||
|
||||
return;
|
||||
}
|
||||
ray()->clearAll();
|
||||
$this->info('Cleaning up subcriptions teams');
|
||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
||||
|
||||
@@ -37,7 +36,7 @@ class CloudCleanupSubscriptions extends Command
|
||||
}
|
||||
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
|
||||
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
|
||||
$this->info("Resetting invoice paid status for team {$team->id} {$team->name}");
|
||||
$this->info("Resetting invoice paid status for team {$team->id}");
|
||||
|
||||
$team->subscription->update([
|
||||
'stripe_invoice_paid' => false,
|
||||
@@ -62,9 +61,9 @@ class CloudCleanupSubscriptions extends Command
|
||||
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
|
||||
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
|
||||
if (! $confirm) {
|
||||
$this->info("Skipping team {$team->id} {$team->name}");
|
||||
$this->info("Skipping team {$team->id}");
|
||||
} else {
|
||||
$this->info("Cancelling subscription for team {$team->id} {$team->name}");
|
||||
$this->info("Cancelling subscription for team {$team->id}");
|
||||
$team->subscription->update([
|
||||
'stripe_invoice_paid' => false,
|
||||
'stripe_trial_already_ended' => false,
|
||||
@@ -74,7 +73,6 @@ class CloudCleanupSubscriptions extends Command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
@@ -96,6 +94,5 @@ class CloudCleanupSubscriptions extends Command
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\InstanceSettings;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Dev extends Command
|
||||
{
|
||||
@@ -25,26 +26,38 @@ class Dev extends Command
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function generateOpenApi()
|
||||
{
|
||||
// Generate OpenAPI documentation
|
||||
echo "Generating OpenAPI documentation.\n";
|
||||
$process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
|
||||
// https://github.com/OAI/OpenAPI-Specification/releases
|
||||
$process = Process::run([
|
||||
'/var/www/html/vendor/bin/openapi',
|
||||
'app',
|
||||
'-o',
|
||||
'openapi.yaml',
|
||||
'--version',
|
||||
'3.1.0',
|
||||
]);
|
||||
$error = $process->errorOutput();
|
||||
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
|
||||
$error = preg_replace('/^\h*\v+/m', '', $error);
|
||||
echo $error;
|
||||
echo $process->output();
|
||||
// Convert YAML to JSON
|
||||
$yaml = file_get_contents('openapi.yaml');
|
||||
$json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
|
||||
file_put_contents('openapi.json', $json);
|
||||
echo "Converted OpenAPI YAML to JSON.\n";
|
||||
}
|
||||
|
||||
public function init()
|
||||
{
|
||||
// Generate APP_KEY if not exists
|
||||
|
||||
if (empty(env('APP_KEY'))) {
|
||||
if (empty(config('app.key'))) {
|
||||
echo "Generating APP_KEY.\n";
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
@@ -63,7 +76,5 @@ class Dev extends Command
|
||||
} else {
|
||||
echo "Instance already initialized.\n";
|
||||
}
|
||||
// Set permissions
|
||||
Process::run(['chmod', '-R', 'o+rwx', '.']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,17 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\SendConfirmationForWaitlistJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
use App\Models\Waitlist;
|
||||
use App\Notifications\Application\DeploymentFailed;
|
||||
use App\Notifications\Application\DeploymentSuccess;
|
||||
use App\Notifications\Application\StatusChanged;
|
||||
use App\Notifications\Database\BackupFailed;
|
||||
use App\Notifications\Database\BackupSuccess;
|
||||
use App\Notifications\Database\DailyBackup;
|
||||
use App\Notifications\Test;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
@@ -65,8 +62,6 @@ class Emails extends Command
|
||||
'backup-success' => 'Database - Backup Success',
|
||||
'backup-failed' => 'Database - Backup Failed',
|
||||
// 'invitation-link' => 'Invitation Link',
|
||||
'waitlist-invitation-link' => 'Waitlist Invitation Link',
|
||||
'waitlist-confirmation' => 'Waitlist Confirmation',
|
||||
'realusers-before-trial' => 'REAL - Registered Users Before Trial without Subscription',
|
||||
'realusers-server-lost-connection' => 'REAL - Server Lost Connection',
|
||||
],
|
||||
@@ -121,28 +116,10 @@ class Emails extends Command
|
||||
$this->mail = (new Test)->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'database-backup-statuses-daily':
|
||||
$scheduled_backups = ScheduledDatabaseBackup::all();
|
||||
$databases = collect();
|
||||
foreach ($scheduled_backups as $scheduled_backup) {
|
||||
$last_days_backups = $scheduled_backup->get_last_days_backup_status();
|
||||
if ($last_days_backups->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
$failed = $last_days_backups->where('status', 'failed');
|
||||
$database = $scheduled_backup->database;
|
||||
$databases->put($database->name, [
|
||||
'failed_count' => $failed->count(),
|
||||
]);
|
||||
}
|
||||
$this->mail = (new DailyBackup($databases))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'application-deployment-success-daily':
|
||||
$applications = Application::all();
|
||||
foreach ($applications as $application) {
|
||||
$deployments = $application->get_last_days_deployments();
|
||||
ray($deployments);
|
||||
if ($deployments->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
@@ -206,7 +183,7 @@ class Emails extends Command
|
||||
'team_id' => 0,
|
||||
]);
|
||||
}
|
||||
$this->mail = (new BackupSuccess($backup, $db))->toMail();
|
||||
//$this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
// case 'invitation-link':
|
||||
@@ -223,23 +200,6 @@ class Emails extends Command
|
||||
// $this->mail = (new InvitationLink($user))->toMail();
|
||||
// $this->sendEmail();
|
||||
// break;
|
||||
case 'waitlist-invitation-link':
|
||||
$this->mail = new MailMessage;
|
||||
$this->mail->view('emails.waitlist-invitation', [
|
||||
'loginLink' => 'https://coolify.io',
|
||||
]);
|
||||
$this->mail->subject('Congratulations! You are invited to join Coolify Cloud.');
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'waitlist-confirmation':
|
||||
$found = Waitlist::where('email', $this->email)->first();
|
||||
if ($found) {
|
||||
SendConfirmationForWaitlistJob::dispatch($this->email, $found->uuid);
|
||||
} else {
|
||||
throw new Exception('Waitlist not found');
|
||||
}
|
||||
|
||||
break;
|
||||
case 'realusers-before-trial':
|
||||
$this->mail = new MailMessage;
|
||||
$this->mail->view('emails.before-trial-conversion');
|
||||
|
||||
@@ -12,8 +12,8 @@ class Horizon extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (config('coolify.is_horizon_enabled')) {
|
||||
$this->info('Horizon is enabled. Starting.');
|
||||
if (config('constants.horizon.is_horizon_enabled')) {
|
||||
$this->info('Horizon is enabled on this server.');
|
||||
$this->call('horizon');
|
||||
exit(0);
|
||||
} else {
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Actions\Server\StopSentinel;
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
@@ -24,6 +26,8 @@ class Init extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->optimize();
|
||||
|
||||
if (isCloud() && ! $this->option('force-cloud')) {
|
||||
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
|
||||
|
||||
@@ -32,16 +36,15 @@ class Init extends Command
|
||||
|
||||
$this->servers = Server::all();
|
||||
if (isCloud()) {
|
||||
|
||||
} else {
|
||||
$this->send_alive_signal();
|
||||
get_public_ips();
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
$this->disable_metrics();
|
||||
$this->replace_slash_in_environment_name();
|
||||
$this->restore_coolify_db_backup();
|
||||
$this->update_user_emails();
|
||||
//
|
||||
$this->update_traefik_labels();
|
||||
if (! isCloud() || $this->option('force-cloud')) {
|
||||
@@ -53,15 +56,29 @@ class Init extends Command
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
}
|
||||
$this->call('cleanup:redis');
|
||||
|
||||
$this->call('cleanup:stucked-resources');
|
||||
|
||||
try {
|
||||
$this->pullHelperImage();
|
||||
} catch (\Throwable $e) {
|
||||
//
|
||||
}
|
||||
|
||||
if (isCloud()) {
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
if ($response->successful()) {
|
||||
$services = $response->json();
|
||||
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
||||
try {
|
||||
$this->pullTemplatesFromCDN();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (! isCloud()) {
|
||||
try {
|
||||
$this->pullTemplatesFromCDN();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$localhost = $this->servers->where('id', 0)->first();
|
||||
$localhost->setupDynamicProxyConfiguration();
|
||||
@@ -69,8 +86,8 @@ class Init extends Command
|
||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
if (! is_null(env('AUTOUPDATE', null))) {
|
||||
if (env('AUTOUPDATE') == true) {
|
||||
if (! is_null(config('constants.coolify.autoupdate', null))) {
|
||||
if (config('constants.coolify.autoupdate') == true) {
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
@@ -79,17 +96,32 @@ class Init extends Command
|
||||
}
|
||||
}
|
||||
|
||||
private function disable_metrics()
|
||||
private function pullHelperImage()
|
||||
{
|
||||
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
|
||||
foreach ($this->servers as $server) {
|
||||
if ($server->settings->is_metrics_enabled === true) {
|
||||
$server->settings->update(['is_metrics_enabled' => false]);
|
||||
}
|
||||
if ($server->isFunctional()) {
|
||||
StopSentinel::dispatch($server);
|
||||
}
|
||||
}
|
||||
CheckHelperImageJob::dispatch();
|
||||
}
|
||||
|
||||
private function pullTemplatesFromCDN()
|
||||
{
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
if ($response->successful()) {
|
||||
$services = $response->json();
|
||||
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
||||
}
|
||||
}
|
||||
|
||||
private function optimize()
|
||||
{
|
||||
Artisan::call('optimize:clear');
|
||||
Artisan::call('optimize');
|
||||
}
|
||||
|
||||
private function update_user_emails()
|
||||
{
|
||||
try {
|
||||
User::whereRaw('email ~ \'[A-Z]\'')->get()->each(fn (User $user) => $user->update(['email' => strtolower($user->email)]));
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in updating user emails: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +152,6 @@ class Init extends Command
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +186,6 @@ class Init extends Command
|
||||
}
|
||||
}
|
||||
if ($commands->isNotEmpty()) {
|
||||
echo "Cleaning up unused networks from coolify proxy\n";
|
||||
remote_process(command: $commands, type: ActivityTypes::INLINE->value, server: $server, ignore_errors: false);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
@@ -166,7 +196,7 @@ class Init extends Command
|
||||
|
||||
private function restore_coolify_db_backup()
|
||||
{
|
||||
if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
|
||||
if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) {
|
||||
try {
|
||||
$database = StandalonePostgresql::withTrashed()->find(0);
|
||||
if ($database && $database->trashed()) {
|
||||
@@ -180,7 +210,7 @@ class Init extends Command
|
||||
'save_s3' => false,
|
||||
'frequency' => '0 0 * * *',
|
||||
'database_id' => $database->id,
|
||||
'database_type' => 'App\Models\StandalonePostgresql',
|
||||
'database_type' => \App\Models\StandalonePostgresql::class,
|
||||
'team_id' => 0,
|
||||
]);
|
||||
}
|
||||
@@ -194,19 +224,18 @@ class Init extends Command
|
||||
private function send_alive_signal()
|
||||
{
|
||||
$id = config('app.id');
|
||||
$version = config('version');
|
||||
$version = config('constants.coolify.version');
|
||||
$settings = instanceSettings();
|
||||
$do_not_track = data_get($settings, 'do_not_track');
|
||||
if ($do_not_track == true) {
|
||||
echo "Skipping alive as do_not_track is enabled\n";
|
||||
echo "Do_not_track is enabled\n";
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
|
||||
echo "I am alive!\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in alive: {$e->getMessage()}\n";
|
||||
echo "Error in sending live signal: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,8 +248,6 @@ class Init extends Command
|
||||
}
|
||||
$queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get();
|
||||
foreach ($queued_inprogress_deployments as $deployment) {
|
||||
ray($deployment->id, $deployment->status);
|
||||
echo "Cleaning up deployment: {$deployment->id}\n";
|
||||
$deployment->status = ApplicationDeploymentStatus::FAILED->value;
|
||||
$deployment->save();
|
||||
}
|
||||
@@ -231,7 +258,7 @@ class Init extends Command
|
||||
|
||||
private function replace_slash_in_environment_name()
|
||||
{
|
||||
if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
|
||||
if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {
|
||||
$environments = Environment::all();
|
||||
foreach ($environments as $environment) {
|
||||
if (str_contains($environment->name, '/')) {
|
||||
|
||||
@@ -36,8 +36,6 @@ class NotifyDemo extends Command
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ray($channel);
|
||||
}
|
||||
|
||||
private function showHelp()
|
||||
|
||||
@@ -15,12 +15,19 @@ class OpenApi extends Command
|
||||
{
|
||||
// Generate OpenAPI documentation
|
||||
echo "Generating OpenAPI documentation.\n";
|
||||
$process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
|
||||
// https://github.com/OAI/OpenAPI-Specification/releases
|
||||
$process = Process::run([
|
||||
'/var/www/html/vendor/bin/openapi',
|
||||
'app',
|
||||
'-o',
|
||||
'openapi.yaml',
|
||||
'--version',
|
||||
'3.1.0',
|
||||
]);
|
||||
$error = $process->errorOutput();
|
||||
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
|
||||
$error = preg_replace('/^\h*\v+/m', '', $error);
|
||||
echo $error;
|
||||
echo $process->output();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ class Scheduler extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (config('coolify.is_scheduler_enabled')) {
|
||||
$this->info('Scheduler is enabled. Starting.');
|
||||
if (config('constants.horizon.is_scheduler_enabled')) {
|
||||
$this->info('Scheduler is enabled on this server.');
|
||||
$this->call('schedule:work');
|
||||
exit(0);
|
||||
} else {
|
||||
|
||||
@@ -3,128 +3,85 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Arr;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ServicesGenerate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $signature = 'services:generate';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $description = 'Generate service-templates.yaml based on /templates/compose directory';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
|
||||
$files = array_filter($files, function ($file) {
|
||||
return strpos($file, '.yaml') !== false;
|
||||
});
|
||||
$serviceTemplatesJson = [];
|
||||
foreach ($files as $file) {
|
||||
$parsed = $this->process_file($file);
|
||||
if ($parsed) {
|
||||
$name = data_get($parsed, 'name');
|
||||
$parsed = data_forget($parsed, 'name');
|
||||
$serviceTemplatesJson[$name] = $parsed;
|
||||
}
|
||||
}
|
||||
$serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$serviceTemplatesJson = collect(array_merge(
|
||||
glob(base_path('templates/compose/*.yaml')),
|
||||
glob(base_path('templates/compose/*.yml'))
|
||||
))
|
||||
->mapWithKeys(function ($file): array {
|
||||
$file = basename($file);
|
||||
$parsed = $this->processFile($file);
|
||||
|
||||
return $parsed === false ? [] : [
|
||||
Arr::pull($parsed, 'name') => $parsed,
|
||||
];
|
||||
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function process_file($file)
|
||||
private function processFile(string $file): false|array
|
||||
{
|
||||
$serviceName = str($file)->before('.yaml')->value();
|
||||
$content = file_get_contents(base_path("templates/compose/$file"));
|
||||
// $this->info($content);
|
||||
$ignore = collect(preg_grep('/^# ignore:/', explode("\n", $content)))->values();
|
||||
if ($ignore->count() > 0) {
|
||||
$ignore = (bool) str($ignore[0])->after('# ignore:')->trim()->value();
|
||||
} else {
|
||||
$ignore = false;
|
||||
}
|
||||
if ($ignore) {
|
||||
|
||||
$data = collect(explode(PHP_EOL, $content))->mapWithKeys(function ($line): array {
|
||||
preg_match('/^#(?<key>.*):(?<value>.*)$/U', $line, $m);
|
||||
|
||||
return $m ? [trim($m['key']) => trim($m['value'])] : [];
|
||||
});
|
||||
|
||||
if (str($data->get('ignore'))->toBoolean()) {
|
||||
$this->info("Ignoring $file");
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info("Processing $file");
|
||||
$documentation = collect(preg_grep('/^# documentation:/', explode("\n", $content)))->values();
|
||||
if ($documentation->count() > 0) {
|
||||
$documentation = str($documentation[0])->after('# documentation:')->trim()->value();
|
||||
$documentation = str($documentation)->append('?utm_source=coolify.io');
|
||||
} else {
|
||||
$documentation = 'https://coolify.io/docs';
|
||||
}
|
||||
|
||||
$slogan = collect(preg_grep('/^# slogan:/', explode("\n", $content)))->values();
|
||||
if ($slogan->count() > 0) {
|
||||
$slogan = str($slogan[0])->after('# slogan:')->trim()->value();
|
||||
} else {
|
||||
$slogan = str($file)->headline()->value();
|
||||
}
|
||||
$logo = collect(preg_grep('/^# logo:/', explode("\n", $content)))->values();
|
||||
if ($logo->count() > 0) {
|
||||
$logo = str($logo[0])->after('# logo:')->trim()->value();
|
||||
} else {
|
||||
$logo = 'svgs/coolify.png';
|
||||
}
|
||||
$minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values();
|
||||
if ($minversion->count() > 0) {
|
||||
$minversion = str($minversion[0])->after('# minversion:')->trim()->value();
|
||||
} else {
|
||||
$minversion = '0.0.0';
|
||||
}
|
||||
$env_file = collect(preg_grep('/^# env_file:/', explode("\n", $content)))->values();
|
||||
if ($env_file->count() > 0) {
|
||||
$env_file = str($env_file[0])->after('# env_file:')->trim()->value();
|
||||
} else {
|
||||
$env_file = null;
|
||||
}
|
||||
$documentation = $data->get('documentation');
|
||||
$documentation = $documentation ? $documentation.'?utm_source=coolify.io' : 'https://coolify.io/docs';
|
||||
|
||||
$tags = collect(preg_grep('/^# tags:/', explode("\n", $content)))->values();
|
||||
if ($tags->count() > 0) {
|
||||
$tags = str($tags[0])->after('# tags:')->trim()->explode(',')->map(function ($tag) {
|
||||
return str($tag)->trim()->lower()->value();
|
||||
})->values();
|
||||
} else {
|
||||
$tags = null;
|
||||
}
|
||||
$port = collect(preg_grep('/^# port:/', explode("\n", $content)))->values();
|
||||
if ($port->count() > 0) {
|
||||
$port = str($port[0])->after('# port:')->trim()->value();
|
||||
} else {
|
||||
$port = null;
|
||||
}
|
||||
$json = Yaml::parse($content);
|
||||
$yaml = base64_encode(Yaml::dump($json, 10, 2));
|
||||
$compose = base64_encode(Yaml::dump($json, 10, 2));
|
||||
|
||||
$tags = str($data->get('tags'))->lower()->explode(',')->map(fn ($tag) => trim($tag))->filter();
|
||||
$tags = $tags->isEmpty() ? null : $tags->all();
|
||||
|
||||
$payload = [
|
||||
'name' => $serviceName,
|
||||
'name' => pathinfo($file, PATHINFO_FILENAME),
|
||||
'documentation' => $documentation,
|
||||
'slogan' => $slogan,
|
||||
'compose' => $yaml,
|
||||
'slogan' => $data->get('slogan', str($file)->headline()),
|
||||
'compose' => $compose,
|
||||
'tags' => $tags,
|
||||
'logo' => $logo,
|
||||
'minversion' => $minversion,
|
||||
'logo' => $data->get('logo', 'svgs/default.webp'),
|
||||
'minversion' => $data->get('minversion', '0.0.0'),
|
||||
];
|
||||
if ($port) {
|
||||
|
||||
if ($port = $data->get('port')) {
|
||||
$payload['port'] = $port;
|
||||
}
|
||||
if ($env_file) {
|
||||
$env_file_content = file_get_contents(base_path("templates/compose/$env_file"));
|
||||
$env_file_base64 = base64_encode($env_file_content);
|
||||
$payload['envs'] = $env_file_base64;
|
||||
|
||||
if ($envFile = $data->get('env_file')) {
|
||||
$envFileContent = file_get_contents(base_path("templates/compose/$envFile"));
|
||||
$payload['envs'] = base64_encode($envFileContent);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
|
||||
@@ -57,7 +57,7 @@ class SyncBunny extends Command
|
||||
|
||||
PendingRequest::macro('storage', function ($fileName) use ($that) {
|
||||
$headers = [
|
||||
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
|
||||
'AccessKey' => config('constants.bunny.storage_api_key'),
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
];
|
||||
@@ -69,7 +69,7 @@ class SyncBunny extends Command
|
||||
});
|
||||
PendingRequest::macro('purge', function ($url) use ($that) {
|
||||
$headers = [
|
||||
'AccessKey' => env('BUNNY_API_KEY'),
|
||||
'AccessKey' => config('constants.bunny.api_key'),
|
||||
'Accept' => 'application/json',
|
||||
];
|
||||
$that->info('Purging: '.$url);
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Waitlist;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class WaitlistInvite extends Command
|
||||
{
|
||||
public Waitlist|User|null $next_patient = null;
|
||||
|
||||
public ?string $password = null;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'waitlist:invite {--people=1} {--only-email} {email?}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Send invitation to the next user (or by email) in the waitlist';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$people = $this->option('people');
|
||||
for ($i = 0; $i < $people; $i++) {
|
||||
$this->main();
|
||||
}
|
||||
}
|
||||
|
||||
private function main()
|
||||
{
|
||||
if ($this->argument('email')) {
|
||||
if ($this->option('only-email')) {
|
||||
$this->next_patient = User::whereEmail($this->argument('email'))->first();
|
||||
$this->password = Str::password();
|
||||
$this->next_patient->update([
|
||||
'password' => Hash::make($this->password),
|
||||
'force_password_reset' => true,
|
||||
]);
|
||||
} else {
|
||||
$this->next_patient = Waitlist::where('email', $this->argument('email'))->first();
|
||||
}
|
||||
if (! $this->next_patient) {
|
||||
$this->error("{$this->argument('email')} not found in the waitlist.");
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
|
||||
}
|
||||
if ($this->next_patient) {
|
||||
if ($this->option('only-email')) {
|
||||
$this->send_email();
|
||||
|
||||
return;
|
||||
}
|
||||
$this->register_user();
|
||||
$this->remove_from_waitlist();
|
||||
$this->send_email();
|
||||
} else {
|
||||
$this->info('No verified user found in the waitlist. 👀');
|
||||
}
|
||||
}
|
||||
|
||||
private function register_user()
|
||||
{
|
||||
$already_registered = User::whereEmail($this->next_patient->email)->first();
|
||||
if (! $already_registered) {
|
||||
$this->password = Str::password();
|
||||
User::create([
|
||||
'name' => str($this->next_patient->email)->before('@'),
|
||||
'email' => $this->next_patient->email,
|
||||
'password' => Hash::make($this->password),
|
||||
'force_password_reset' => true,
|
||||
]);
|
||||
$this->info("User registered ({$this->next_patient->email}) successfully. 🎉");
|
||||
} else {
|
||||
throw new \Exception('User already registered');
|
||||
}
|
||||
}
|
||||
|
||||
private function remove_from_waitlist()
|
||||
{
|
||||
$this->next_patient->delete();
|
||||
$this->info('User removed from waitlist successfully.');
|
||||
}
|
||||
|
||||
private function send_email()
|
||||
{
|
||||
$token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
|
||||
$loginLink = route('auth.link', ['token' => $token]);
|
||||
$mail = new MailMessage;
|
||||
$mail->view('emails.waitlist-invitation', [
|
||||
'loginLink' => $loginLink,
|
||||
]);
|
||||
$mail->subject('Congratulations! You are invited to join Coolify Cloud.');
|
||||
send_user_an_email($mail, $this->next_patient->email);
|
||||
$this->info('Email sent successfully. 📧');
|
||||
}
|
||||
}
|
||||
@@ -2,142 +2,181 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
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\PullHelperImageJob;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
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;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
private $all_servers;
|
||||
private $allServers;
|
||||
|
||||
private Schedule $scheduleInstance;
|
||||
|
||||
private InstanceSettings $settings;
|
||||
|
||||
private string $updateCheckFrequency;
|
||||
|
||||
private string $instanceTimezone;
|
||||
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
$this->all_servers = Server::all();
|
||||
$settings = instanceSettings();
|
||||
$this->scheduleInstance = $schedule;
|
||||
$this->allServers = Server::where('ip', '!=', '1.2.3.4');
|
||||
|
||||
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||
$this->settings = instanceSettings();
|
||||
$this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *';
|
||||
|
||||
$this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone');
|
||||
|
||||
if (validate_timezone($this->instanceTimezone) === false) {
|
||||
$this->instanceTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
// $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||
|
||||
if (isDev()) {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyMinute();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
|
||||
$this->scheduleInstance->command('horizon:snapshot')->everyMinute();
|
||||
$this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
|
||||
$this->scheduleInstance->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer();
|
||||
|
||||
// Server Jobs
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->check_resources($schedule);
|
||||
$this->check_scheduled_tasks($schedule);
|
||||
$schedule->command('uploads:clear')->everyTwoMinutes();
|
||||
$this->checkResources();
|
||||
|
||||
$schedule->command('telescope:prune')->daily();
|
||||
$this->checkScheduledBackups();
|
||||
$this->checkScheduledTasks();
|
||||
|
||||
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
|
||||
|
||||
$schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
|
||||
} else {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
|
||||
$schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
|
||||
$this->schedule_updates($schedule);
|
||||
$this->scheduleInstance->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer();
|
||||
|
||||
$this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
|
||||
$this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
|
||||
$this->scheduleUpdates();
|
||||
|
||||
// Server Jobs
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->check_resources($schedule);
|
||||
$this->pull_images($schedule);
|
||||
$this->check_scheduled_tasks($schedule);
|
||||
$this->checkResources();
|
||||
|
||||
$schedule->command('cleanup:database --yes')->daily();
|
||||
$schedule->command('uploads:clear')->everyTwoMinutes();
|
||||
$this->pullImages();
|
||||
|
||||
$this->checkScheduledBackups();
|
||||
$this->checkScheduledTasks();
|
||||
|
||||
$this->scheduleInstance->command('cleanup:database --yes')->daily();
|
||||
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
|
||||
}
|
||||
}
|
||||
|
||||
private function pull_images($schedule)
|
||||
private function pullImages(): void
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
|
||||
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
|
||||
foreach ($servers as $server) {
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$schedule->job(function () use ($server) {
|
||||
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false);
|
||||
$sentinel_found = json_decode($sentinel_found, true);
|
||||
$status = data_get($sentinel_found, '0.State.Status', 'exited');
|
||||
if ($status !== 'running') {
|
||||
PullSentinelImageJob::dispatch($server);
|
||||
}
|
||||
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
|
||||
$this->scheduleInstance->job(function () use ($server) {
|
||||
CheckAndStartSentinelJob::dispatch($server);
|
||||
})->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
$schedule->job(new PullHelperImageJob)
|
||||
->cron($settings->update_check_frequency)
|
||||
->timezone($settings->instance_timezone)
|
||||
$this->scheduleInstance->job(new CheckHelperImageJob)
|
||||
->cron($this->updateCheckFrequency)
|
||||
->timezone($this->instanceTimezone)
|
||||
->onOneServer();
|
||||
}
|
||||
|
||||
private function schedule_updates($schedule)
|
||||
private function scheduleUpdates(): void
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
|
||||
$updateCheckFrequency = $settings->update_check_frequency;
|
||||
$schedule->job(new CheckForUpdatesJob)
|
||||
->cron($updateCheckFrequency)
|
||||
->timezone($settings->instance_timezone)
|
||||
$this->scheduleInstance->job(new CheckForUpdatesJob)
|
||||
->cron($this->updateCheckFrequency)
|
||||
->timezone($this->instanceTimezone)
|
||||
->onOneServer();
|
||||
|
||||
if ($settings->is_auto_update_enabled) {
|
||||
$autoUpdateFrequency = $settings->auto_update_frequency;
|
||||
$schedule->job(new UpdateCoolifyJob)
|
||||
if ($this->settings->is_auto_update_enabled) {
|
||||
$autoUpdateFrequency = $this->settings->auto_update_frequency;
|
||||
$this->scheduleInstance->job(new UpdateCoolifyJob)
|
||||
->cron($autoUpdateFrequency)
|
||||
->timezone($settings->instance_timezone)
|
||||
->timezone($this->instanceTimezone)
|
||||
->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
private function check_resources($schedule)
|
||||
private function checkResources(): void
|
||||
{
|
||||
if (isCloud()) {
|
||||
$servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
|
||||
$servers = $this->allServers->whereHas('team.subscription')->get();
|
||||
$own = Team::find(0)->servers;
|
||||
$servers = $servers->merge($own);
|
||||
} else {
|
||||
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
|
||||
$servers = $this->allServers->get();
|
||||
}
|
||||
|
||||
foreach ($servers as $server) {
|
||||
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
|
||||
// $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer();
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
$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
|
||||
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();
|
||||
}
|
||||
// $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) {
|
||||
$schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
|
||||
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
|
||||
} else {
|
||||
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
|
||||
$this->scheduleInstance->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function check_scheduled_backups($schedule)
|
||||
private function checkScheduledBackups(): void
|
||||
{
|
||||
$scheduled_backups = ScheduledDatabaseBackup::all();
|
||||
$scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get();
|
||||
if ($scheduled_backups->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
foreach ($scheduled_backups as $scheduled_backup) {
|
||||
if (! $scheduled_backup->enabled) {
|
||||
continue;
|
||||
}
|
||||
if (is_null(data_get($scheduled_backup, 'database'))) {
|
||||
ray('database not found');
|
||||
$scheduled_backup->delete();
|
||||
|
||||
continue;
|
||||
@@ -145,35 +184,30 @@ class Kernel extends ConsoleKernel
|
||||
|
||||
$server = $scheduled_backup->server();
|
||||
|
||||
if (! $server) {
|
||||
if (is_null($server)) {
|
||||
continue;
|
||||
}
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
}
|
||||
$schedule->job(new DatabaseBackupJob(
|
||||
$this->scheduleInstance->job(new DatabaseBackupJob(
|
||||
backup: $scheduled_backup
|
||||
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
private function check_scheduled_tasks($schedule)
|
||||
private function checkScheduledTasks(): void
|
||||
{
|
||||
$scheduled_tasks = ScheduledTask::all();
|
||||
$scheduled_tasks = ScheduledTask::where('enabled', true)->get();
|
||||
if ($scheduled_tasks->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
foreach ($scheduled_tasks as $scheduled_task) {
|
||||
if ($scheduled_task->enabled === false) {
|
||||
continue;
|
||||
}
|
||||
$service = $scheduled_task->service;
|
||||
$application = $scheduled_task->application;
|
||||
|
||||
if (! $application && ! $service) {
|
||||
ray('application/service attached to scheduled task does not exist');
|
||||
$scheduled_task->delete();
|
||||
|
||||
continue;
|
||||
@@ -193,14 +227,13 @@ class Kernel extends ConsoleKernel
|
||||
if (! $server) {
|
||||
continue;
|
||||
}
|
||||
$serverTimezone = $server->settings->server_timezone ?: config('app.timezone');
|
||||
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
|
||||
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
|
||||
}
|
||||
$schedule->job(new ScheduledTaskJob(
|
||||
$this->scheduleInstance->job(new ScheduledTaskJob(
|
||||
task: $scheduled_task
|
||||
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
app/Enums/Role.php
Normal file
37
app/Enums/Role.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum Role: string
|
||||
{
|
||||
case MEMBER = 'member';
|
||||
case ADMIN = 'admin';
|
||||
case OWNER = 'owner';
|
||||
|
||||
public function rank(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::MEMBER => 1,
|
||||
self::ADMIN => 2,
|
||||
self::OWNER => 3,
|
||||
};
|
||||
}
|
||||
|
||||
public function lt(Role|string $role): bool
|
||||
{
|
||||
if (is_string($role)) {
|
||||
$role = Role::from($role);
|
||||
}
|
||||
|
||||
return $this->rank() < $role->rank();
|
||||
}
|
||||
|
||||
public function gt(Role|string $role): bool
|
||||
{
|
||||
if (is_string($role)) {
|
||||
$role = Role::from($role);
|
||||
}
|
||||
|
||||
return $this->rank() > $role->rank();
|
||||
}
|
||||
}
|
||||
35
app/Events/DatabaseProxyStopped.php
Normal file
35
app/Events/DatabaseProxyStopped.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class DatabaseProxyStopped implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = Auth::user()?->currentTeam()?->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,27 +7,29 @@ use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class DatabaseStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public ?string $userId = null;
|
||||
public $userId = null;
|
||||
|
||||
public function __construct($userId = null)
|
||||
{
|
||||
if (is_null($userId)) {
|
||||
$userId = auth()->user()->id ?? null;
|
||||
$userId = Auth::id() ?? null;
|
||||
}
|
||||
if (is_null($userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): ?array
|
||||
{
|
||||
if ($this->userId) {
|
||||
if (! is_null($this->userId)) {
|
||||
return [
|
||||
new PrivateChannel("user.{$this->userId}"),
|
||||
];
|
||||
|
||||
@@ -16,7 +16,6 @@ class FileStorageChanged implements ShouldBroadcast
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
ray($teamId);
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
}
|
||||
|
||||
34
app/Events/ScheduledTaskDone.php
Normal file
34
app/Events/ScheduledTaskDone.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ScheduledTaskDone implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ServiceStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
@@ -17,7 +18,7 @@ class ServiceStatusChanged implements ShouldBroadcast
|
||||
public function __construct($userId = null)
|
||||
{
|
||||
if (is_null($userId)) {
|
||||
$userId = auth()->user()->id ?? null;
|
||||
$userId = Auth::id() ?? null;
|
||||
}
|
||||
if (is_null($userId)) {
|
||||
return false;
|
||||
|
||||
@@ -84,7 +84,6 @@ class Handler extends ExceptionHandler
|
||||
if (str($e->getMessage())->contains('No space left on device')) {
|
||||
return;
|
||||
}
|
||||
ray('reporting to sentry');
|
||||
Integration::captureUnhandledException($e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,17 +21,14 @@ class SshMultiplexingHelper
|
||||
];
|
||||
}
|
||||
|
||||
public static function ensureMultiplexedConnection(Server $server)
|
||||
public static function ensureMultiplexedConnection(Server $server): bool
|
||||
{
|
||||
if (! self::isMultiplexingEnabled()) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
|
||||
self::validateSshKey($sshKeyLocation);
|
||||
|
||||
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
@@ -41,16 +38,17 @@ class SshMultiplexingHelper
|
||||
$process = Process::run($checkCommand);
|
||||
|
||||
if ($process->exitCode() !== 0) {
|
||||
self::establishNewMultiplexedConnection($server);
|
||||
return self::establishNewMultiplexedConnection($server);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function establishNewMultiplexedConnection(Server $server)
|
||||
public static function establishNewMultiplexedConnection(Server $server): bool
|
||||
{
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$connectionTimeout = config('constants.ssh.connection_timeout');
|
||||
$serverInterval = config('constants.ssh.server_interval');
|
||||
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||
@@ -60,15 +58,14 @@ class SshMultiplexingHelper
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||
}
|
||||
|
||||
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
|
||||
$establishCommand .= "{$server->user}@{$server->ip}";
|
||||
|
||||
$establishProcess = Process::run($establishCommand);
|
||||
|
||||
if ($establishProcess->exitCode() !== 0) {
|
||||
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function removeMuxFile(Server $server)
|
||||
@@ -97,9 +94,8 @@ class SshMultiplexingHelper
|
||||
if ($server->isIpv6()) {
|
||||
$scp_command .= '-6 ';
|
||||
}
|
||||
if (self::isMultiplexingEnabled()) {
|
||||
if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
|
||||
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||
self::ensureMultiplexedConnection($server);
|
||||
}
|
||||
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
@@ -120,6 +116,9 @@ class SshMultiplexingHelper
|
||||
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
|
||||
self::validateSshKey($server->privateKey);
|
||||
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$timeout = config('constants.ssh.command_timeout');
|
||||
@@ -127,9 +126,8 @@ class SshMultiplexingHelper
|
||||
|
||||
$ssh_command = "timeout $timeout ssh ";
|
||||
|
||||
if (self::isMultiplexingEnabled()) {
|
||||
if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
|
||||
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||
self::ensureMultiplexedConnection($server);
|
||||
}
|
||||
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
@@ -151,16 +149,17 @@ class SshMultiplexingHelper
|
||||
|
||||
private static function isMultiplexingEnabled(): bool
|
||||
{
|
||||
return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
|
||||
return config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop');
|
||||
}
|
||||
|
||||
private static function validateSshKey(string $sshKeyLocation): void
|
||||
private static function validateSshKey(PrivateKey $privateKey): void
|
||||
{
|
||||
$checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
|
||||
$keyLocation = $privateKey->getKeyLocation();
|
||||
$checkKeyCommand = "ls $keyLocation 2>/dev/null";
|
||||
$keyCheckProcess = Process::run($checkKeyCommand);
|
||||
|
||||
if ($keyCheckProcess->exitCode() !== 0) {
|
||||
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
|
||||
$privateKey->storeInFileSystem();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,26 +25,24 @@ class ApplicationsController extends Controller
|
||||
{
|
||||
private function removeSensitiveData($application)
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
$application->makeHidden([
|
||||
'id',
|
||||
]);
|
||||
if ($token->can('view:sensitive')) {
|
||||
return serializeApiResponse($application);
|
||||
if (request()->attributes->get('can_read_sensitive', false) === false) {
|
||||
$application->makeHidden([
|
||||
'custom_labels',
|
||||
'dockerfile',
|
||||
'docker_compose',
|
||||
'docker_compose_raw',
|
||||
'manual_webhook_secret_bitbucket',
|
||||
'manual_webhook_secret_gitea',
|
||||
'manual_webhook_secret_github',
|
||||
'manual_webhook_secret_gitlab',
|
||||
'private_key_id',
|
||||
'value',
|
||||
'real_value',
|
||||
]);
|
||||
}
|
||||
$application->makeHidden([
|
||||
'custom_labels',
|
||||
'dockerfile',
|
||||
'docker_compose',
|
||||
'docker_compose_raw',
|
||||
'manual_webhook_secret_bitbucket',
|
||||
'manual_webhook_secret_gitea',
|
||||
'manual_webhook_secret_github',
|
||||
'manual_webhook_secret_gitlab',
|
||||
'private_key_id',
|
||||
'value',
|
||||
'real_value',
|
||||
]);
|
||||
|
||||
return serializeApiResponse($application);
|
||||
}
|
||||
@@ -70,7 +68,8 @@ class ApplicationsController extends Controller
|
||||
items: new OA\Items(ref: '#/components/schemas/Application')
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -180,8 +179,10 @@ class ApplicationsController extends Controller
|
||||
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
],
|
||||
)),
|
||||
]),
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
@@ -284,8 +285,10 @@ class ApplicationsController extends Controller
|
||||
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
],
|
||||
)),
|
||||
]),
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
@@ -388,8 +391,10 @@ class ApplicationsController extends Controller
|
||||
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
],
|
||||
)),
|
||||
]),
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
@@ -476,8 +481,10 @@ class ApplicationsController extends Controller
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
],
|
||||
)),
|
||||
]),
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
@@ -561,8 +568,10 @@ class ApplicationsController extends Controller
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
],
|
||||
)),
|
||||
]),
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
@@ -612,8 +621,10 @@ class ApplicationsController extends Controller
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
],
|
||||
)),
|
||||
]),
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
@@ -636,7 +647,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'];
|
||||
$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'];
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
@@ -676,6 +687,27 @@ class ApplicationsController extends Controller
|
||||
$githubAppUuid = $request->github_app_uuid;
|
||||
$useBuildServer = $request->use_build_server;
|
||||
$isStatic = $request->is_static;
|
||||
$customNginxConfiguration = $request->custom_nginx_configuration;
|
||||
|
||||
if (! is_null($customNginxConfiguration)) {
|
||||
if (! isBase64Encoded($customNginxConfiguration)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$customNginxConfiguration = base64_decode($customNginxConfiguration);
|
||||
if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
|
||||
if (! $project) {
|
||||
@@ -1213,7 +1245,6 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Invalid type.'], 400);
|
||||
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
@@ -1248,7 +1279,8 @@ class ApplicationsController extends Controller
|
||||
ref: '#/components/schemas/Application'
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -1320,7 +1352,8 @@ class ApplicationsController extends Controller
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -1446,8 +1479,10 @@ class ApplicationsController extends Controller
|
||||
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
],
|
||||
)),
|
||||
]),
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
@@ -1462,7 +1497,8 @@ class ApplicationsController extends Controller
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -1501,7 +1537,7 @@ class ApplicationsController extends Controller
|
||||
], 404);
|
||||
}
|
||||
$server = $application->destination->server;
|
||||
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', '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', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
|
||||
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', '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', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration'];
|
||||
|
||||
$validationRules = [
|
||||
'name' => 'string|max:255',
|
||||
@@ -1513,6 +1549,7 @@ class ApplicationsController extends Controller
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
'docker_compose_custom_start_command' => 'string|nullable',
|
||||
'docker_compose_custom_build_command' => 'string|nullable',
|
||||
'custom_nginx_configuration' => 'string|nullable',
|
||||
];
|
||||
$validationRules = array_merge($validationRules, sharedDataApplications());
|
||||
$validator = customApiValidator($request->all(), $validationRules);
|
||||
@@ -1531,6 +1568,25 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($request->has('custom_nginx_configuration')) {
|
||||
if (! isBase64Encoded($request->custom_nginx_configuration)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$customNginxConfiguration = base64_decode($request->custom_nginx_configuration);
|
||||
if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
@@ -1551,16 +1607,33 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
$domains = $request->domains;
|
||||
if ($request->has('domains') && $server->isProxyShouldRun()) {
|
||||
$errors = [];
|
||||
$uuid = $request->uuid;
|
||||
$fqdn = $request->domains;
|
||||
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
|
||||
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
|
||||
$application->fqdn = $fqdn;
|
||||
if (! $application->settings->is_container_label_readonly_enabled) {
|
||||
$customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
$application->custom_labels = base64_encode($customLabels);
|
||||
$errors = [];
|
||||
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
|
||||
$domain = trim($domain);
|
||||
if (filter_var($domain, FILTER_VALIDATE_URL) === false || ! preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}/', $domain)) {
|
||||
$errors[] = 'Invalid domain: '.$domain;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
});
|
||||
if (count($errors) > 0) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'domains' => 'One of the domain is already used.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$request->offsetUnset('domains');
|
||||
}
|
||||
|
||||
$dockerComposeDomainsJson = collect();
|
||||
@@ -1579,11 +1652,16 @@ class ApplicationsController extends Controller
|
||||
$request->offsetUnset('docker_compose_domains');
|
||||
}
|
||||
$instantDeploy = $request->instant_deploy;
|
||||
$isStatic = $request->is_static;
|
||||
$useBuildServer = $request->use_build_server;
|
||||
|
||||
$use_build_server = $request->use_build_server;
|
||||
if (isset($useBuildServer)) {
|
||||
$application->settings->is_build_server_enabled = $useBuildServer;
|
||||
$application->settings->save();
|
||||
}
|
||||
|
||||
if (isset($use_build_server)) {
|
||||
$application->settings->is_build_server_enabled = $use_build_server;
|
||||
if (isset($isStatic)) {
|
||||
$application->settings->is_static = $isStatic;
|
||||
$application->settings->save();
|
||||
}
|
||||
|
||||
@@ -1645,7 +1723,8 @@ class ApplicationsController extends Controller
|
||||
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -1687,9 +1766,8 @@ class ApplicationsController extends Controller
|
||||
'standalone_postgresql_id',
|
||||
'standalone_redis_id',
|
||||
]);
|
||||
$env = $this->removeSensitiveData($env);
|
||||
|
||||
return $env;
|
||||
return $this->removeSensitiveData($env);
|
||||
});
|
||||
|
||||
return response()->json($envs);
|
||||
@@ -1752,7 +1830,8 @@ class ApplicationsController extends Controller
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -1864,18 +1943,15 @@ class ApplicationsController extends Controller
|
||||
|
||||
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
|
||||
} else {
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Environment variable not found.',
|
||||
], 404);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Something is not okay. Are you okay?',
|
||||
], 500);
|
||||
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
@@ -1943,7 +2019,8 @@ class ApplicationsController extends Controller
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -2124,7 +2201,8 @@ class ApplicationsController extends Controller
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -2220,14 +2298,12 @@ class ApplicationsController extends Controller
|
||||
return response()->json([
|
||||
'uuid' => $env->uuid,
|
||||
])->setStatusCode(201);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Something went wrong.',
|
||||
], 500);
|
||||
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
@@ -2275,7 +2351,8 @@ class ApplicationsController extends Controller
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -2367,9 +2444,11 @@ class ApplicationsController extends Controller
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Deployment request queued.', 'description' => 'Message.'],
|
||||
'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'],
|
||||
])
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -2455,7 +2534,8 @@ class ApplicationsController extends Controller
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -2529,7 +2609,8 @@ class ApplicationsController extends Controller
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
@@ -2575,7 +2656,6 @@ class ApplicationsController extends Controller
|
||||
'deployment_uuid' => $deployment_uuid->toString(),
|
||||
],
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
@@ -2741,7 +2821,6 @@ class ApplicationsController extends Controller
|
||||
'custom_labels' => 'The custom_labels should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
|
||||
}
|
||||
}
|
||||
if ($request->has('domains') && $server->isProxyShouldRun()) {
|
||||
|
||||
@@ -19,26 +19,23 @@ class DatabasesController extends Controller
|
||||
{
|
||||
private function removeSensitiveData($database)
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
$database->makeHidden([
|
||||
'id',
|
||||
'laravel_through_key',
|
||||
]);
|
||||
if ($token->can('view:sensitive')) {
|
||||
return serializeApiResponse($database);
|
||||
if (request()->attributes->get('can_read_sensitive', false) === false) {
|
||||
$database->makeHidden([
|
||||
'internal_db_url',
|
||||
'external_db_url',
|
||||
'postgres_password',
|
||||
'dragonfly_password',
|
||||
'redis_password',
|
||||
'mongo_initdb_root_password',
|
||||
'keydb_password',
|
||||
'clickhouse_admin_password',
|
||||
]);
|
||||
}
|
||||
|
||||
$database->makeHidden([
|
||||
'internal_db_url',
|
||||
'external_db_url',
|
||||
'postgres_password',
|
||||
'dragonfly_password',
|
||||
'redis_password',
|
||||
'mongo_initdb_root_password',
|
||||
'keydb_password',
|
||||
'clickhouse_admin_password',
|
||||
]);
|
||||
|
||||
return serializeApiResponse($database);
|
||||
}
|
||||
|
||||
@@ -211,8 +208,9 @@ class DatabasesController extends Controller
|
||||
'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'],
|
||||
'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'],
|
||||
'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'],
|
||||
'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
|
||||
'mongo_initdb_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
|
||||
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
|
||||
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
|
||||
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
|
||||
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
|
||||
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
|
||||
@@ -241,7 +239,7 @@ class DatabasesController extends Controller
|
||||
)]
|
||||
public function update_by_uuid(Request $request)
|
||||
{
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', '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_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', '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)) {
|
||||
return invalidTokenResponse();
|
||||
@@ -413,12 +411,12 @@ class DatabasesController extends Controller
|
||||
}
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', '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_init_database'];
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', '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',
|
||||
'mongo_initdb_root_password' => 'string',
|
||||
'mongo_initdb_init_database' => 'string',
|
||||
'mongo_initdb_database' => 'string',
|
||||
]);
|
||||
if ($request->has('mongo_conf')) {
|
||||
if (! isBase64Encoded($request->mongo_conf)) {
|
||||
@@ -443,9 +441,10 @@ class DatabasesController extends Controller
|
||||
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', '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',
|
||||
'mysql_user' => 'string',
|
||||
'mysql_database' => 'string',
|
||||
'mysql_conf' => 'string',
|
||||
@@ -471,7 +470,6 @@ class DatabasesController extends Controller
|
||||
$request->offsetSet('mysql_conf', $mysqlConf);
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
@@ -506,7 +504,6 @@ class DatabasesController extends Controller
|
||||
return response()->json([
|
||||
'message' => 'Database updated.',
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
@@ -911,6 +908,7 @@ class DatabasesController extends Controller
|
||||
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
|
||||
'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'],
|
||||
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
|
||||
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
|
||||
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
|
||||
@@ -1015,7 +1013,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_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
|
||||
$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'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -1165,7 +1163,6 @@ 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'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
@@ -1223,9 +1220,10 @@ 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_user', 'mysql_database', 'mysql_conf'];
|
||||
$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'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'mysql_root_password' => 'string',
|
||||
'mysql_password' => 'string',
|
||||
'mysql_user' => 'string',
|
||||
'mysql_database' => 'string',
|
||||
'mysql_conf' => 'string',
|
||||
@@ -1459,12 +1457,12 @@ 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_init_database'];
|
||||
$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'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'mongo_conf' => 'string',
|
||||
'mongo_initdb_root_username' => 'string',
|
||||
'mongo_initdb_root_password' => 'string',
|
||||
'mongo_initdb_init_database' => 'string',
|
||||
'mongo_initdb_database' => 'string',
|
||||
]);
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
@@ -1560,7 +1558,8 @@ class DatabasesController extends Controller
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -1635,9 +1634,11 @@ class DatabasesController extends Controller
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Database starting request queued.'],
|
||||
])
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -1711,9 +1712,11 @@ class DatabasesController extends Controller
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'],
|
||||
])
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -1787,9 +1790,11 @@ class DatabasesController extends Controller
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'],
|
||||
])
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
@@ -1826,6 +1831,5 @@ class DatabasesController extends Controller
|
||||
],
|
||||
200
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,12 @@ class DeployController extends Controller
|
||||
{
|
||||
private function removeSensitiveData($deployment)
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
if ($token->can('view:sensitive')) {
|
||||
return serializeApiResponse($deployment);
|
||||
if (request()->attributes->get('can_read_sensitive', false) === false) {
|
||||
$deployment->makeHidden([
|
||||
'logs',
|
||||
]);
|
||||
}
|
||||
|
||||
$deployment->makeHidden([
|
||||
'logs',
|
||||
]);
|
||||
|
||||
return serializeApiResponse($deployment);
|
||||
}
|
||||
|
||||
@@ -292,7 +289,7 @@ class DeployController extends Controller
|
||||
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
|
||||
}
|
||||
switch ($resource?->getMorphClass()) {
|
||||
case 'App\Models\Application':
|
||||
case \App\Models\Application::class:
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
application: $resource,
|
||||
@@ -301,7 +298,7 @@ class DeployController extends Controller
|
||||
);
|
||||
$message = "Application {$resource->name} deployment queued.";
|
||||
break;
|
||||
case 'App\Models\Service':
|
||||
case \App\Models\Service::class:
|
||||
StartService::run($resource);
|
||||
$message = "Service {$resource->name} started. It could take a while, be patient.";
|
||||
break;
|
||||
|
||||
@@ -37,7 +37,7 @@ class OtherController extends Controller
|
||||
)]
|
||||
public function version(Request $request)
|
||||
{
|
||||
return response(config('version'));
|
||||
return response(config('constants.coolify.version'));
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
@@ -147,7 +147,7 @@ class OtherController extends Controller
|
||||
public function feedback(Request $request)
|
||||
{
|
||||
$content = $request->input('content');
|
||||
$webhook_url = config('coolify.feedback_discord_webhook');
|
||||
$webhook_url = config('constants.webhooks.feedback_discord_webhook');
|
||||
if ($webhook_url) {
|
||||
Http::post($webhook_url, [
|
||||
'content' => $content,
|
||||
@@ -160,7 +160,7 @@ class OtherController extends Controller
|
||||
#[OA\Get(
|
||||
summary: 'Healthcheck',
|
||||
description: 'Healthcheck endpoint.',
|
||||
path: '/healthcheck',
|
||||
path: '/health',
|
||||
operationId: 'healthcheck',
|
||||
responses: [
|
||||
new OA\Response(
|
||||
|
||||
@@ -116,7 +116,7 @@ class ProjectController extends Controller
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Project details',
|
||||
description: 'Environment details',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
@@ -356,7 +356,6 @@ class ProjectController extends Controller
|
||||
'name' => $project->name,
|
||||
'description' => $project->description,
|
||||
])->setStatusCode(201);
|
||||
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
@@ -423,7 +422,7 @@ class ProjectController extends Controller
|
||||
if (! $project) {
|
||||
return response()->json(['message' => 'Project not found.'], 404);
|
||||
}
|
||||
if ($project->resource_count() > 0) {
|
||||
if (! $project->isEmpty()) {
|
||||
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class ResourcesController extends Controller
|
||||
$resources = $resources->flatten();
|
||||
$resources = $resources->map(function ($resource) {
|
||||
$payload = $resource->toArray();
|
||||
if ($resource->getMorphClass() === 'App\Models\Service') {
|
||||
if ($resource->getMorphClass() === \App\Models\Service::class) {
|
||||
$payload['status'] = $resource->status();
|
||||
} else {
|
||||
$payload['status'] = $resource->status;
|
||||
|
||||
@@ -11,13 +11,11 @@ class SecurityController extends Controller
|
||||
{
|
||||
private function removeSensitiveData($team)
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
if ($token->can('view:sensitive')) {
|
||||
return serializeApiResponse($team);
|
||||
if (request()->attributes->get('can_read_sensitive', false) === false) {
|
||||
$team->makeHidden([
|
||||
'private_key',
|
||||
]);
|
||||
}
|
||||
$team->makeHidden([
|
||||
'private_key',
|
||||
]);
|
||||
|
||||
return serializeApiResponse($team);
|
||||
}
|
||||
@@ -81,15 +79,8 @@ class SecurityController extends Controller
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all private keys.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/PrivateKey')
|
||||
)
|
||||
),
|
||||
]),
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/PrivateKey')
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use App\Actions\Server\ValidateServer;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
@@ -18,25 +19,22 @@ class ServersController extends Controller
|
||||
{
|
||||
private function removeSensitiveDataFromSettings($settings)
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
if ($token->can('view:sensitive')) {
|
||||
return serializeApiResponse($settings);
|
||||
if (request()->attributes->get('can_read_sensitive', false) === false) {
|
||||
$settings = $settings->makeHidden([
|
||||
'sentinel_token',
|
||||
]);
|
||||
}
|
||||
$settings = $settings->makeHidden([
|
||||
'metrics_token',
|
||||
]);
|
||||
|
||||
return serializeApiResponse($settings);
|
||||
}
|
||||
|
||||
private function removeSensitiveData($server)
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
$server->makeHidden([
|
||||
'id',
|
||||
]);
|
||||
if ($token->can('view:sensitive')) {
|
||||
return serializeApiResponse($server);
|
||||
if (request()->attributes->get('can_read_sensitive', false) === false) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return serializeApiResponse($server);
|
||||
@@ -248,7 +246,6 @@ class ServersController extends Controller
|
||||
return $payload;
|
||||
});
|
||||
$server = $this->removeSensitiveData($server);
|
||||
ray($server);
|
||||
|
||||
return response()->json(serializeApiResponse(data_get($server, 'resources')));
|
||||
}
|
||||
@@ -426,6 +423,7 @@ class ServersController extends Controller
|
||||
'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'],
|
||||
'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'],
|
||||
'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'],
|
||||
'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'example' => 'traefik', 'description' => 'The proxy type.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -461,7 +459,7 @@ class ServersController extends Controller
|
||||
)]
|
||||
public function create_server(Request $request)
|
||||
{
|
||||
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
|
||||
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -481,6 +479,7 @@ class ServersController extends Controller
|
||||
'user' => 'string|nullable',
|
||||
'is_build_server' => 'boolean|nullable',
|
||||
'instant_validate' => 'boolean|nullable',
|
||||
'proxy_type' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
@@ -512,6 +511,14 @@ class ServersController extends Controller
|
||||
if (is_null($request->instant_validate)) {
|
||||
$request->offsetSet('instant_validate', false);
|
||||
}
|
||||
if ($request->proxy_type) {
|
||||
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
|
||||
return str($proxyType->value)->lower();
|
||||
});
|
||||
if (! $validProxyTypes->contains(str($request->proxy_type)->lower())) {
|
||||
return response()->json(['message' => 'Invalid proxy type.'], 422);
|
||||
}
|
||||
}
|
||||
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
|
||||
if (! $privateKey) {
|
||||
return response()->json(['message' => 'Private key not found.'], 404);
|
||||
@@ -521,6 +528,8 @@ class ServersController extends Controller
|
||||
return response()->json(['message' => 'Server with this IP already exists.'], 400);
|
||||
}
|
||||
|
||||
$proxyType = $request->proxy_type ? str($request->proxy_type)->upper() : ProxyTypes::TRAEFIK->value;
|
||||
|
||||
$server = ModelsServer::create([
|
||||
'name' => $request->name,
|
||||
'description' => $request->description,
|
||||
@@ -530,7 +539,7 @@ class ServersController extends Controller
|
||||
'private_key_id' => $privateKey->id,
|
||||
'team_id' => $teamId,
|
||||
'proxy' => [
|
||||
'type' => ProxyTypes::TRAEFIK->value,
|
||||
'type' => $proxyType,
|
||||
'status' => ProxyStatus::EXITED->value,
|
||||
],
|
||||
]);
|
||||
@@ -555,6 +564,9 @@ class ServersController extends Controller
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Servers'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
description: 'Server updated.',
|
||||
@@ -571,6 +583,7 @@ class ServersController extends Controller
|
||||
'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'],
|
||||
'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'],
|
||||
'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'],
|
||||
'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -583,8 +596,7 @@ class ServersController extends Controller
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/Server')
|
||||
ref: '#/components/schemas/Server'
|
||||
)
|
||||
),
|
||||
]),
|
||||
@@ -604,7 +616,7 @@ class ServersController extends Controller
|
||||
)]
|
||||
public function update_server(Request $request)
|
||||
{
|
||||
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
|
||||
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -624,6 +636,7 @@ class ServersController extends Controller
|
||||
'user' => 'string|nullable',
|
||||
'is_build_server' => 'boolean|nullable',
|
||||
'instant_validate' => 'boolean|nullable',
|
||||
'proxy_type' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
@@ -644,6 +657,16 @@ class ServersController extends Controller
|
||||
if (! $server) {
|
||||
return response()->json(['message' => 'Server not found.'], 404);
|
||||
}
|
||||
if ($request->proxy_type) {
|
||||
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
|
||||
return str($proxyType->value)->lower();
|
||||
});
|
||||
if ($validProxyTypes->contains(str($request->proxy_type)->lower())) {
|
||||
$server->changeProxy($request->proxy_type, async: true);
|
||||
} else {
|
||||
return response()->json(['message' => 'Invalid proxy type.'], 422);
|
||||
}
|
||||
}
|
||||
$server->update($request->only(['name', 'description', 'ip', 'port', 'user']));
|
||||
if ($request->is_build_server) {
|
||||
$server->settings()->update([
|
||||
@@ -654,7 +677,9 @@ class ServersController extends Controller
|
||||
ValidateServer::dispatch($server);
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse($server))->setStatusCode(201);
|
||||
return response()->json([
|
||||
'uuid' => $server->uuid,
|
||||
])->setStatusCode(201);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
@@ -726,6 +751,7 @@ class ServersController extends Controller
|
||||
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
|
||||
}
|
||||
$server->delete();
|
||||
DeleteServer::dispatch($server);
|
||||
|
||||
return response()->json(['message' => 'Server deleted.']);
|
||||
}
|
||||
|
||||
@@ -18,19 +18,16 @@ class ServicesController extends Controller
|
||||
{
|
||||
private function removeSensitiveData($service)
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
$service->makeHidden([
|
||||
'id',
|
||||
]);
|
||||
if ($token->can('view:sensitive')) {
|
||||
return serializeApiResponse($service);
|
||||
if (request()->attributes->get('can_read_sensitive', false) === false) {
|
||||
$service->makeHidden([
|
||||
'docker_compose_raw',
|
||||
'docker_compose',
|
||||
]);
|
||||
}
|
||||
|
||||
$service->makeHidden([
|
||||
'docker_compose_raw',
|
||||
'docker_compose',
|
||||
]);
|
||||
|
||||
return serializeApiResponse($service);
|
||||
}
|
||||
|
||||
@@ -566,9 +563,8 @@ class ServicesController extends Controller
|
||||
'standalone_postgresql_id',
|
||||
'standalone_redis_id',
|
||||
]);
|
||||
$env = $this->removeSensitiveData($env);
|
||||
|
||||
return $env;
|
||||
return $this->removeSensitiveData($env);
|
||||
});
|
||||
|
||||
return response()->json($envs);
|
||||
@@ -1238,6 +1234,5 @@ class ServicesController extends Controller
|
||||
],
|
||||
200
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,20 +10,18 @@ class TeamController extends Controller
|
||||
{
|
||||
private function removeSensitiveData($team)
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
$team->makeHidden([
|
||||
'custom_server_limit',
|
||||
'pivot',
|
||||
]);
|
||||
if ($token->can('view:sensitive')) {
|
||||
return serializeApiResponse($team);
|
||||
if (request()->attributes->get('can_read_sensitive', false) === false) {
|
||||
$team->makeHidden([
|
||||
'smtp_username',
|
||||
'smtp_password',
|
||||
'resend_api_key',
|
||||
'telegram_token',
|
||||
]);
|
||||
}
|
||||
$team->makeHidden([
|
||||
'smtp_username',
|
||||
'smtp_password',
|
||||
'resend_api_key',
|
||||
'telegram_token',
|
||||
]);
|
||||
|
||||
return serializeApiResponse($team);
|
||||
}
|
||||
|
||||
@@ -42,15 +42,13 @@ class Controller extends BaseController
|
||||
public function email_verify(EmailVerificationRequest $request)
|
||||
{
|
||||
$request->fulfill();
|
||||
$name = request()->user()?->name;
|
||||
|
||||
// send_internal_notification("User {$name} verified their email address.");
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
|
||||
public function forgot_password(Request $request)
|
||||
{
|
||||
if (is_transactional_emails_active()) {
|
||||
if (is_transactional_emails_enabled()) {
|
||||
$arrayOfRequest = $request->only(Fortify::email());
|
||||
$request->merge([
|
||||
'email' => Str::lower($arrayOfRequest['email']),
|
||||
@@ -110,59 +108,54 @@ class Controller extends BaseController
|
||||
return redirect()->route('login')->with('error', 'Invalid credentials.');
|
||||
}
|
||||
|
||||
public function accept_invitation()
|
||||
public function acceptInvitation()
|
||||
{
|
||||
try {
|
||||
$resetPassword = request()->query('reset-password');
|
||||
$invitationUuid = request()->route('uuid');
|
||||
$invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail();
|
||||
$user = User::whereEmail($invitation->email)->firstOrFail();
|
||||
$invitationValid = $invitation->isValid();
|
||||
if ($invitationValid) {
|
||||
if ($resetPassword) {
|
||||
$user->update([
|
||||
'password' => Hash::make($invitationUuid),
|
||||
'force_password_reset' => true,
|
||||
]);
|
||||
}
|
||||
if ($user->teams()->where('team_id', $invitation->team->id)->exists()) {
|
||||
$invitation->delete();
|
||||
$resetPassword = request()->query('reset-password');
|
||||
$invitationUuid = request()->route('uuid');
|
||||
|
||||
return redirect()->route('team.index');
|
||||
}
|
||||
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
|
||||
$invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail();
|
||||
$user = User::whereEmail($invitation->email)->firstOrFail();
|
||||
|
||||
if (Auth::id() !== $user->id) {
|
||||
abort(400, 'You are not allowed to accept this invitation.');
|
||||
}
|
||||
$invitationValid = $invitation->isValid();
|
||||
|
||||
if ($invitationValid) {
|
||||
if ($resetPassword) {
|
||||
$user->update([
|
||||
'password' => Hash::make($invitationUuid),
|
||||
'force_password_reset' => true,
|
||||
]);
|
||||
}
|
||||
if ($user->teams()->where('team_id', $invitation->team->id)->exists()) {
|
||||
$invitation->delete();
|
||||
if (auth()->user()?->id !== $user->id) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
refreshSession($invitation->team);
|
||||
|
||||
return redirect()->route('team.index');
|
||||
} else {
|
||||
abort(401);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e->getMessage());
|
||||
throw $e;
|
||||
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
|
||||
$invitation->delete();
|
||||
|
||||
refreshSession($invitation->team);
|
||||
|
||||
return redirect()->route('team.index');
|
||||
} else {
|
||||
abort(400, 'Invitation expired.');
|
||||
}
|
||||
}
|
||||
|
||||
public function revoke_invitation()
|
||||
{
|
||||
try {
|
||||
$invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
|
||||
$user = User::whereEmail($invitation->email)->firstOrFail();
|
||||
if (is_null(auth()->user())) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
if (auth()->user()->id !== $user->id) {
|
||||
abort(401);
|
||||
}
|
||||
$invitation->delete();
|
||||
|
||||
return redirect()->route('team.index');
|
||||
} catch (\Throwable $e) {
|
||||
throw $e;
|
||||
$invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
|
||||
$user = User::whereEmail($invitation->email)->firstOrFail();
|
||||
if (is_null(Auth::user())) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
if (Auth::id() !== $user->id) {
|
||||
abort(401);
|
||||
}
|
||||
$invitation->delete();
|
||||
|
||||
return redirect()->route('team.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,6 @@ class OauthController extends Controller
|
||||
|
||||
return redirect('/');
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
$errorCode = $e instanceof HttpException ? 'auth.failed' : 'auth.failed.callback';
|
||||
|
||||
return redirect()->route('login')->withErrors([__($errorCode)]);
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Http\Controllers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException;
|
||||
use Pion\Laravel\ChunkUpload\Handler\HandlerFactory;
|
||||
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
|
||||
|
||||
@@ -16,7 +16,6 @@ class Bitbucket extends Controller
|
||||
{
|
||||
try {
|
||||
if (app()->isDownForMaintenance()) {
|
||||
ray('Maintenance mode is on');
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
@@ -55,7 +54,6 @@ class Bitbucket extends Controller
|
||||
'message' => 'Nothing to do. No branch found in the request.',
|
||||
]);
|
||||
}
|
||||
ray('Manual webhook bitbucket push event with branch: '.$branch);
|
||||
}
|
||||
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
|
||||
$branch = data_get($payload, 'pullrequest.destination.branch.name');
|
||||
@@ -85,7 +83,6 @@ class Bitbucket extends Controller
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid signature.',
|
||||
]);
|
||||
ray('Invalid signature');
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -96,13 +93,11 @@ class Bitbucket extends Controller
|
||||
'status' => 'failed',
|
||||
'message' => 'Server is not functional.',
|
||||
]);
|
||||
ray('Server is not functional: '.$application->destination->server->name);
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($x_bitbucket_event === 'repo:push') {
|
||||
if ($application->isDeployable()) {
|
||||
ray('Deploying '.$application->name.' with branch '.$branch);
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -126,7 +121,6 @@ class Bitbucket extends Controller
|
||||
}
|
||||
if ($x_bitbucket_event === 'pullrequest:created') {
|
||||
if ($application->isPRDeployable()) {
|
||||
ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id);
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found) {
|
||||
@@ -171,7 +165,6 @@ class Bitbucket extends Controller
|
||||
}
|
||||
}
|
||||
if ($x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
|
||||
ray('Pull request rejected');
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
@@ -191,12 +184,9 @@ class Bitbucket extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
ray($return_payloads);
|
||||
|
||||
return response($return_payloads);
|
||||
} catch (Exception $e) {
|
||||
ray($e);
|
||||
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,15 +19,12 @@ class Gitea extends Controller
|
||||
$return_payloads = collect([]);
|
||||
$x_gitea_delivery = request()->header('X-Gitea-Delivery');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
ray('Maintenance mode is on');
|
||||
$epoch = now()->valueOf();
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$gitea_delivery_found = collect($files)->filter(function ($file) use ($x_gitea_delivery) {
|
||||
return Str::contains($file, $x_gitea_delivery);
|
||||
})->first();
|
||||
if ($gitea_delivery_found) {
|
||||
ray('Webhook already found');
|
||||
|
||||
return;
|
||||
}
|
||||
$data = [
|
||||
@@ -67,8 +64,6 @@ class Gitea extends Controller
|
||||
$removed_files = data_get($payload, 'commits.*.removed');
|
||||
$modified_files = data_get($payload, 'commits.*.modified');
|
||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||
ray($changed_files);
|
||||
ray('Manual Webhook Gitea Push Event with branch: '.$branch);
|
||||
}
|
||||
if ($x_gitea_event === 'pull_request') {
|
||||
$action = data_get($payload, 'action');
|
||||
@@ -77,7 +72,6 @@ class Gitea extends Controller
|
||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||
$branch = data_get($payload, 'pull_request.head.ref');
|
||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||
ray('Webhook Gitea Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id);
|
||||
}
|
||||
if (! $branch) {
|
||||
return response('Nothing to do. No branch found in the request.');
|
||||
@@ -99,7 +93,6 @@ class Gitea extends Controller
|
||||
$webhook_secret = data_get($application, 'manual_webhook_secret_gitea');
|
||||
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
|
||||
if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) {
|
||||
ray('Invalid signature');
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -122,7 +115,6 @@ class Gitea extends Controller
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
ray('Deploying '.$application->name.' with branch '.$branch);
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -182,7 +174,6 @@ class Gitea extends Controller
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -228,12 +219,9 @@ class Gitea extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
ray($return_payloads);
|
||||
|
||||
return response($return_payloads);
|
||||
} catch (Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,15 +25,12 @@ class Github extends Controller
|
||||
$return_payloads = collect([]);
|
||||
$x_github_delivery = request()->header('X-GitHub-Delivery');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
ray('Maintenance mode is on');
|
||||
$epoch = now()->valueOf();
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) {
|
||||
return Str::contains($file, $x_github_delivery);
|
||||
})->first();
|
||||
if ($github_delivery_found) {
|
||||
ray('Webhook already found');
|
||||
|
||||
return;
|
||||
}
|
||||
$data = [
|
||||
@@ -73,7 +70,6 @@ class Github extends Controller
|
||||
$removed_files = data_get($payload, 'commits.*.removed');
|
||||
$modified_files = data_get($payload, 'commits.*.modified');
|
||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||
ray('Manual Webhook GitHub Push Event with branch: '.$branch);
|
||||
}
|
||||
if ($x_github_event === 'pull_request') {
|
||||
$action = data_get($payload, 'action');
|
||||
@@ -82,7 +78,6 @@ class Github extends Controller
|
||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||
$branch = data_get($payload, 'pull_request.head.ref');
|
||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||
ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id);
|
||||
}
|
||||
if (! $branch) {
|
||||
return response('Nothing to do. No branch found in the request.');
|
||||
@@ -104,7 +99,6 @@ class Github extends Controller
|
||||
$webhook_secret = data_get($application, 'manual_webhook_secret_github');
|
||||
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
|
||||
if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) {
|
||||
ray('Invalid signature');
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -127,7 +121,6 @@ class Github extends Controller
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
ray('Deploying '.$application->name.' with branch '.$branch);
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -232,12 +225,9 @@ class Github extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
ray($return_payloads);
|
||||
|
||||
return response($return_payloads);
|
||||
} catch (Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
@@ -249,15 +239,12 @@ class Github extends Controller
|
||||
$id = null;
|
||||
$x_github_delivery = $request->header('X-GitHub-Delivery');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
ray('Maintenance mode is on');
|
||||
$epoch = now()->valueOf();
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) {
|
||||
return Str::contains($file, $x_github_delivery);
|
||||
})->first();
|
||||
if ($github_delivery_found) {
|
||||
ray('Webhook already found');
|
||||
|
||||
return;
|
||||
}
|
||||
$data = [
|
||||
@@ -313,7 +300,6 @@ class Github extends Controller
|
||||
$removed_files = data_get($payload, 'commits.*.removed');
|
||||
$modified_files = data_get($payload, 'commits.*.modified');
|
||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||
ray('Webhook GitHub Push Event: '.$id.' with branch: '.$branch);
|
||||
}
|
||||
if ($x_github_event === 'pull_request') {
|
||||
$action = data_get($payload, 'action');
|
||||
@@ -322,7 +308,6 @@ class Github extends Controller
|
||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||
$branch = data_get($payload, 'pull_request.head.ref');
|
||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||
ray('Webhook GitHub Pull Request Event: '.$id.' with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id);
|
||||
}
|
||||
if (! $id || ! $branch) {
|
||||
return response('Nothing to do. No id or branch found.');
|
||||
@@ -356,7 +341,6 @@ class Github extends Controller
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
ray('Deploying '.$application->name.' with branch '.$branch);
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -460,8 +444,6 @@ class Github extends Controller
|
||||
|
||||
return response($return_payloads);
|
||||
} catch (Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
@@ -481,7 +463,7 @@ class Github extends Controller
|
||||
$private_key = data_get($data, 'pem');
|
||||
$webhook_secret = data_get($data, 'webhook_secret');
|
||||
$private_key = PrivateKey::create([
|
||||
'name' => $slug,
|
||||
'name' => "github-app-{$slug}",
|
||||
'private_key' => $private_key,
|
||||
'team_id' => $github_app->team_id,
|
||||
'is_git_related' => true,
|
||||
@@ -505,7 +487,6 @@ class Github extends Controller
|
||||
try {
|
||||
$installation_id = $request->get('installation_id');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
ray('Maintenance mode is on');
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
|
||||
@@ -17,7 +17,6 @@ class Gitlab extends Controller
|
||||
{
|
||||
try {
|
||||
if (app()->isDownForMaintenance()) {
|
||||
ray('Maintenance mode is on');
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
@@ -34,6 +33,7 @@ class Gitlab extends Controller
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$return_payloads = collect([]);
|
||||
$payload = $request->collect();
|
||||
$headers = $request->headers->all();
|
||||
@@ -49,6 +49,15 @@ class Gitlab extends Controller
|
||||
return response($return_payloads);
|
||||
}
|
||||
|
||||
if (empty($x_gitlab_token)) {
|
||||
$return_payloads->push([
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid signature.',
|
||||
]);
|
||||
|
||||
return response($return_payloads);
|
||||
}
|
||||
|
||||
if ($x_gitlab_event === 'push') {
|
||||
$branch = data_get($payload, 'ref');
|
||||
$full_name = data_get($payload, 'project.path_with_namespace');
|
||||
@@ -67,7 +76,6 @@ class Gitlab extends Controller
|
||||
$removed_files = data_get($payload, 'commits.*.removed');
|
||||
$modified_files = data_get($payload, 'commits.*.modified');
|
||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||
ray('Manual Webhook GitLab Push Event with branch: '.$branch);
|
||||
}
|
||||
if ($x_gitlab_event === 'merge_request') {
|
||||
$action = data_get($payload, 'object_attributes.action');
|
||||
@@ -84,7 +92,6 @@ class Gitlab extends Controller
|
||||
|
||||
return response($return_payloads);
|
||||
}
|
||||
ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id);
|
||||
}
|
||||
$applications = Application::where('git_repository', 'like', "%$full_name%");
|
||||
if ($x_gitlab_event === 'push') {
|
||||
@@ -117,7 +124,6 @@ class Gitlab extends Controller
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid signature.',
|
||||
]);
|
||||
ray('Invalid signature');
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -128,7 +134,6 @@ class Gitlab extends Controller
|
||||
'status' => 'failed',
|
||||
'message' => 'Server is not functional',
|
||||
]);
|
||||
ray('Server is not functional: '.$application->destination->server->name);
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -136,7 +141,6 @@ class Gitlab extends Controller
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
ray('Deploying '.$application->name.' with branch '.$branch);
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -171,7 +175,6 @@ class Gitlab extends Controller
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
ray('Deployments disabled for '.$application->name);
|
||||
}
|
||||
}
|
||||
if ($x_gitlab_event === 'merge_request') {
|
||||
@@ -207,7 +210,6 @@ class Gitlab extends Controller
|
||||
is_webhook: true,
|
||||
git_type: 'gitlab'
|
||||
);
|
||||
ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
@@ -219,7 +221,6 @@ class Gitlab extends Controller
|
||||
'status' => 'failed',
|
||||
'message' => 'Preview deployments disabled',
|
||||
]);
|
||||
ray('Preview deployments disabled for '.$application->name);
|
||||
}
|
||||
} elseif ($action === 'closed' || $action === 'close' || $action === 'merge') {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
@@ -253,8 +254,6 @@ class Gitlab extends Controller
|
||||
|
||||
return response($return_payloads);
|
||||
} catch (Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,26 +3,27 @@
|
||||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\ServerLimitCheckJob;
|
||||
use App\Jobs\SubscriptionInvoiceFailedJob;
|
||||
use App\Jobs\SubscriptionTrialEndedJob;
|
||||
use App\Jobs\SubscriptionTrialEndsSoonJob;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\Team;
|
||||
use App\Jobs\StripeProcessJob;
|
||||
use App\Models\Webhook;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Stripe extends Controller
|
||||
{
|
||||
protected $webhook;
|
||||
|
||||
public function events(Request $request)
|
||||
{
|
||||
try {
|
||||
$webhookSecret = config('subscription.stripe_webhook_secret');
|
||||
$signature = $request->header('Stripe-Signature');
|
||||
$event = \Stripe\Webhook::constructEvent(
|
||||
$request->getContent(),
|
||||
$signature,
|
||||
$webhookSecret
|
||||
);
|
||||
if (app()->isDownForMaintenance()) {
|
||||
ray('Maintenance mode is on');
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
@@ -37,265 +38,17 @@ class Stripe extends Controller
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
|
||||
|
||||
return;
|
||||
return response('Webhook received. Cool cool cool cool cool.', 200);
|
||||
}
|
||||
$webhookSecret = config('subscription.stripe_webhook_secret');
|
||||
$signature = $request->header('Stripe-Signature');
|
||||
$excludedPlans = config('subscription.stripe_excluded_plans');
|
||||
$event = \Stripe\Webhook::constructEvent(
|
||||
$request->getContent(),
|
||||
$signature,
|
||||
$webhookSecret
|
||||
);
|
||||
$webhook = Webhook::create([
|
||||
$this->webhook = Webhook::create([
|
||||
'type' => 'stripe',
|
||||
'payload' => $request->getContent(),
|
||||
]);
|
||||
$type = data_get($event, 'type');
|
||||
$data = data_get($event, 'data.object');
|
||||
switch ($type) {
|
||||
case 'radar.early_fraud_warning.created':
|
||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
||||
$id = data_get($data, 'id');
|
||||
$charge = data_get($data, 'charge');
|
||||
if ($charge) {
|
||||
$stripe->refunds->create(['charge' => $charge]);
|
||||
}
|
||||
$pi = data_get($data, 'payment_intent');
|
||||
$piData = $stripe->paymentIntents->retrieve($pi, []);
|
||||
$customerId = data_get($piData, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
Sleep::for(5)->seconds();
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
}
|
||||
if (! $subscription) {
|
||||
Sleep::for(5)->seconds();
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
}
|
||||
if ($subscription) {
|
||||
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
|
||||
$stripe->subscriptions->cancel($subscriptionId, []);
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
}
|
||||
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
|
||||
break;
|
||||
case 'checkout.session.completed':
|
||||
$clientReferenceId = data_get($data, 'client_reference_id');
|
||||
if (is_null($clientReferenceId)) {
|
||||
send_internal_notification('Checkout session completed without client reference id.');
|
||||
break;
|
||||
}
|
||||
$userId = Str::before($clientReferenceId, ':');
|
||||
$teamId = Str::after($clientReferenceId, ':');
|
||||
$subscriptionId = data_get($data, 'subscription');
|
||||
$customerId = data_get($data, 'customer');
|
||||
$team = Team::find($teamId);
|
||||
$found = $team->members->where('id', $userId)->first();
|
||||
if (! $found->isAdmin()) {
|
||||
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
|
||||
throw new Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
|
||||
}
|
||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||
if ($subscription) {
|
||||
// send_internal_notification('Old subscription activated for team: '.$teamId);
|
||||
$subscription->update([
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
]);
|
||||
} else {
|
||||
// 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,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case 'invoice.paid':
|
||||
$customerId = data_get($data, 'customer');
|
||||
$planId = data_get($data, 'lines.data.0.plan.id');
|
||||
if (Str::contains($excludedPlans, $planId)) {
|
||||
// send_internal_notification('Subscription excluded.');
|
||||
break;
|
||||
}
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
Sleep::for(5)->seconds();
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
||||
}
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => true,
|
||||
]);
|
||||
break;
|
||||
case 'invoice.payment_failed':
|
||||
$customerId = data_get($data, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
// send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
|
||||
StripeProcessJob::dispatch($event);
|
||||
|
||||
return response('No subscription found in Coolify.');
|
||||
}
|
||||
$team = data_get($subscription, 'team');
|
||||
if (! $team) {
|
||||
// send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
|
||||
|
||||
return response('No team found in Coolify.');
|
||||
}
|
||||
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);
|
||||
}
|
||||
break;
|
||||
case 'payment_intent.payment_failed':
|
||||
$customerId = data_get($data, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
// send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
|
||||
|
||||
return response('No subscription found in Coolify.');
|
||||
}
|
||||
if ($subscription->stripe_invoice_paid) {
|
||||
// send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
|
||||
|
||||
return;
|
||||
}
|
||||
send_internal_notification('Subscription payment failed for customer: '.$customerId);
|
||||
break;
|
||||
case 'customer.subscription.updated':
|
||||
$customerId = data_get($data, 'customer');
|
||||
$status = data_get($data, 'status');
|
||||
$subscriptionId = data_get($data, 'items.data.0.subscription');
|
||||
$planId = data_get($data, 'items.data.0.plan.id');
|
||||
if (Str::contains($excludedPlans, $planId)) {
|
||||
// send_internal_notification('Subscription excluded.');
|
||||
break;
|
||||
}
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
Sleep::for(5)->seconds();
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
}
|
||||
if (! $subscription) {
|
||||
if ($status === 'incomplete_expired') {
|
||||
// send_internal_notification('Subscription incomplete expired for customer: '.$customerId);
|
||||
|
||||
return response('Subscription incomplete expired', 200);
|
||||
}
|
||||
// send_internal_notification('No subscription found for: '.$customerId);
|
||||
|
||||
return response('No subscription found', 400);
|
||||
}
|
||||
$trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended');
|
||||
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
|
||||
$alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end');
|
||||
$feedback = data_get($data, 'cancellation_details.feedback');
|
||||
$comment = data_get($data, 'cancellation_details.comment');
|
||||
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
|
||||
if (str($lookup_key)->contains('ultimate') || str($lookup_key)->contains('dynamic')) {
|
||||
if (str($lookup_key)->contains('dynamic')) {
|
||||
$quantity = data_get($data, 'items.data.0.quantity', 2);
|
||||
} else {
|
||||
$quantity = data_get($data, 'items.data.0.quantity', 10);
|
||||
}
|
||||
$team = data_get($subscription, 'team');
|
||||
if ($team) {
|
||||
$team->update([
|
||||
'custom_server_limit' => $quantity,
|
||||
]);
|
||||
}
|
||||
ServerLimitCheckJob::dispatch($team);
|
||||
}
|
||||
$subscription->update([
|
||||
'stripe_feedback' => $feedback,
|
||||
'stripe_comment' => $comment,
|
||||
'stripe_plan_id' => $planId,
|
||||
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
|
||||
]);
|
||||
if ($status === 'paused' || $status === 'incomplete_expired') {
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
// send_internal_notification('Subscription paused or incomplete for customer: '.$customerId);
|
||||
}
|
||||
|
||||
// Trial ended but subscribed, reactive servers
|
||||
if ($trialEndedAlready && $status === 'active') {
|
||||
$team = data_get($subscription, 'team');
|
||||
$team->trialEndedButSubscribed();
|
||||
}
|
||||
|
||||
if ($feedback) {
|
||||
$reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
|
||||
if ($comment) {
|
||||
$reason .= ' with comment: \''.$comment."'";
|
||||
}
|
||||
// send_internal_notification($reason);
|
||||
}
|
||||
if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) {
|
||||
if ($cancelAtPeriodEnd) {
|
||||
// send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id);
|
||||
} else {
|
||||
// send_internal_notification('customer.subscription.updated for customer: '.$customerId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'customer.subscription.deleted':
|
||||
// End subscription
|
||||
$customerId = data_get($data, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
||||
$team = data_get($subscription, 'team');
|
||||
if ($team) {
|
||||
$team->trialEnded();
|
||||
}
|
||||
$subscription->update([
|
||||
'stripe_subscription_id' => null,
|
||||
'stripe_plan_id' => null,
|
||||
'stripe_cancel_at_period_end' => false,
|
||||
'stripe_invoice_paid' => false,
|
||||
'stripe_trial_already_ended' => false,
|
||||
]);
|
||||
// send_internal_notification('customer.subscription.deleted for customer: '.$customerId);
|
||||
break;
|
||||
case 'customer.subscription.trial_will_end':
|
||||
// Not used for now
|
||||
$customerId = data_get($data, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
||||
$team = data_get($subscription, 'team');
|
||||
if (! $team) {
|
||||
throw new Exception('No team found for subscription: '.$subscription->id);
|
||||
}
|
||||
SubscriptionTrialEndsSoonJob::dispatch($team);
|
||||
break;
|
||||
case 'customer.subscription.paused':
|
||||
$customerId = data_get($data, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
||||
$team = data_get($subscription, 'team');
|
||||
if (! $team) {
|
||||
throw new Exception('No team found for subscription: '.$subscription->id);
|
||||
}
|
||||
$team->trialEnded();
|
||||
$subscription->update([
|
||||
'stripe_trial_already_ended' => true,
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
SubscriptionTrialEndedJob::dispatch($team);
|
||||
// send_internal_notification('Subscription paused for customer: '.$customerId);
|
||||
break;
|
||||
default:
|
||||
// Unhandled event type
|
||||
}
|
||||
return response('Webhook received. Cool cool cool cool cool.', 200);
|
||||
} catch (Exception $e) {
|
||||
if ($type !== 'payment_intent.payment_failed') {
|
||||
send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
|
||||
}
|
||||
$webhook->update([
|
||||
$this->webhook->update([
|
||||
'status' => 'failed',
|
||||
'failure_reason' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Waitlist as ModelsWaitlist;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Waitlist extends Controller
|
||||
{
|
||||
public function confirm(Request $request)
|
||||
{
|
||||
$email = request()->get('email');
|
||||
$confirmation_code = request()->get('confirmation_code');
|
||||
ray($email, $confirmation_code);
|
||||
try {
|
||||
$found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
|
||||
if ($found) {
|
||||
if (! $found->verified) {
|
||||
if ($found->created_at > now()->subMinutes(config('constants.waitlist.expiration'))) {
|
||||
$found->verified = true;
|
||||
$found->save();
|
||||
send_internal_notification('Waitlist confirmed: '.$email);
|
||||
|
||||
return 'Thank you for confirming your email address. We will notify you when you are next in line.';
|
||||
} else {
|
||||
$found->delete();
|
||||
send_internal_notification('Waitlist expired: '.$email);
|
||||
|
||||
return 'Your confirmation code has expired. Please sign up again.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
} catch (Exception $e) {
|
||||
send_internal_notification('Waitlist confirmation failed: '.$e->getMessage());
|
||||
ray($e->getMessage());
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
public function cancel(Request $request)
|
||||
{
|
||||
$email = request()->get('email');
|
||||
$confirmation_code = request()->get('confirmation_code');
|
||||
try {
|
||||
$found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
|
||||
if ($found && ! $found->verified) {
|
||||
$found->delete();
|
||||
send_internal_notification('Waitlist cancelled: '.$email);
|
||||
|
||||
return 'Your email address has been removed from the waitlist.';
|
||||
}
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
} catch (Exception $e) {
|
||||
send_internal_notification('Waitlist cancellation failed: '.$e->getMessage());
|
||||
ray($e->getMessage());
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,5 +69,7 @@ class Kernel extends HttpKernel
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
|
||||
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
|
||||
'api.ability' => \App\Http\Middleware\ApiAbility::class,
|
||||
'api.sensitive' => \App\Http\Middleware\ApiSensitiveData::class,
|
||||
];
|
||||
}
|
||||
|
||||
27
app/Http/Middleware/ApiAbility.php
Normal file
27
app/Http/Middleware/ApiAbility.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
|
||||
|
||||
class ApiAbility extends CheckForAnyAbility
|
||||
{
|
||||
public function handle($request, $next, ...$abilities)
|
||||
{
|
||||
try {
|
||||
if ($request->user()->tokenCan('root')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return parent::handle($request, $next, ...$abilities);
|
||||
} catch (\Illuminate\Auth\AuthenticationException $e) {
|
||||
return response()->json([
|
||||
'message' => 'Unauthenticated.',
|
||||
], 401);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'message' => 'Missing required permissions: '.implode(', ', $abilities),
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ class ApiAllowed
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
ray()->clearAll();
|
||||
if (isCloud()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
21
app/Http/Middleware/ApiSensitiveData.php
Normal file
21
app/Http/Middleware/ApiSensitiveData.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ApiSensitiveData
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$token = $request->user()->currentAccessToken();
|
||||
|
||||
// Allow access to sensitive data if token has root or read:sensitive permission
|
||||
$request->attributes->add([
|
||||
'can_read_sensitive' => $token->can('root') || $token->can('read:sensitive'),
|
||||
]);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IgnoreReadOnlyApiToken
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
if ($token->can('*')) {
|
||||
return $next($request);
|
||||
}
|
||||
if ($token->can('read-only')) {
|
||||
return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class OnlyRootApiToken
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
if ($token->can('*')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private ?string $buildTarget = null;
|
||||
|
||||
private bool $disableBuildCache = false;
|
||||
|
||||
private Collection $saved_outputs;
|
||||
|
||||
private ?string $full_healthcheck_url = null;
|
||||
@@ -166,6 +168,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(int $application_deployment_queue_id)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
|
||||
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
|
||||
$this->application = Application::find($this->application_deployment_queue->application_id);
|
||||
$this->build_pack = data_get($this->application, 'build_pack');
|
||||
@@ -176,7 +180,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
|
||||
$this->commit = $this->application_deployment_queue->commit;
|
||||
$this->rollback = $this->application_deployment_queue->rollback;
|
||||
$this->disableBuildCache = $this->application->settings->disable_build_cache;
|
||||
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
|
||||
if ($this->disableBuildCache) {
|
||||
$this->force_rebuild = true;
|
||||
}
|
||||
$this->restart_only = $this->application_deployment_queue->restart_only;
|
||||
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
|
||||
$this->only_this_server = $this->application_deployment_queue->only_this_server;
|
||||
@@ -208,7 +216,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}";
|
||||
}
|
||||
}
|
||||
ray('New container name: ', $this->container_name)->green();
|
||||
|
||||
$this->saved_outputs = collect();
|
||||
|
||||
@@ -226,12 +233,17 @@ 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,
|
||||
]);
|
||||
if (! $this->server->isFunctional()) {
|
||||
if ($this->server->isFunctional() === false) {
|
||||
$this->application_deployment_queue->addLogEntry('Server is not functional.');
|
||||
$this->fail('Server is not functional.');
|
||||
|
||||
@@ -298,7 +310,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->pull_request_id !== 0 && $this->application->is_github_based()) {
|
||||
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::ERROR);
|
||||
}
|
||||
ray($e);
|
||||
$this->fail($e);
|
||||
throw $e;
|
||||
} finally {
|
||||
@@ -346,8 +357,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
private function post_deployment()
|
||||
{
|
||||
if ($this->server->isProxyShouldRun()) {
|
||||
GetContainersStatus::dispatch($this->server)->onQueue('high');
|
||||
// dispatch(new ContainerStatusJob($this->server));
|
||||
GetContainersStatus::dispatch($this->server);
|
||||
}
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
if ($this->pull_request_id !== 0) {
|
||||
@@ -389,7 +399,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
$this->dockerImageTag = $this->application->docker_registry_image_tag;
|
||||
}
|
||||
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.'");
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.");
|
||||
$this->generate_image_names();
|
||||
$this->prepare_builder_image();
|
||||
@@ -460,7 +469,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
|
||||
$this->save_environment_variables();
|
||||
if (! is_null($this->env_filename)) {
|
||||
$services = collect($composeFile['services']);
|
||||
$services = collect(data_get($composeFile, 'services', []));
|
||||
$services = $services->map(function ($service, $name) {
|
||||
$service['env_file'] = [$this->env_filename];
|
||||
|
||||
@@ -712,38 +721,26 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
$forceFail = true;
|
||||
if (str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
ray('empty docker_registry_image_name');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->restart_only) {
|
||||
ray('restart_only');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->application->build_pack === 'dockerimage') {
|
||||
ray('dockerimage');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->use_build_server) {
|
||||
ray('use_build_server');
|
||||
$forceFail = true;
|
||||
}
|
||||
if ($this->server->isSwarm() && $this->build_pack !== 'dockerimage') {
|
||||
ray('isSwarm');
|
||||
$forceFail = true;
|
||||
}
|
||||
if ($this->application->additional_servers->count() > 0) {
|
||||
ray('additional_servers');
|
||||
$forceFail = true;
|
||||
}
|
||||
if ($this->is_this_additional_server) {
|
||||
ray('this is an additional_servers, no pushy pushy');
|
||||
|
||||
return;
|
||||
}
|
||||
ray('push_to_docker_registry noww: '.$this->production_image_name);
|
||||
try {
|
||||
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
|
||||
$this->application_deployment_queue->addLogEntry('----------------------------------------');
|
||||
@@ -775,7 +772,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($forceFail) {
|
||||
throw new RuntimeException($e->getMessage(), 69420);
|
||||
}
|
||||
ray($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1334,7 +1330,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
private function prepare_builder_image()
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$helperImage = config('constants.coolify.helper_image');
|
||||
$helperImage = "{$helperImage}:{$settings->helper_version}";
|
||||
// Get user home directory
|
||||
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
|
||||
@@ -1386,8 +1382,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return;
|
||||
}
|
||||
if ($destination_ids->contains($this->destination->id)) {
|
||||
ray('Same destination found in additional destinations. Skipping.');
|
||||
|
||||
return;
|
||||
}
|
||||
foreach ($destination_ids as $destination_id) {
|
||||
@@ -1854,7 +1848,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
|
||||
if ($this->pull_request_id === 0) {
|
||||
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
|
||||
$custom_compose = convertDockerRunToCompose($this->application->custom_docker_run_options);
|
||||
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
|
||||
if (! $this->application->settings->custom_internal_name) {
|
||||
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
|
||||
@@ -1988,6 +1982,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->build_args = $this->build_args->implode(' ');
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('----------------------------------------');
|
||||
if ($this->disableBuildCache) {
|
||||
$this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
|
||||
}
|
||||
if ($this->application->build_pack === 'static') {
|
||||
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
|
||||
} else {
|
||||
@@ -2008,22 +2005,11 @@ COPY . .
|
||||
RUN rm -f /usr/share/nginx/html/nginx.conf
|
||||
RUN rm -f /usr/share/nginx/html/Dockerfile
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$nginx_config = base64_encode('server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri.html $uri/index.html $uri/ /index.html =404;
|
||||
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
|
||||
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
|
||||
} else {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}');
|
||||
} else {
|
||||
if ($this->application->build_pack === 'nixpacks') {
|
||||
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
|
||||
@@ -2086,23 +2072,11 @@ WORKDIR /usr/share/nginx/html/
|
||||
LABEL coolify.deploymentId={$this->deployment_uuid}
|
||||
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
$nginx_config = base64_encode('server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri.html $uri/index.html $uri/ /index.html =404;
|
||||
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
|
||||
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
|
||||
} else {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}');
|
||||
}
|
||||
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
@@ -2449,7 +2423,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
if ($this->application->build_pack !== 'dockercompose') {
|
||||
$code = $exception->getCode();
|
||||
ray($code);
|
||||
if ($code !== 69420) {
|
||||
// 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
|
||||
if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) {
|
||||
|
||||
@@ -25,14 +25,14 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
public ApplicationPreview $preview,
|
||||
public ProcessStatus $status,
|
||||
public ?string $deployment_uuid = null
|
||||
) {}
|
||||
) {
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
if ($this->application->is_public_repository()) {
|
||||
ray('Public repository. Skipping comment update.');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->status === ProcessStatus::CLOSED) {
|
||||
@@ -53,16 +53,12 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
$this->body .= '[Open Build Logs]('.$this->build_logs_url.")\n\n\n";
|
||||
$this->body .= 'Last updated at: '.now()->toDateTimeString().' CET';
|
||||
|
||||
ray('Updating comment', $this->body);
|
||||
if ($this->preview->pull_request_issue_comment_id) {
|
||||
$this->update_comment();
|
||||
} else {
|
||||
$this->create_comment();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
|
||||
return $e;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +69,6 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'body' => $this->body,
|
||||
], throwError: false);
|
||||
if (data_get($data, 'message') === 'Not Found') {
|
||||
ray('Comment not found. Creating new one.');
|
||||
$this->create_comment();
|
||||
}
|
||||
}
|
||||
|
||||
52
app/Jobs/CheckAndStartSentinelJob.php
Normal file
52
app/Jobs/CheckAndStartSentinelJob.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Server\StartSentinel;
|
||||
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 CheckAndStartSentinelJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 120;
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$latestVersion = get_latest_sentinel_version();
|
||||
|
||||
// Check if sentinel is running
|
||||
$sentinelFound = instant_remote_process(['docker inspect coolify-sentinel'], $this->server, false);
|
||||
$sentinelFoundJson = json_decode($sentinelFound, true);
|
||||
$sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited');
|
||||
if ($sentinelStatus !== 'running') {
|
||||
StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion);
|
||||
|
||||
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);
|
||||
if (empty($runningVersion)) {
|
||||
$runningVersion = '0.0.0';
|
||||
}
|
||||
if ($latestVersion === '0.0.0' && $runningVersion === '0.0.0') {
|
||||
StartSentinel::run(server: $this->server, restart: true, latestVersion: 'latest');
|
||||
|
||||
return;
|
||||
} else {
|
||||
if (version_compare($runningVersion, $latestVersion, '<')) {
|
||||
StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$versions = $response->json();
|
||||
|
||||
$latest_version = data_get($versions, 'coolify.v4.version');
|
||||
$current_version = config('version');
|
||||
$current_version = config('constants.coolify.version');
|
||||
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// New version available
|
||||
|
||||
39
app/Jobs/CheckHelperImageJob.php
Normal file
39
app/Jobs/CheckHelperImageJob.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
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;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CheckHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
$settings = instanceSettings();
|
||||
$latest_version = data_get($versions, 'coolify.helper.version');
|
||||
$current_version = $settings->helper_version;
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
$settings->update(['helper_version' => $latest_version]);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('CheckHelperImageJob failed with: '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\License\CheckResaleLicense;
|
||||
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 CheckResaleLicenseJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
CheckResaleLicense::run();
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('CheckResaleLicenseJob failed with: '.$e->getMessage());
|
||||
ray($e);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,18 +20,15 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
ray('Cleaning up helper containers on '.$this->server->name);
|
||||
$containers = instant_remote_process(['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) {
|
||||
ray('Removing container '.$containerId);
|
||||
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('CleanupHelperContainersJob failed with error: '.$e->getMessage());
|
||||
ray($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user