Merge branch 'coollabsio:next' into next
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Actions\Application;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\Application;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -14,30 +15,46 @@ class StopApplication
|
||||
|
||||
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
$server = $application->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
|
||||
if ($server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$containersToStop = $application->getContainersToStop($previewDeployments);
|
||||
$application->stopContainers($containersToStop, $server);
|
||||
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
$application->delete_connected_networks($application->uuid);
|
||||
}
|
||||
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $e->getMessage();
|
||||
$servers = collect([$application->destination->server]);
|
||||
if ($application?->additional_servers?->count() > 0) {
|
||||
$servers = $servers->merge($application->additional_servers);
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
|
||||
if ($server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$containers = $previewDeployments
|
||||
? getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true)
|
||||
: getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
|
||||
$containersToStop = $containers->pluck('Names')->toArray();
|
||||
|
||||
foreach ($containersToStop as $containerName) {
|
||||
instant_remote_process(command: [
|
||||
"docker stop --time=30 $containerName",
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
}
|
||||
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
$application->deleteConnectedNetworks();
|
||||
}
|
||||
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
ServiceStatusChanged::dispatch($application->environment->project->team->id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ class StopApplicationOneServer
|
||||
$containerName = data_get($container, 'Names');
|
||||
if ($containerName) {
|
||||
instant_remote_process(
|
||||
["docker rm -f {$containerName}"],
|
||||
[
|
||||
"docker stop --time=30 $containerName",
|
||||
"docker rm -f $containerName",
|
||||
],
|
||||
$server
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ class RunRemoteProcess
|
||||
]);
|
||||
|
||||
$processResult = $process->wait();
|
||||
// $processResult = Process::timeout($timeout)->run($this->getCommand(), $this->handleOutput(...));
|
||||
if ($this->activity->properties->get('status') === ProcessStatus::ERROR->value) {
|
||||
$status = ProcessStatus::ERROR;
|
||||
} else {
|
||||
@@ -105,14 +104,11 @@ class RunRemoteProcess
|
||||
$this->activity->save();
|
||||
if ($this->call_event_on_finish) {
|
||||
try {
|
||||
if ($this->call_event_data) {
|
||||
event(resolve("App\\Events\\$this->call_event_on_finish", [
|
||||
'data' => $this->call_event_data,
|
||||
]));
|
||||
$eventClass = "App\\Events\\$this->call_event_on_finish";
|
||||
if (! is_null($this->call_event_data)) {
|
||||
event(new $eventClass($this->call_event_data));
|
||||
} else {
|
||||
event(resolve("App\\Events\\$this->call_event_on_finish", [
|
||||
'userId' => $this->activity->causer_id,
|
||||
]));
|
||||
event(new $eventClass($this->activity->causer_id));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Error calling event: '.$e->getMessage());
|
||||
|
||||
@@ -22,75 +22,39 @@ class StartDatabaseProxy
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
|
||||
{
|
||||
$internalPort = null;
|
||||
$type = $database->getMorphClass();
|
||||
$databaseType = $database->database_type;
|
||||
$network = data_get($database, 'destination.network');
|
||||
$server = data_get($database, 'destination.server');
|
||||
$containerName = data_get($database, 'uuid');
|
||||
$proxyContainerName = "{$database->uuid}-proxy";
|
||||
$isSSLEnabled = $database->enable_ssl ?? false;
|
||||
|
||||
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
|
||||
$databaseType = $database->databaseType();
|
||||
// $connectPredefined = data_get($database, 'service.connect_to_docker_network');
|
||||
$network = $database->service->uuid;
|
||||
$server = data_get($database, 'service.destination.server');
|
||||
$proxyContainerName = "{$database->service->uuid}-proxy";
|
||||
switch ($databaseType) {
|
||||
case 'standalone-mariadb':
|
||||
$type = \App\Models\StandaloneMariadb::class;
|
||||
$containerName = "mariadb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
$type = \App\Models\StandaloneMongodb::class;
|
||||
$containerName = "mongodb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
$type = \App\Models\StandaloneMysql::class;
|
||||
$containerName = "mysql-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
$type = \App\Models\StandalonePostgresql::class;
|
||||
$containerName = "postgresql-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
$type = \App\Models\StandaloneRedis::class;
|
||||
$containerName = "redis-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-keydb':
|
||||
$type = \App\Models\StandaloneKeydb::class;
|
||||
$containerName = "keydb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-dragonfly':
|
||||
$type = \App\Models\StandaloneDragonfly::class;
|
||||
$containerName = "dragonfly-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-clickhouse':
|
||||
$type = \App\Models\StandaloneClickhouse::class;
|
||||
$containerName = "clickhouse-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-supabase/postgres':
|
||||
$type = \App\Models\StandalonePostgresql::class;
|
||||
$containerName = "supabase-db-{$database->service->uuid}";
|
||||
break;
|
||||
}
|
||||
$containerName = "{$database->name}-{$database->service->uuid}";
|
||||
}
|
||||
if ($type === \App\Models\StandaloneRedis::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === \App\Models\StandalonePostgresql::class) {
|
||||
$internalPort = 5432;
|
||||
} elseif ($type === \App\Models\StandaloneMongodb::class) {
|
||||
$internalPort = 27017;
|
||||
} elseif ($type === \App\Models\StandaloneMysql::class) {
|
||||
$internalPort = 3306;
|
||||
} elseif ($type === \App\Models\StandaloneMariadb::class) {
|
||||
$internalPort = 3306;
|
||||
} elseif ($type === \App\Models\StandaloneKeydb::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === \App\Models\StandaloneDragonfly::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === \App\Models\StandaloneClickhouse::class) {
|
||||
$internalPort = 9000;
|
||||
$internalPort = match ($databaseType) {
|
||||
'standalone-mariadb', 'standalone-mysql' => 3306,
|
||||
'standalone-postgresql', 'standalone-supabase/postgres' => 5432,
|
||||
'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6379,
|
||||
'standalone-clickhouse' => 9000,
|
||||
'standalone-mongodb' => 27017,
|
||||
default => throw new \Exception("Unsupported database type: $databaseType"),
|
||||
};
|
||||
if ($isSSLEnabled) {
|
||||
$internalPort = match ($databaseType) {
|
||||
'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6380,
|
||||
default => throw new \Exception("Unsupported database type: $databaseType"),
|
||||
};
|
||||
}
|
||||
|
||||
$configuration_dir = database_proxy_dir($database->uuid);
|
||||
if (isDev()) {
|
||||
$configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$database->uuid.'/proxy';
|
||||
}
|
||||
$nginxconf = <<<EOF
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
@@ -106,19 +70,10 @@ class StartDatabaseProxy
|
||||
proxy_pass $containerName:$internalPort;
|
||||
}
|
||||
}
|
||||
EOF;
|
||||
$dockerfile = <<< 'EOF'
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
EOF;
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
$proxyContainerName => [
|
||||
'build' => [
|
||||
'context' => $configuration_dir,
|
||||
'dockerfile' => 'Dockerfile',
|
||||
],
|
||||
'image' => 'nginx:stable-alpine',
|
||||
'container_name' => $proxyContainerName,
|
||||
'restart' => RESTART_MODE,
|
||||
@@ -128,6 +83,13 @@ class StartDatabaseProxy
|
||||
'networks' => [
|
||||
$network,
|
||||
],
|
||||
'volumes' => [
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => "$configuration_dir/nginx.conf",
|
||||
'target' => '/etc/nginx/nginx.conf',
|
||||
],
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
@@ -150,15 +112,13 @@ class StartDatabaseProxy
|
||||
];
|
||||
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
|
||||
$nginxconf_base64 = base64_encode($nginxconf);
|
||||
$dockerfile_base64 = base64_encode($dockerfile);
|
||||
instant_remote_process(["docker rm -f $proxyContainerName"], $server, false);
|
||||
instant_remote_process([
|
||||
"mkdir -p $configuration_dir",
|
||||
"echo '{$dockerfile_base64}' | base64 -d | tee $configuration_dir/Dockerfile > /dev/null",
|
||||
"echo '{$nginxconf_base64}' | base64 -d | tee $configuration_dir/nginx.conf > /dev/null",
|
||||
"echo '{$dockercompose_base64}' | base64 -d | tee $configuration_dir/docker-compose.yaml > /dev/null",
|
||||
"docker compose --project-directory {$configuration_dir} pull",
|
||||
"docker compose --project-directory {$configuration_dir} up --build -d",
|
||||
"docker compose --project-directory {$configuration_dir} up -d",
|
||||
], $server);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -16,24 +18,81 @@ class StartDragonfly
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneDragonfly $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
"echo 'Directories created successfully.'",
|
||||
];
|
||||
|
||||
if (! $this->database->enable_ssl) {
|
||||
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||
$this->database->sslCertificates()->delete();
|
||||
$this->database->fileStorages()
|
||||
->where('resource_type', $this->database->getMorphClass())
|
||||
->where('resource_id', $this->database->id)
|
||||
->get()
|
||||
->filter(function ($storage) {
|
||||
return in_array($storage->mount_path, [
|
||||
'/etc/dragonfly/certs/server.crt',
|
||||
'/etc/dragonfly/certs/server.key',
|
||||
]);
|
||||
})
|
||||
->each(function ($storage) {
|
||||
$storage->delete();
|
||||
});
|
||||
} else {
|
||||
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||
|
||||
$server = $this->database->destination->server;
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $this->ssl_certificate) {
|
||||
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||
commonName: $this->database->uuid,
|
||||
resourceType: $this->database->getMorphClass(),
|
||||
resourceId: $this->database->id,
|
||||
serverId: $server->id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $this->configuration_dir,
|
||||
mountPath: '/etc/dragonfly/certs',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$startCommand = $this->buildStartCommand();
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
@@ -70,27 +129,55 @@ class StartDragonfly
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
$persistent_storages
|
||||
);
|
||||
}
|
||||
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
$persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||
'target' => '/etc/dragonfly/certs/coolify-ca.crt',
|
||||
'read_only' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
@@ -102,12 +189,32 @@ class StartDragonfly
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||
}
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
private function buildStartCommand(): string
|
||||
{
|
||||
$command = "dragonfly --requirepass {$this->database->dragonfly_password}";
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$sslArgs = [
|
||||
'--tls',
|
||||
'--tls_cert_file /etc/dragonfly/certs/server.crt',
|
||||
'--tls_key_file /etc/dragonfly/certs/server.key',
|
||||
'--tls_ca_cert_file /etc/dragonfly/certs/coolify-ca.crt',
|
||||
];
|
||||
$command .= ' '.implode(' ', $sslArgs);
|
||||
}
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
private function generate_local_persistent_volumes()
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -17,26 +19,84 @@ class StartKeydb
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneKeydb $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
"echo 'Directories created successfully.'",
|
||||
];
|
||||
|
||||
if (! $this->database->enable_ssl) {
|
||||
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||
$this->database->sslCertificates()->delete();
|
||||
$this->database->fileStorages()
|
||||
->where('resource_type', $this->database->getMorphClass())
|
||||
->where('resource_id', $this->database->id)
|
||||
->get()
|
||||
->filter(function ($storage) {
|
||||
return in_array($storage->mount_path, [
|
||||
'/etc/keydb/certs/server.crt',
|
||||
'/etc/keydb/certs/server.key',
|
||||
]);
|
||||
})
|
||||
->each(function ($storage) {
|
||||
$storage->delete();
|
||||
});
|
||||
} else {
|
||||
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||
|
||||
$server = $this->database->destination->server;
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $this->ssl_certificate) {
|
||||
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||
commonName: $this->database->uuid,
|
||||
resourceType: $this->database->getMorphClass(),
|
||||
resourceId: $this->database->id,
|
||||
serverId: $server->id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $this->configuration_dir,
|
||||
mountPath: '/etc/keydb/certs',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->add_custom_keydb();
|
||||
|
||||
$startCommand = $this->buildStartCommand();
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
$container_name => [
|
||||
@@ -72,34 +132,67 @@ class StartKeydb
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
$persistent_storages
|
||||
);
|
||||
}
|
||||
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
$persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
|
||||
if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/keydb.conf',
|
||||
'target' => '/etc/keydb/keydb.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/keydb.conf',
|
||||
'target' => '/etc/keydb/keydb.conf',
|
||||
'read_only' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||
'target' => '/etc/keydb/certs/coolify-ca.crt',
|
||||
'read_only' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
@@ -112,6 +205,9 @@ class StartKeydb
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||
}
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
@@ -177,4 +273,36 @@ class StartKeydb
|
||||
instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
|
||||
Storage::disk('local')->delete("tmp/keydb.conf_{$this->database->uuid}");
|
||||
}
|
||||
|
||||
private function buildStartCommand(): string
|
||||
{
|
||||
$hasKeydbConf = ! is_null($this->database->keydb_conf) && ! empty($this->database->keydb_conf);
|
||||
$keydbConfPath = '/etc/keydb/keydb.conf';
|
||||
|
||||
if ($hasKeydbConf) {
|
||||
$confContent = $this->database->keydb_conf;
|
||||
$hasRequirePass = str_contains($confContent, 'requirepass');
|
||||
|
||||
if ($hasRequirePass) {
|
||||
$command = "keydb-server $keydbConfPath";
|
||||
} else {
|
||||
$command = "keydb-server $keydbConfPath --requirepass {$this->database->keydb_password}";
|
||||
}
|
||||
} else {
|
||||
$command = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||
}
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$sslArgs = [
|
||||
'--tls-port 6380',
|
||||
'--tls-cert-file /etc/keydb/certs/server.crt',
|
||||
'--tls-key-file /etc/keydb/certs/server.key',
|
||||
'--tls-ca-cert-file /etc/keydb/certs/coolify-ca.crt',
|
||||
'--tls-auth-clients optional',
|
||||
];
|
||||
$command .= ' '.implode(' ', $sslArgs);
|
||||
}
|
||||
|
||||
return $command;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -16,6 +18,8 @@ class StartMariadb
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneMariadb $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
@@ -25,9 +29,64 @@ class StartMariadb
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
"echo 'Directories created successfully.'",
|
||||
];
|
||||
|
||||
if (! $this->database->enable_ssl) {
|
||||
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||
|
||||
$this->database->sslCertificates()->delete();
|
||||
|
||||
$this->database->fileStorages()
|
||||
->where('resource_type', $this->database->getMorphClass())
|
||||
->where('resource_id', $this->database->id)
|
||||
->get()
|
||||
->filter(function ($storage) {
|
||||
return in_array($storage->mount_path, [
|
||||
'/etc/mysql/certs/server.crt',
|
||||
'/etc/mysql/certs/server.key',
|
||||
]);
|
||||
})
|
||||
->each(function ($storage) {
|
||||
$storage->delete();
|
||||
});
|
||||
} else {
|
||||
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||
|
||||
$server = $this->database->destination->server;
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $this->ssl_certificate) {
|
||||
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||
commonName: $this->database->uuid,
|
||||
resourceType: $this->database->getMorphClass(),
|
||||
resourceId: $this->database->id,
|
||||
serverId: $server->id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $this->configuration_dir,
|
||||
mountPath: '/etc/mysql/certs',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
@@ -67,38 +126,81 @@ class StartMariadb
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
}
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
$persistent_storages
|
||||
);
|
||||
}
|
||||
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
$persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||
'target' => '/etc/mysql/certs/coolify-ca.crt',
|
||||
'read_only' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
[
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||
'read_only' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
if ($this->database->enable_ssl) {
|
||||
$docker_compose['services'][$container_name]['command'] = [
|
||||
'mariadbd',
|
||||
'--ssl-cert=/etc/mysql/certs/server.crt',
|
||||
'--ssl-key=/etc/mysql/certs/server.key',
|
||||
'--ssl-ca=/etc/mysql/certs/coolify-ca.crt',
|
||||
'--require-secure-transport=1',
|
||||
];
|
||||
}
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
@@ -109,6 +211,9 @@ class StartMariadb
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = executeInDocker($this->database->uuid, 'chown mysql:mysql /etc/mysql/certs/server.crt /etc/mysql/certs/server.key');
|
||||
}
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -16,6 +18,8 @@ class StartMongodb
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneMongodb $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
@@ -24,16 +28,69 @@ 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.'",
|
||||
"echo 'Creating directories.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
"echo 'Directories created successfully.'",
|
||||
];
|
||||
|
||||
if (! $this->database->enable_ssl) {
|
||||
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||
|
||||
$this->database->sslCertificates()->delete();
|
||||
|
||||
$this->database->fileStorages()
|
||||
->where('resource_type', $this->database->getMorphClass())
|
||||
->where('resource_id', $this->database->id)
|
||||
->get()
|
||||
->filter(function ($storage) {
|
||||
return in_array($storage->mount_path, [
|
||||
'/etc/mongo/certs/server.pem',
|
||||
]);
|
||||
})
|
||||
->each(function ($storage) {
|
||||
$storage->delete();
|
||||
});
|
||||
} else {
|
||||
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||
|
||||
$server = $this->database->destination->server;
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $this->ssl_certificate) {
|
||||
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||
commonName: $this->database->uuid,
|
||||
resourceType: $this->database->getMorphClass(),
|
||||
resourceId: $this->database->id,
|
||||
serverId: $server->id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $this->configuration_dir,
|
||||
mountPath: '/etc/mongo/certs',
|
||||
isPemKeyFileRequired: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
@@ -79,47 +136,123 @@ class StartMongodb
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
$persistent_storages
|
||||
);
|
||||
}
|
||||
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
$persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/mongod.conf',
|
||||
'target' => '/etc/mongo/mongod.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
|
||||
|
||||
if (! empty($this->database->mongo_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[[
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/mongod.conf',
|
||||
'target' => '/etc/mongo/mongod.conf',
|
||||
'read_only' => true,
|
||||
]]
|
||||
);
|
||||
$docker_compose['services'][$container_name]['command'] = ['mongod', '--config', '/etc/mongo/mongod.conf'];
|
||||
}
|
||||
|
||||
$this->add_default_database();
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
|
||||
'target' => '/docker-entrypoint-initdb.d',
|
||||
'read_only' => true,
|
||||
];
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[[
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
|
||||
'target' => '/docker-entrypoint-initdb.d',
|
||||
'read_only' => true,
|
||||
]]
|
||||
);
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||
'target' => '/etc/mongo/certs/ca.pem',
|
||||
'read_only' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$commandParts = ['mongod'];
|
||||
|
||||
if (! empty($this->database->mongo_conf)) {
|
||||
$commandParts = ['mongod', '--config', '/etc/mongo/mongod.conf'];
|
||||
}
|
||||
|
||||
$sslConfig = match ($this->database->ssl_mode) {
|
||||
'allow' => [
|
||||
'--tlsMode=allowTLS',
|
||||
'--tlsAllowConnectionsWithoutCertificates',
|
||||
'--tlsAllowInvalidHostnames',
|
||||
],
|
||||
'prefer' => [
|
||||
'--tlsMode=preferTLS',
|
||||
'--tlsAllowConnectionsWithoutCertificates',
|
||||
'--tlsAllowInvalidHostnames',
|
||||
],
|
||||
'require' => [
|
||||
'--tlsMode=requireTLS',
|
||||
'--tlsAllowConnectionsWithoutCertificates',
|
||||
'--tlsAllowInvalidHostnames',
|
||||
],
|
||||
'verify-full' => [
|
||||
'--tlsMode=requireTLS',
|
||||
'--tlsAllowInvalidHostnames',
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
|
||||
$commandParts = [...$commandParts, ...$sslConfig];
|
||||
$commandParts[] = '--tlsCAFile';
|
||||
$commandParts[] = '/etc/mongo/certs/ca.pem';
|
||||
$commandParts[] = '--tlsCertificateKeyFile';
|
||||
$commandParts[] = '/etc/mongo/certs/server.pem';
|
||||
|
||||
$docker_compose['services'][$container_name]['command'] = $commandParts;
|
||||
}
|
||||
|
||||
$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";
|
||||
@@ -128,6 +261,9 @@ class StartMongodb
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = executeInDocker($this->database->uuid, 'chown mongodb:mongodb /etc/mongo/certs/server.pem');
|
||||
}
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMysql;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -16,6 +18,8 @@ class StartMysql
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneMysql $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
@@ -25,9 +29,64 @@ class StartMysql
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
"echo 'Directories created successfully.'",
|
||||
];
|
||||
|
||||
if (! $this->database->enable_ssl) {
|
||||
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||
|
||||
$this->database->sslCertificates()->delete();
|
||||
|
||||
$this->database->fileStorages()
|
||||
->where('resource_type', $this->database->getMorphClass())
|
||||
->where('resource_id', $this->database->id)
|
||||
->get()
|
||||
->filter(function ($storage) {
|
||||
return in_array($storage->mount_path, [
|
||||
'/etc/mysql/certs/server.crt',
|
||||
'/etc/mysql/certs/server.key',
|
||||
]);
|
||||
})
|
||||
->each(function ($storage) {
|
||||
$storage->delete();
|
||||
});
|
||||
} else {
|
||||
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||
|
||||
$server = $this->database->destination->server;
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $this->ssl_certificate) {
|
||||
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||
commonName: $this->database->uuid,
|
||||
resourceType: $this->database->getMorphClass(),
|
||||
resourceId: $this->database->id,
|
||||
serverId: $server->id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $this->configuration_dir,
|
||||
mountPath: '/etc/mysql/certs',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
@@ -67,39 +126,83 @@ class StartMysql
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
$persistent_storages
|
||||
);
|
||||
}
|
||||
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
$persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||
'target' => '/etc/mysql/certs/coolify-ca.crt',
|
||||
'read_only' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||
'read_only' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$docker_compose['services'][$container_name]['command'] = [
|
||||
'mysqld',
|
||||
'--ssl-cert=/etc/mysql/certs/server.crt',
|
||||
'--ssl-key=/etc/mysql/certs/server.key',
|
||||
'--ssl-ca=/etc/mysql/certs/coolify-ca.crt',
|
||||
'--require-secure-transport=1',
|
||||
];
|
||||
}
|
||||
|
||||
$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";
|
||||
@@ -108,6 +211,11 @@ class StartMysql
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->mysql_user}:{$this->database->mysql_user} /etc/mysql/certs/server.crt /etc/mysql/certs/server.key");
|
||||
}
|
||||
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -18,6 +20,8 @@ class StartPostgresql
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandalonePostgresql $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
@@ -29,10 +33,65 @@ class StartPostgresql
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
|
||||
"echo 'Directories created successfully.'",
|
||||
];
|
||||
|
||||
if (! $this->database->enable_ssl) {
|
||||
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||
|
||||
$this->database->sslCertificates()->delete();
|
||||
|
||||
$this->database->fileStorages()
|
||||
->where('resource_type', $this->database->getMorphClass())
|
||||
->where('resource_id', $this->database->id)
|
||||
->get()
|
||||
->filter(function ($storage) {
|
||||
return in_array($storage->mount_path, [
|
||||
'/var/lib/postgresql/certs/server.crt',
|
||||
'/var/lib/postgresql/certs/server.key',
|
||||
]);
|
||||
})
|
||||
->each(function ($storage) {
|
||||
$storage->delete();
|
||||
});
|
||||
} else {
|
||||
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||
|
||||
$server = $this->database->destination->server;
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $this->ssl_certificate) {
|
||||
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||
commonName: $this->database->uuid,
|
||||
resourceType: $this->database->getMorphClass(),
|
||||
resourceId: $this->database->id,
|
||||
serverId: $server->id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $this->configuration_dir,
|
||||
mountPath: '/var/lib/postgresql/certs',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
@@ -77,49 +136,84 @@ class StartPostgresql
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (filled($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
$persistent_storages
|
||||
);
|
||||
}
|
||||
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
$persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
|
||||
if (count($this->init_scripts) > 0) {
|
||||
foreach ($this->init_scripts as $init_script) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $init_script,
|
||||
'target' => '/docker-entrypoint-initdb.d/'.basename($init_script),
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
[[
|
||||
'type' => 'bind',
|
||||
'source' => $init_script,
|
||||
'target' => '/docker-entrypoint-initdb.d/'.basename($init_script),
|
||||
'read_only' => true,
|
||||
]]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (filled($this->database->postgres_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/custom-postgres.conf',
|
||||
'target' => '/etc/postgresql/postgresql.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
[[
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/custom-postgres.conf',
|
||||
'target' => '/etc/postgresql/postgresql.conf',
|
||||
'read_only' => true,
|
||||
]]
|
||||
);
|
||||
$docker_compose['services'][$container_name]['command'] = [
|
||||
'postgres',
|
||||
'-c',
|
||||
'config_file=/etc/postgresql/postgresql.conf',
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$docker_compose['services'][$container_name]['command'] = [
|
||||
'postgres',
|
||||
'-c',
|
||||
'ssl=on',
|
||||
'-c',
|
||||
'ssl_cert_file=/var/lib/postgresql/certs/server.crt',
|
||||
'-c',
|
||||
'ssl_key_file=/var/lib/postgresql/certs/server.key',
|
||||
];
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
@@ -132,6 +226,9 @@ class StartPostgresql
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->postgres_user}:{$this->database->postgres_user} /var/lib/postgresql/certs/server.key /var/lib/postgresql/certs/server.crt");
|
||||
}
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -17,6 +19,8 @@ class StartRedis
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneRedis $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
@@ -26,9 +30,62 @@ class StartRedis
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
"echo 'Directories created successfully.'",
|
||||
];
|
||||
|
||||
if (! $this->database->enable_ssl) {
|
||||
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||
$this->database->sslCertificates()->delete();
|
||||
$this->database->fileStorages()
|
||||
->where('resource_type', $this->database->getMorphClass())
|
||||
->where('resource_id', $this->database->id)
|
||||
->get()
|
||||
->filter(function ($storage) {
|
||||
return in_array($storage->mount_path, [
|
||||
'/etc/redis/certs/server.crt',
|
||||
'/etc/redis/certs/server.key',
|
||||
]);
|
||||
})
|
||||
->each(function ($storage) {
|
||||
$storage->delete();
|
||||
});
|
||||
} else {
|
||||
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||
|
||||
$server = $this->database->destination->server;
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $this->ssl_certificate) {
|
||||
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||
commonName: $this->database->uuid,
|
||||
resourceType: $this->database->getMorphClass(),
|
||||
resourceId: $this->database->id,
|
||||
serverId: $server->id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $this->configuration_dir,
|
||||
mountPath: '/etc/redis/certs',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
@@ -76,26 +133,55 @@ class StartRedis
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
$persistent_storages
|
||||
);
|
||||
}
|
||||
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
$persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||
'target' => '/etc/redis/certs/coolify-ca.crt',
|
||||
'read_only' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
@@ -116,6 +202,9 @@ class StartRedis
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||
}
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
@@ -202,6 +291,20 @@ class StartRedis
|
||||
$command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
}
|
||||
|
||||
if ($this->database->enable_ssl) {
|
||||
$sslArgs = [
|
||||
'--tls-port 6380',
|
||||
'--tls-cert-file /etc/redis/certs/server.crt',
|
||||
'--tls-key-file /etc/redis/certs/server.key',
|
||||
'--tls-ca-cert-file /etc/redis/certs/coolify-ca.crt',
|
||||
'--tls-auth-clients optional',
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($sslArgs)) {
|
||||
$command .= ' '.implode(' ', $sslArgs);
|
||||
}
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
@@ -11,7 +12,6 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopDatabase
|
||||
@@ -20,56 +20,37 @@ class StopDatabase
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
try {
|
||||
$server = $database->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
|
||||
$this->stopContainer($database, $database->uuid, 30);
|
||||
|
||||
$this->stopContainer($database, $database->uuid, 300);
|
||||
if (! $isDeleteOperation) {
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
|
||||
if ($database->is_public) {
|
||||
StopDatabaseProxy::run($database);
|
||||
}
|
||||
|
||||
return 'Database stopped successfully';
|
||||
} catch (\Exception $e) {
|
||||
return 'Database stop failed: '.$e->getMessage();
|
||||
} finally {
|
||||
ServiceStatusChanged::dispatch($database->environment->project->team->id);
|
||||
}
|
||||
|
||||
if ($database->is_public) {
|
||||
StopDatabaseProxy::run($database);
|
||||
}
|
||||
|
||||
return 'Database stopped successfully';
|
||||
}
|
||||
|
||||
private function stopContainer($database, string $containerName, int $timeout = 300): void
|
||||
private function stopContainer($database, string $containerName, int $timeout = 30): void
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
|
||||
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
|
||||
$startTime = time();
|
||||
while ($process->running()) {
|
||||
if (time() - $startTime >= $timeout) {
|
||||
$this->forceStopContainer($containerName, $server);
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
|
||||
private function forceStopContainer(string $containerName, $server): void
|
||||
{
|
||||
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
|
||||
}
|
||||
|
||||
private function removeContainer(string $containerName, $server): void
|
||||
{
|
||||
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
|
||||
}
|
||||
|
||||
private function deleteConnectedNetworks($uuid, $server)
|
||||
{
|
||||
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
||||
instant_remote_process(command: [
|
||||
"docker stop --time=$timeout $containerName",
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ class StopDatabaseProxy
|
||||
}
|
||||
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
|
||||
|
||||
$database->is_public = false;
|
||||
$database->save();
|
||||
|
||||
DatabaseProxyStopped::dispatch();
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Actions\Docker;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Shared\ComplexStatusCheck;
|
||||
use App\Events\ServiceChecked;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServiceDatabase;
|
||||
@@ -208,7 +209,6 @@ class GetContainersStatus
|
||||
$foundServices[] = "$service->id-$service->name";
|
||||
$statusFromDb = $service->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
// ray('Updating status: ' . $containerStatus);
|
||||
$service->update(['status' => $containerStatus]);
|
||||
} else {
|
||||
$service->update(['last_online_at' => now()]);
|
||||
@@ -274,24 +274,13 @@ class GetContainersStatus
|
||||
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;
|
||||
// Only protection: If no containers at all, Docker query might have failed
|
||||
if ($this->containers->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$application->update(['status' => 'exited']);
|
||||
}
|
||||
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
|
||||
foreach ($notRunningApplicationPreviews as $previewId) {
|
||||
@@ -299,24 +288,13 @@ class GetContainersStatus
|
||||
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;
|
||||
// Only protection: If no containers at all, Docker query might have failed
|
||||
if ($this->containers->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$preview->update(['status' => 'exited']);
|
||||
}
|
||||
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
|
||||
foreach ($notRunningDatabases as $database) {
|
||||
@@ -342,5 +320,6 @@ class GetContainersStatus
|
||||
}
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
ServiceChecked::dispatch($this->server->team->id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,5 +24,6 @@ class ResetUserPassword implements ResetsUserPasswords
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
$user->deleteAllSessions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Services\ProxyDashboardCacheService;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class CheckConfiguration
|
||||
@@ -28,6 +29,8 @@ class CheckConfiguration
|
||||
throw new \Exception('Could not generate proxy configuration');
|
||||
}
|
||||
|
||||
ProxyDashboardCacheService::isTraefikDashboardAvailableFromConfiguration($server, $proxy_configuration);
|
||||
|
||||
return $proxy_configuration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Actions\Proxy;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
@@ -27,13 +28,9 @@ class CheckProxy
|
||||
return false;
|
||||
}
|
||||
$proxyType = $server->proxyType();
|
||||
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
|
||||
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) {
|
||||
return false;
|
||||
}
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
|
||||
if (! $uptime) {
|
||||
throw new \Exception($error);
|
||||
}
|
||||
if (! $server->isProxyShouldRun()) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception('Proxy should not run. You selected the Custom Proxy.');
|
||||
@@ -41,8 +38,12 @@ class CheckProxy
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine proxy container name based on environment
|
||||
$proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
|
||||
if ($server->isSwarm()) {
|
||||
$status = getContainerStatus($server, 'coolify-proxy_traefik');
|
||||
$status = getContainerStatus($server, $proxyContainerName);
|
||||
$server->proxy->set('status', $status);
|
||||
$server->save();
|
||||
if ($status === 'running') {
|
||||
@@ -51,7 +52,7 @@ class CheckProxy
|
||||
|
||||
return true;
|
||||
} else {
|
||||
$status = getContainerStatus($server, 'coolify-proxy');
|
||||
$status = getContainerStatus($server, $proxyContainerName);
|
||||
if ($status === 'running') {
|
||||
$server->proxy->set('status', 'running');
|
||||
$server->save();
|
||||
@@ -65,7 +66,6 @@ class CheckProxy
|
||||
if ($server->id === 0) {
|
||||
$ip = 'host.docker.internal';
|
||||
}
|
||||
|
||||
$portsToCheck = ['80', '443'];
|
||||
|
||||
try {
|
||||
@@ -73,7 +73,7 @@ class CheckProxy
|
||||
$proxyCompose = CheckConfiguration::run($server);
|
||||
if (isset($proxyCompose)) {
|
||||
$yaml = Yaml::parse($proxyCompose);
|
||||
$portsToCheck = [];
|
||||
$configPorts = [];
|
||||
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
|
||||
$ports = data_get($yaml, 'services.traefik.ports');
|
||||
} elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
|
||||
@@ -81,9 +81,11 @@ class CheckProxy
|
||||
}
|
||||
if (isset($ports)) {
|
||||
foreach ($ports as $port) {
|
||||
$portsToCheck[] = str($port)->before(':')->value();
|
||||
$configPorts[] = str($port)->before(':')->value();
|
||||
}
|
||||
}
|
||||
// Combine default ports with config ports
|
||||
$portsToCheck = array_merge($portsToCheck, $configPorts);
|
||||
}
|
||||
} else {
|
||||
$portsToCheck = [];
|
||||
@@ -94,11 +96,13 @@ class CheckProxy
|
||||
if (count($portsToCheck) === 0) {
|
||||
return false;
|
||||
}
|
||||
foreach ($portsToCheck as $port) {
|
||||
$connection = @fsockopen($ip, $port);
|
||||
if (is_resource($connection) && fclose($connection)) {
|
||||
$portsToCheck = array_values(array_unique($portsToCheck));
|
||||
// Check port conflicts in parallel
|
||||
$conflicts = $this->checkPortConflictsInParallel($server, $portsToCheck, $proxyContainerName);
|
||||
foreach ($conflicts as $port => $conflict) {
|
||||
if ($conflict) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
|
||||
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br><br>Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a>");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -108,4 +112,306 @@ class CheckProxy
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check multiple ports for conflicts in parallel
|
||||
* Returns an array with port => conflict_status mapping
|
||||
*/
|
||||
private function checkPortConflictsInParallel(Server $server, array $ports, string $proxyContainerName): array
|
||||
{
|
||||
if (empty($ports)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Build concurrent port check commands
|
||||
$results = Process::concurrently(function ($pool) use ($server, $ports, $proxyContainerName) {
|
||||
foreach ($ports as $port) {
|
||||
$commands = $this->buildPortCheckCommands($server, $port, $proxyContainerName);
|
||||
$pool->command($commands['ssh_command'])->timeout(10);
|
||||
}
|
||||
});
|
||||
|
||||
// Process results
|
||||
$conflicts = [];
|
||||
|
||||
foreach ($ports as $index => $port) {
|
||||
$result = $results[$index] ?? null;
|
||||
|
||||
if ($result) {
|
||||
$conflicts[$port] = $this->parsePortCheckResult($result, $port, $proxyContainerName);
|
||||
} else {
|
||||
// If process failed, assume no conflict to avoid false positives
|
||||
$conflicts[$port] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $conflicts;
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Parallel port checking failed: '.$e->getMessage().'. Falling back to sequential checking.');
|
||||
|
||||
// Fallback to sequential checking if parallel fails
|
||||
$conflicts = [];
|
||||
foreach ($ports as $port) {
|
||||
$conflicts[$port] = $this->isPortConflict($server, $port, $proxyContainerName);
|
||||
}
|
||||
|
||||
return $conflicts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the SSH command for checking a specific port
|
||||
*/
|
||||
private function buildPortCheckCommands(Server $server, string $port, string $proxyContainerName): array
|
||||
{
|
||||
// First check if our own proxy is using this port (which is fine)
|
||||
$getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
|
||||
$checkProxyPortScript = "
|
||||
CONTAINER_ID=\$($getProxyContainerId);
|
||||
if [ ! -z \"\$CONTAINER_ID\" ]; then
|
||||
if docker inspect \$CONTAINER_ID --format '{{json .NetworkSettings.Ports}}' | grep -q '\"$port/tcp\"'; then
|
||||
echo 'proxy_using_port';
|
||||
exit 0;
|
||||
fi;
|
||||
fi;
|
||||
";
|
||||
|
||||
// Command sets for different ways to check ports, ordered by preference
|
||||
$portCheckScript = "
|
||||
$checkProxyPortScript
|
||||
|
||||
# Try ss command first
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null);
|
||||
if [ -z \"\$ss_output\" ]; then
|
||||
echo 'port_free';
|
||||
exit 0;
|
||||
fi;
|
||||
count=\$(echo \"\$ss_output\" | grep -c ':$port ');
|
||||
if [ \$count -eq 0 ]; then
|
||||
echo 'port_free';
|
||||
exit 0;
|
||||
fi;
|
||||
# Check for dual-stack or docker processes
|
||||
if [ \$count -le 2 ] && (echo \"\$ss_output\" | grep -q 'docker\\|coolify'); then
|
||||
echo 'port_free';
|
||||
exit 0;
|
||||
fi;
|
||||
echo \"port_conflict|\$ss_output\";
|
||||
exit 0;
|
||||
fi;
|
||||
|
||||
# Try netstat as fallback
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
netstat_output=\$(netstat -tuln 2>/dev/null | grep ':$port ');
|
||||
if [ -z \"\$netstat_output\" ]; then
|
||||
echo 'port_free';
|
||||
exit 0;
|
||||
fi;
|
||||
count=\$(echo \"\$netstat_output\" | grep -c 'LISTEN');
|
||||
if [ \$count -eq 0 ]; then
|
||||
echo 'port_free';
|
||||
exit 0;
|
||||
fi;
|
||||
if [ \$count -le 2 ] && (echo \"\$netstat_output\" | grep -q 'docker\\|coolify'); then
|
||||
echo 'port_free';
|
||||
exit 0;
|
||||
fi;
|
||||
echo \"port_conflict|\$netstat_output\";
|
||||
exit 0;
|
||||
fi;
|
||||
|
||||
# Final fallback using nc
|
||||
if nc -z -w1 127.0.0.1 $port >/dev/null 2>&1; then
|
||||
echo 'port_conflict|nc_detected';
|
||||
else
|
||||
echo 'port_free';
|
||||
fi;
|
||||
";
|
||||
|
||||
$sshCommand = \App\Helpers\SshMultiplexingHelper::generateSshCommand($server, $portCheckScript);
|
||||
|
||||
return [
|
||||
'ssh_command' => $sshCommand,
|
||||
'script' => $portCheckScript,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the result from port check command
|
||||
*/
|
||||
private function parsePortCheckResult($processResult, string $port, string $proxyContainerName): bool
|
||||
{
|
||||
$exitCode = $processResult->exitCode();
|
||||
$output = trim($processResult->output());
|
||||
$errorOutput = trim($processResult->errorOutput());
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($output === 'proxy_using_port' || $output === 'port_free') {
|
||||
return false; // No conflict
|
||||
}
|
||||
|
||||
if (str_starts_with($output, 'port_conflict|')) {
|
||||
$details = substr($output, strlen('port_conflict|'));
|
||||
|
||||
// Additional logic to detect dual-stack scenarios
|
||||
if ($details !== 'nc_detected') {
|
||||
// Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6)
|
||||
$lines = explode("\n", $details);
|
||||
if (count($lines) <= 2) {
|
||||
// Look for IPv4 and IPv6 in the listing
|
||||
if ((strpos($details, '0.0.0.0:'.$port) !== false && strpos($details, ':::'.$port) !== false) ||
|
||||
(strpos($details, '*:'.$port) !== false && preg_match('/\*:'.$port.'.*IPv[46]/', $details))) {
|
||||
|
||||
return false; // This is just a normal dual-stack setup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Real port conflict
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart port checker that handles dual-stack configurations
|
||||
* Returns true only if there's a real port conflict (not just dual-stack)
|
||||
*/
|
||||
private function isPortConflict(Server $server, string $port, string $proxyContainerName): bool
|
||||
{
|
||||
// First check if our own proxy is using this port (which is fine)
|
||||
try {
|
||||
$getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
|
||||
$containerId = trim(instant_remote_process([$getProxyContainerId], $server));
|
||||
|
||||
if (! empty($containerId)) {
|
||||
$checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'";
|
||||
try {
|
||||
instant_remote_process([$checkProxyPort], $server);
|
||||
|
||||
// Our proxy is using the port, which is fine
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
// Our container exists but not using this port
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Container not found or error checking, continue with regular checks
|
||||
}
|
||||
|
||||
// Command sets for different ways to check ports, ordered by preference
|
||||
$commandSets = [
|
||||
// Set 1: Use ss to check listener counts by protocol stack
|
||||
[
|
||||
'available' => 'command -v ss >/dev/null 2>&1',
|
||||
'check' => [
|
||||
// Get listening process details
|
||||
"ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"",
|
||||
// Count IPv4 listeners
|
||||
"echo \"\$ss_output\" | grep -c ':$port '",
|
||||
],
|
||||
],
|
||||
// Set 2: Use netstat as alternative to ss
|
||||
[
|
||||
'available' => 'command -v netstat >/dev/null 2>&1',
|
||||
'check' => [
|
||||
// Get listening process details
|
||||
"netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '",
|
||||
// Count listeners
|
||||
"echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'",
|
||||
],
|
||||
],
|
||||
// Set 3: Use lsof as last resort
|
||||
[
|
||||
'available' => 'command -v lsof >/dev/null 2>&1',
|
||||
'check' => [
|
||||
// Get process using the port
|
||||
"lsof -i :$port -P -n | grep 'LISTEN'",
|
||||
// Count listeners
|
||||
"lsof -i :$port -P -n | grep 'LISTEN' | wc -l",
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Try each command set until we find one available
|
||||
foreach ($commandSets as $set) {
|
||||
try {
|
||||
// Check if the command is available
|
||||
instant_remote_process([$set['available']], $server);
|
||||
|
||||
// Run the actual check commands
|
||||
$output = instant_remote_process($set['check'], $server, true);
|
||||
// Parse the output lines
|
||||
$lines = explode("\n", trim($output));
|
||||
// Get the detailed output and listener count
|
||||
$details = trim(implode("\n", array_slice($lines, 0, -1)));
|
||||
$count = intval(trim($lines[count($lines) - 1] ?? '0'));
|
||||
// If no listeners or empty result, port is free
|
||||
if ($count == 0 || empty($details)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to detect if this is our coolify-proxy
|
||||
if (strpos($details, 'docker') !== false || strpos($details, $proxyContainerName) !== false) {
|
||||
// It's likely our docker or proxy, which is fine
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6)
|
||||
// If exactly 2 listeners and both have same port, likely dual-stack
|
||||
if ($count <= 2) {
|
||||
// Check if it looks like a standard dual-stack setup
|
||||
$isDualStack = false;
|
||||
|
||||
// Look for IPv4 and IPv6 in the listing (ss output format)
|
||||
if (preg_match('/LISTEN.*:'.$port.'\s/', $details) &&
|
||||
(preg_match('/\*:'.$port.'\s/', $details) ||
|
||||
preg_match('/:::'.$port.'\s/', $details))) {
|
||||
$isDualStack = true;
|
||||
}
|
||||
|
||||
// For netstat format
|
||||
if (strpos($details, '0.0.0.0:'.$port) !== false &&
|
||||
strpos($details, ':::'.$port) !== false) {
|
||||
$isDualStack = true;
|
||||
}
|
||||
|
||||
// For lsof format (IPv4 and IPv6)
|
||||
if (strpos($details, '*:'.$port) !== false &&
|
||||
preg_match('/\*:'.$port.'.*IPv4/', $details) &&
|
||||
preg_match('/\*:'.$port.'.*IPv6/', $details)) {
|
||||
$isDualStack = true;
|
||||
}
|
||||
|
||||
if ($isDualStack) {
|
||||
return false; // This is just a normal dual-stack setup
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, it's likely a real port conflict
|
||||
return true;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// This command set failed, try the next one
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to simpler check if all above methods fail
|
||||
try {
|
||||
// Just try to bind to the port directly to see if it's available
|
||||
$checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'";
|
||||
$result = instant_remote_process([$checkCommand], $server, true);
|
||||
|
||||
return trim($result) === 'in-use';
|
||||
} catch (\Throwable $e) {
|
||||
// If everything fails, assume the port is free to avoid false positives
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStarted;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
@@ -28,6 +29,7 @@ class StartProxy
|
||||
$docker_compose_yml_base64 = base64_encode($configuration);
|
||||
$server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
|
||||
$server->save();
|
||||
|
||||
if ($server->isSwarmManager()) {
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
@@ -57,20 +59,22 @@ class StartProxy
|
||||
" echo 'Successfully stopped and removed existing coolify-proxy.'",
|
||||
'fi',
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d --remove-orphans',
|
||||
'docker compose up -d --wait --remove-orphans',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
$commands = $commands->merge(connectProxyToNetworks($server));
|
||||
}
|
||||
$server->proxy->set('status', 'starting');
|
||||
$server->save();
|
||||
ProxyStatusChangedUI::dispatch($server->team_id);
|
||||
|
||||
if ($async) {
|
||||
return remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server);
|
||||
return remote_process($commands, $server, callEventOnFinish: 'ProxyStatusChanged', callEventData: $server->id);
|
||||
} else {
|
||||
instant_remote_process($commands, $server);
|
||||
$server->proxy->set('status', 'running');
|
||||
$server->proxy->set('type', $proxyType);
|
||||
$server->save();
|
||||
ProxyStarted::dispatch($server);
|
||||
ProxyStatusChanged::dispatch($server->id);
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
38
app/Actions/Proxy/StopProxy.php
Normal file
38
app/Actions/Proxy/StopProxy.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
use App\Services\ProxyDashboardCacheService;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, bool $forceStop = true, int $timeout = 30)
|
||||
{
|
||||
try {
|
||||
$containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
$server->proxy->status = 'stopping';
|
||||
$server->save();
|
||||
ProxyStatusChangedUI::dispatch($server->team_id);
|
||||
|
||||
instant_remote_process(command: [
|
||||
"docker stop --time=$timeout $containerName",
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
|
||||
$server->proxy->force_stop = $forceStop;
|
||||
$server->proxy->status = 'exited';
|
||||
$server->save();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
} finally {
|
||||
ProxyDashboardCacheService::clearCache($server);
|
||||
ProxyStatusChanged::dispatch($server->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
223
app/Actions/Server/CheckUpdates.php
Normal file
223
app/Actions/Server/CheckUpdates.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class CheckUpdates
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
try {
|
||||
if ($server->serverStatus() === false) {
|
||||
return [
|
||||
'error' => 'Server is not reachable or not ready.',
|
||||
];
|
||||
}
|
||||
|
||||
// Try first method - using instant_remote_process
|
||||
$output = instant_remote_process(['cat /etc/os-release'], $server);
|
||||
|
||||
// Parse os-release into an associative array
|
||||
$osInfo = [];
|
||||
foreach (explode("\n", $output) as $line) {
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
if (strpos($line, '=') === false) {
|
||||
continue;
|
||||
}
|
||||
[$key, $value] = explode('=', $line, 2);
|
||||
$osInfo[$key] = trim($value, '"');
|
||||
}
|
||||
|
||||
// Get the main OS identifier
|
||||
$osId = $osInfo['ID'] ?? '';
|
||||
// $osIdLike = $osInfo['ID_LIKE'] ?? '';
|
||||
// $versionId = $osInfo['VERSION_ID'] ?? '';
|
||||
|
||||
// Normalize OS types based on install.sh logic
|
||||
switch ($osId) {
|
||||
case 'manjaro':
|
||||
case 'manjaro-arm':
|
||||
case 'endeavouros':
|
||||
$osType = 'arch';
|
||||
break;
|
||||
case 'pop':
|
||||
case 'linuxmint':
|
||||
case 'zorin':
|
||||
$osType = 'ubuntu';
|
||||
break;
|
||||
case 'fedora-asahi-remix':
|
||||
$osType = 'fedora';
|
||||
break;
|
||||
default:
|
||||
$osType = $osId;
|
||||
}
|
||||
|
||||
// Determine package manager based on OS type
|
||||
$packageManager = match ($osType) {
|
||||
'arch' => 'pacman',
|
||||
'alpine' => 'apk',
|
||||
'ubuntu', 'debian', 'raspbian' => 'apt',
|
||||
'centos', 'fedora', 'rhel', 'ol', 'rocky', 'almalinux', 'amzn' => 'dnf',
|
||||
'sles', 'opensuse-leap', 'opensuse-tumbleweed' => 'zypper',
|
||||
default => null
|
||||
};
|
||||
|
||||
switch ($packageManager) {
|
||||
case 'zypper':
|
||||
$output = instant_remote_process(['LANG=C zypper -tx list-updates'], $server);
|
||||
$out = $this->parseZypperOutput($output);
|
||||
$out['osId'] = $osId;
|
||||
$out['package_manager'] = $packageManager;
|
||||
|
||||
return $out;
|
||||
case 'dnf':
|
||||
$output = instant_remote_process(['LANG=C dnf list -q --updates --refresh'], $server);
|
||||
$out = $this->parseDnfOutput($output);
|
||||
$out['osId'] = $osId;
|
||||
$out['package_manager'] = $packageManager;
|
||||
|
||||
return $out;
|
||||
case 'apt':
|
||||
instant_remote_process(['apt-get update -qq'], $server);
|
||||
$output = instant_remote_process(['LANG=C apt list --upgradable 2>/dev/null'], $server);
|
||||
|
||||
$out = $this->parseAptOutput($output);
|
||||
$out['osId'] = $osId;
|
||||
$out['package_manager'] = $packageManager;
|
||||
|
||||
return $out;
|
||||
default:
|
||||
return [
|
||||
'osId' => $osId,
|
||||
'error' => 'Unsupported package manager',
|
||||
'package_manager' => $packageManager,
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray('Error:', $e->getMessage());
|
||||
|
||||
return [
|
||||
'osId' => $osId,
|
||||
'package_manager' => $packageManager,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function parseZypperOutput(string $output): array
|
||||
{
|
||||
$updates = [];
|
||||
|
||||
try {
|
||||
$xml = simplexml_load_string($output);
|
||||
if ($xml === false) {
|
||||
return [
|
||||
'total_updates' => 0,
|
||||
'updates' => [],
|
||||
'error' => 'Failed to parse XML output',
|
||||
];
|
||||
}
|
||||
|
||||
// Navigate to the update-list node
|
||||
$updateList = $xml->xpath('//update-list/update');
|
||||
|
||||
foreach ($updateList as $update) {
|
||||
$updates[] = [
|
||||
'package' => (string) $update['name'],
|
||||
'new_version' => (string) $update['edition'],
|
||||
'current_version' => (string) $update['edition-old'],
|
||||
'architecture' => (string) $update['arch'],
|
||||
'repository' => (string) $update->source['alias'],
|
||||
'summary' => (string) $update->summary,
|
||||
'description' => (string) $update->description,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'total_updates' => count($updates),
|
||||
'updates' => $updates,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'total_updates' => 0,
|
||||
'updates' => [],
|
||||
'error' => 'Error parsing zypper output: '.$e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function parseDnfOutput(string $output): array
|
||||
{
|
||||
$updates = [];
|
||||
$lines = explode("\n", $output);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split by multiple spaces/tabs and filter out empty elements
|
||||
$parts = array_values(array_filter(preg_split('/\s+/', $line)));
|
||||
|
||||
if (count($parts) >= 3) {
|
||||
$package = $parts[0];
|
||||
$new_version = $parts[1];
|
||||
$repository = $parts[2];
|
||||
|
||||
// Extract architecture from package name (e.g., "cloud-init.noarch" -> "noarch")
|
||||
$architecture = str_contains($package, '.') ? explode('.', $package)[1] : 'noarch';
|
||||
|
||||
$updates[] = [
|
||||
'package' => $package,
|
||||
'new_version' => $new_version,
|
||||
'repository' => $repository,
|
||||
'architecture' => $architecture,
|
||||
'current_version' => 'unknown', // DNF doesn't show current version in check-update output
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_updates' => count($updates),
|
||||
'updates' => $updates,
|
||||
];
|
||||
}
|
||||
|
||||
private function parseAptOutput(string $output): array
|
||||
{
|
||||
$updates = [];
|
||||
$lines = explode("\n", $output);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Skip the "Listing... Done" line and empty lines
|
||||
if (empty($line) || str_contains($line, 'Listing...')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Example line: package/stable 2.0-1 amd64 [upgradable from: 1.0-1]
|
||||
if (preg_match('/^(.+?)\/(\S+)\s+(\S+)\s+(\S+)\s+\[upgradable from: (.+?)\]/', $line, $matches)) {
|
||||
$updates[] = [
|
||||
'package' => $matches[1],
|
||||
'repository' => $matches[2],
|
||||
'new_version' => $matches[3],
|
||||
'architecture' => $matches[4],
|
||||
'current_version' => $matches[5],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_updates' => count($updates),
|
||||
'updates' => $updates,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,26 @@ class CleanupDocker
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
$realtimeImage = config('constants.coolify.realtime_image');
|
||||
$realtimeImageVersion = config('constants.coolify.realtime_version');
|
||||
$realtimeImageWithVersion = "$realtimeImage:$realtimeImageVersion";
|
||||
$realtimeImageWithoutPrefix = 'coollabsio/coolify-realtime';
|
||||
$realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion";
|
||||
|
||||
$helperImageVersion = data_get($settings, 'helper_version');
|
||||
$helperImage = config('constants.coolify.helper_image');
|
||||
$helperImageWithVersion = "$helperImage:$helperImageVersion";
|
||||
$helperImageWithoutPrefix = 'coollabsio/coolify-helper';
|
||||
$helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion";
|
||||
|
||||
$commands = [
|
||||
'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"',
|
||||
'docker image prune -af --filter "label!=coolify.managed=true"',
|
||||
'docker builder prune -af',
|
||||
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
|
||||
"docker images --filter before=$realtimeImageWithVersion --filter reference=$realtimeImage | grep $realtimeImage | awk '{print $3}' | xargs -r docker rmi -f",
|
||||
"docker images --filter before=$helperImageWithoutPrefixVersion --filter reference=$helperImageWithoutPrefix | grep $helperImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f",
|
||||
"docker images --filter before=$realtimeImageWithoutPrefixVersion --filter reference=$realtimeImageWithoutPrefix | grep $realtimeImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f",
|
||||
];
|
||||
|
||||
if ($server->settings->delete_unused_volumes) {
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Events\CloudflareTunnelConfigured;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ConfigureCloudflared
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, string $cloudflare_token)
|
||||
public function handle(Server $server, string $cloudflare_token, string $ssh_domain): Activity
|
||||
{
|
||||
try {
|
||||
$config = [
|
||||
@@ -24,6 +24,13 @@ class ConfigureCloudflared
|
||||
'command' => 'tunnel run',
|
||||
'environment' => [
|
||||
"TUNNEL_TOKEN={$cloudflare_token}",
|
||||
'TUNNEL_METRICS=127.0.0.1:60123',
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'cloudflared', 'tunnel', '--metrics', '127.0.0.1:60123', 'ready'],
|
||||
'interval' => '5s',
|
||||
'timeout' => '30s',
|
||||
'retries' => 5,
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -34,22 +41,20 @@ class ConfigureCloudflared
|
||||
'mkdir -p /tmp/cloudflared',
|
||||
'cd /tmp/cloudflared',
|
||||
"echo '$docker_compose_yml_base64' | base64 -d | tee docker-compose.yml > /dev/null",
|
||||
'echo Pulling latest Cloudflare Tunnel image.',
|
||||
'docker compose pull',
|
||||
'docker compose down -v --remove-orphans > /dev/null 2>&1',
|
||||
'docker compose up -d --remove-orphans',
|
||||
'echo Stopping existing Cloudflare Tunnel container.',
|
||||
'docker rm -f coolify-cloudflared || true',
|
||||
'echo Starting new Cloudflare Tunnel container.',
|
||||
'docker compose up --wait --wait-timeout 15 --remove-orphans || docker logs coolify-cloudflared',
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
} catch (\Throwable $e) {
|
||||
$server->settings->is_cloudflare_tunnel = false;
|
||||
$server->settings->save();
|
||||
throw $e;
|
||||
} finally {
|
||||
CloudflareTunnelConfigured::dispatch($server->team_id);
|
||||
|
||||
$commands = collect([
|
||||
'rm -fr /tmp/cloudflared',
|
||||
return remote_process($commands, $server, callEventOnFinish: 'CloudflareTunnelChanged', callEventData: [
|
||||
'server_id' => $server->id,
|
||||
'ssh_domain' => $ssh_domain,
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
} catch (\Throwable $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -17,6 +19,27 @@ class InstallDocker
|
||||
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>.');
|
||||
}
|
||||
|
||||
if (! SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->exists()) {
|
||||
$serverCert = SslHelper::generateSslCertificate(
|
||||
commonName: 'Coolify CA Certificate',
|
||||
serverId: $server->id,
|
||||
isCaCertificate: true,
|
||||
validityDays: 10 * 365
|
||||
);
|
||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||
|
||||
$commands = collect([
|
||||
"mkdir -p $caCertPath",
|
||||
"chown -R 9999:root $caCertPath",
|
||||
"chmod -R 700 $caCertPath",
|
||||
"rm -rf $caCertPath/coolify-ca.crt",
|
||||
"echo '{$serverCert->ssl_certificate}' > $caCertPath/coolify-ca.crt",
|
||||
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||
]);
|
||||
remote_process($commands, $server);
|
||||
}
|
||||
|
||||
$config = base64_encode('{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
|
||||
@@ -99,11 +99,12 @@ class ServerCheck
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
$proxyStatus = data_get($foundProxyContainer, 'State.Status', 'exited');
|
||||
if (! $foundProxyContainer || $proxyStatus !== 'running') {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
StartProxy::run($this->server, async: false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -15,19 +15,18 @@ class StartLogDrain
|
||||
{
|
||||
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';
|
||||
}
|
||||
if ($type !== 'none') {
|
||||
StopLogDrain::run($server);
|
||||
}
|
||||
try {
|
||||
if ($type === 'none') {
|
||||
return 'No log drain is enabled.';
|
||||
@@ -186,7 +185,6 @@ Files:
|
||||
"echo '{$compose}' | base64 -d | tee $compose_path > /dev/null",
|
||||
"echo '{$readme}' | base64 -d | tee $readme_path > /dev/null",
|
||||
"test -f $config_path/.env && rm $config_path/.env",
|
||||
|
||||
];
|
||||
if ($type === 'newrelic') {
|
||||
$add_envs_command = [
|
||||
|
||||
@@ -25,9 +25,9 @@ class StartSentinel
|
||||
$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";
|
||||
$image = config('constants.coolify.registry_url').'/coollabsio/sentinel:'.$version;
|
||||
if (! $endpoint) {
|
||||
throw new \Exception('You should set FQDN in Instance Settings.');
|
||||
throw new \RuntimeException('You should set FQDN in Instance Settings.');
|
||||
}
|
||||
$environments = [
|
||||
'TOKEN' => $token,
|
||||
|
||||
@@ -52,7 +52,8 @@ class UpdateCoolify
|
||||
{
|
||||
PullHelperImageJob::dispatch($this->server);
|
||||
|
||||
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
|
||||
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
|
||||
instant_remote_process(["docker pull -q $image"], $this->server, false);
|
||||
|
||||
remote_process([
|
||||
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
|
||||
|
||||
52
app/Actions/Server/UpdatePackage.php
Normal file
52
app/Actions/Server/UpdatePackage.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Spatie\Activitylog\Contracts\Activity;
|
||||
|
||||
class UpdatePackage
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Server $server, string $osId, ?string $package = null, ?string $packageManager = null, bool $all = false): Activity|array
|
||||
{
|
||||
try {
|
||||
if ($server->serverStatus() === false) {
|
||||
return [
|
||||
'error' => 'Server is not reachable or not ready.',
|
||||
];
|
||||
}
|
||||
switch ($packageManager) {
|
||||
case 'zypper':
|
||||
$commandAll = 'zypper update -y';
|
||||
$commandInstall = 'zypper install -y '.$package;
|
||||
break;
|
||||
case 'dnf':
|
||||
$commandAll = 'dnf update -y';
|
||||
$commandInstall = 'dnf update -y '.$package;
|
||||
break;
|
||||
case 'apt':
|
||||
$commandAll = 'apt update && apt upgrade -y';
|
||||
$commandInstall = 'apt install -y '.$package;
|
||||
break;
|
||||
default:
|
||||
return [
|
||||
'error' => 'OS not supported',
|
||||
];
|
||||
}
|
||||
if ($all) {
|
||||
return remote_process([$commandAll], $server);
|
||||
}
|
||||
|
||||
return remote_process([$commandInstall], $server);
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,15 +48,15 @@ class DeleteService
|
||||
}
|
||||
|
||||
if ($deleteConnectedNetworks) {
|
||||
$service->delete_connected_networks($service->uuid);
|
||||
$service->deleteConnectedNetworks();
|
||||
}
|
||||
|
||||
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
throw new \RuntimeException($e->getMessage());
|
||||
} finally {
|
||||
if ($deleteConfigurations) {
|
||||
$service->delete_configurations();
|
||||
$service->deleteConfigurations();
|
||||
}
|
||||
foreach ($service->applications()->get() as $application) {
|
||||
$application->forceDelete();
|
||||
|
||||
@@ -11,10 +11,10 @@ class RestartService
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Service $service)
|
||||
public function handle(Service $service, bool $pullLatestImages)
|
||||
{
|
||||
StopService::run($service);
|
||||
|
||||
return StartService::run($service);
|
||||
return StartService::run($service, $pullLatestImages);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,19 +12,25 @@ class StartService
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Service $service)
|
||||
public function handle(Service $service, bool $pullLatestImages = false, bool $stopBeforeStart = false)
|
||||
{
|
||||
$service->parse();
|
||||
if ($stopBeforeStart) {
|
||||
StopService::run(service: $service, dockerCleanup: false);
|
||||
}
|
||||
$service->saveComposeConfigs();
|
||||
$service->isConfigurationChanged(save: true);
|
||||
$commands[] = 'cd '.$service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
if ($pullLatestImages) {
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = 'docker compose pull';
|
||||
}
|
||||
if ($service->networks()->count() > 0) {
|
||||
$commands[] = "echo 'Creating Docker network.'";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
||||
}
|
||||
$commands[] = 'echo Starting service.';
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = 'docker compose pull';
|
||||
$commands[] = "echo 'Starting containers.'";
|
||||
$commands[] = 'docker compose up -d --remove-orphans --force-recreate --build';
|
||||
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
|
||||
if (data_get($service, 'connect_to_docker_network')) {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -20,17 +22,44 @@ class StopService
|
||||
return 'Server is not functional';
|
||||
}
|
||||
|
||||
$containersToStop = $service->getContainersToStop();
|
||||
$service->stopContainers($containersToStop, $server);
|
||||
$containersToStop = [];
|
||||
$applications = $service->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
$containersToStop[] = "{$application->name}-{$service->uuid}";
|
||||
}
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($dbs as $db) {
|
||||
$containersToStop[] = "{$db->name}-{$service->uuid}";
|
||||
}
|
||||
|
||||
if (! $isDeleteOperation) {
|
||||
$service->delete_connected_networks($service->uuid);
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
if (! empty($containersToStop)) {
|
||||
$this->stopContainersInParallel($containersToStop, $server);
|
||||
}
|
||||
|
||||
if ($isDeleteOperation) {
|
||||
$service->deleteConnectedNetworks();
|
||||
}
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $e->getMessage();
|
||||
} finally {
|
||||
ServiceStatusChanged::dispatch($service->environment->project->team->id);
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainersInParallel(array $containersToStop, Server $server): void
|
||||
{
|
||||
$timeout = count($containersToStop) > 5 ? 10 : 30;
|
||||
$commands = [];
|
||||
$containerList = implode(' ', $containersToStop);
|
||||
$commands[] = "docker stop --time=$timeout $containerList";
|
||||
$commands[] = "docker rm -f $containerList";
|
||||
instant_remote_process(
|
||||
command: $commands,
|
||||
server: $server,
|
||||
throwError: false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Shared;
|
||||
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class PullImage
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $resource)
|
||||
{
|
||||
$resource->saveComposeConfigs();
|
||||
|
||||
$commands[] = 'cd '.$resource->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$resource->workdir()}.'";
|
||||
$commands[] = 'docker compose pull';
|
||||
|
||||
$server = data_get($resource, 'server');
|
||||
|
||||
if (! $server) {
|
||||
return;
|
||||
}
|
||||
|
||||
instant_remote_process($commands, $resource->server);
|
||||
}
|
||||
}
|
||||
@@ -13,17 +13,20 @@ class CleanupRedis extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$prefix = config('database.redis.options.prefix');
|
||||
|
||||
$keys = Redis::connection()->keys('*:laravel*');
|
||||
collect($keys)->each(function ($key) use ($prefix) {
|
||||
$redis = Redis::connection('horizon');
|
||||
$keys = $redis->keys('*');
|
||||
$prefix = config('horizon.prefix');
|
||||
foreach ($keys as $key) {
|
||||
$keyWithoutPrefix = str_replace($prefix, '', $key);
|
||||
Redis::connection()->del($keyWithoutPrefix);
|
||||
});
|
||||
$type = $redis->command('type', [$keyWithoutPrefix]);
|
||||
|
||||
$queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
|
||||
collect($queueOverlaps)->each(function ($key) {
|
||||
Redis::connection()->del($key);
|
||||
});
|
||||
if ($type === 5) {
|
||||
$data = $redis->command('hgetall', [$keyWithoutPrefix]);
|
||||
$status = data_get($data, 'status');
|
||||
if ($status === 'completed') {
|
||||
$redis->command('del', [$keyWithoutPrefix]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanupStuckedResources extends Command
|
||||
@@ -36,6 +37,12 @@ class CleanupStuckedResources extends Command
|
||||
private function cleanup_stucked_resources()
|
||||
{
|
||||
try {
|
||||
$teams = Team::all()->filter(function ($team) {
|
||||
return $team->members()->count() === 0 && $team->servers()->count() === 0;
|
||||
});
|
||||
foreach ($teams as $team) {
|
||||
$team->delete();
|
||||
}
|
||||
$servers = Server::all()->filter(function ($server) {
|
||||
return $server->isFunctional();
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ class CloudCleanupSubscriptions extends Command
|
||||
} else {
|
||||
$subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []);
|
||||
$status = data_get($subscription, 'status');
|
||||
if ($status === 'active' || $status === 'past_due') {
|
||||
if ($status === 'active') {
|
||||
$team->subscription->update([
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_trial_already_ended' => false,
|
||||
|
||||
@@ -5,12 +5,10 @@ namespace App\Console\Commands;
|
||||
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
|
||||
{
|
||||
protected $signature = 'dev {--init} {--generate-openapi}';
|
||||
protected $signature = 'dev {--init}';
|
||||
|
||||
protected $description = 'Helper commands for development.';
|
||||
|
||||
@@ -21,36 +19,6 @@ class Dev extends Command
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->option('generate-openapi')) {
|
||||
$this->generateOpenApi();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function generateOpenApi()
|
||||
{
|
||||
// Generate OpenAPI documentation
|
||||
echo "Generating OpenAPI documentation.\n";
|
||||
// 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()
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace App\Console\Commands\Generate;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class OpenApi extends Command
|
||||
{
|
||||
protected $signature = 'openapi';
|
||||
protected $signature = 'generate:openapi';
|
||||
|
||||
protected $description = 'Generate OpenApi file.';
|
||||
|
||||
@@ -17,7 +18,7 @@ class OpenApi extends Command
|
||||
echo "Generating OpenAPI documentation.\n";
|
||||
// https://github.com/OAI/OpenAPI-Specification/releases
|
||||
$process = Process::run([
|
||||
'/var/www/html/vendor/bin/openapi',
|
||||
'./vendor/bin/openapi',
|
||||
'app',
|
||||
'-o',
|
||||
'openapi.yaml',
|
||||
@@ -29,5 +30,10 @@ class OpenApi extends Command
|
||||
$error = preg_replace('/^\h*\v+/m', '', $error);
|
||||
echo $error;
|
||||
echo $process->output();
|
||||
|
||||
$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";
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace App\Console\Commands\Generate;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Arr;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ServicesGenerate extends Command
|
||||
class Services extends Command
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $signature = 'services:generate';
|
||||
protected $signature = 'generate:services';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
@@ -39,7 +39,13 @@ class RootResetPassword extends Command
|
||||
}
|
||||
$this->info('Updating root password...');
|
||||
try {
|
||||
User::find(0)->update(['password' => Hash::make($password)]);
|
||||
$user = User::find(0);
|
||||
if (! $user) {
|
||||
$this->error('Root user not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
$user->update(['password' => Hash::make($password)]);
|
||||
$this->info('Root password updated successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Failed to update root password.');
|
||||
|
||||
@@ -6,13 +6,13 @@ use App\Jobs\CheckAndStartSentinelJob;
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\CleanupStaleMultiplexedConnections;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\RegenerateSslCertJob;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\ServerCheckJob;
|
||||
use App\Jobs\ServerCleanupMux;
|
||||
use App\Jobs\ServerPatchCheckJob;
|
||||
use App\Jobs\ServerStorageCheckJob;
|
||||
use App\Jobs\UpdateCoolifyJob;
|
||||
use App\Models\InstanceSettings;
|
||||
@@ -23,6 +23,7 @@ use App\Models\Team;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
@@ -51,6 +52,7 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
|
||||
// $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||
$this->scheduleInstance->command('cleanup:redis')->hourly();
|
||||
|
||||
if (isDev()) {
|
||||
// Instance Jobs
|
||||
@@ -84,6 +86,8 @@ class Kernel extends ConsoleKernel
|
||||
$this->checkScheduledBackups();
|
||||
$this->checkScheduledTasks();
|
||||
|
||||
$this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily();
|
||||
|
||||
$this->scheduleInstance->command('cleanup:database --yes')->daily();
|
||||
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
|
||||
}
|
||||
@@ -99,10 +103,14 @@ class Kernel extends ConsoleKernel
|
||||
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$this->scheduleInstance->job(function () use ($server) {
|
||||
CheckAndStartSentinelJob::dispatch($server);
|
||||
})->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
try {
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$this->scheduleInstance->job(function () use ($server) {
|
||||
CheckAndStartSentinelJob::dispatch($server);
|
||||
})->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error pulling images: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
$this->scheduleInstance->job(new CheckHelperImageJob)
|
||||
@@ -138,35 +146,50 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
|
||||
foreach ($servers as $server) {
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
// Sentinel check
|
||||
$lastSentinelUpdate = $server->sentinel_updated_at;
|
||||
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
|
||||
// Check container status every minute if Sentinel does not activated
|
||||
if (isCloud()) {
|
||||
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
|
||||
} else {
|
||||
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer();
|
||||
try {
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
// $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer();
|
||||
|
||||
$this->scheduleInstance->job(new ServerStorageCheckJob($server))->cron($server->settings->server_disk_usage_check_frequency)->timezone($serverTimezone)->onOneServer();
|
||||
}
|
||||
// Sentinel check
|
||||
$lastSentinelUpdate = $server->sentinel_updated_at;
|
||||
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
|
||||
// Check container status every minute if Sentinel does not activated
|
||||
if (isCloud()) {
|
||||
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
|
||||
} else {
|
||||
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer();
|
||||
}
|
||||
// $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer();
|
||||
|
||||
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
|
||||
$serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) {
|
||||
$serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency];
|
||||
}
|
||||
$this->scheduleInstance->job(new ServerStorageCheckJob($server))->cron($serverDiskUsageCheckFrequency)->timezone($serverTimezone)->onOneServer();
|
||||
}
|
||||
|
||||
// Cleanup multiplexed connections every hour
|
||||
// $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
|
||||
$dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) {
|
||||
$dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency];
|
||||
}
|
||||
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($dockerCleanupFrequency)->timezone($serverTimezone)->onOneServer();
|
||||
|
||||
// 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();
|
||||
// Server patch check - weekly
|
||||
$this->scheduleInstance->job(new ServerPatchCheckJob($server))->weekly()->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();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error checking resources: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,24 +223,28 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
|
||||
foreach ($finalScheduledBackups as $scheduled_backup) {
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
}
|
||||
try {
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
}
|
||||
$server = $scheduled_backup->server();
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
|
||||
$server = $scheduled_backup->server();
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
}
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
$this->scheduleInstance->job(new DatabaseBackupJob(
|
||||
backup: $scheduled_backup
|
||||
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error scheduling backup: '.$e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
}
|
||||
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
}
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
$this->scheduleInstance->job(new DatabaseBackupJob(
|
||||
backup: $scheduled_backup
|
||||
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,18 +291,23 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
|
||||
foreach ($finalScheduledTasks as $scheduled_task) {
|
||||
$server = $scheduled_task->server();
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
|
||||
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
|
||||
}
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
try {
|
||||
$server = $scheduled_task->server();
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
|
||||
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
|
||||
}
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
$this->scheduleInstance->job(new ScheduledTaskJob(
|
||||
task: $scheduled_task
|
||||
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error scheduling task: '.$e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
}
|
||||
$this->scheduleInstance->job(new ScheduledTaskJob(
|
||||
task: $scheduled_task
|
||||
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,21 +12,22 @@ class ApplicationStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
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');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
||||
@@ -12,21 +12,22 @@ class BackupCreated implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
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');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
||||
@@ -6,7 +6,7 @@ use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProxyStarted
|
||||
class CloudflareTunnelChanged
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
@@ -12,21 +12,22 @@ class CloudflareTunnelConfigured implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
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');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
||||
@@ -7,27 +7,27 @@ 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 ?int $teamId = null;
|
||||
|
||||
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');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
||||
@@ -13,28 +13,24 @@ class DatabaseStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $userId = null;
|
||||
public int|string|null $userId = null;
|
||||
|
||||
public function __construct($userId = null)
|
||||
{
|
||||
if (is_null($userId)) {
|
||||
$userId = Auth::id() ?? null;
|
||||
}
|
||||
if (is_null($userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): ?array
|
||||
{
|
||||
if (! is_null($this->userId)) {
|
||||
return [
|
||||
new PrivateChannel("user.{$this->userId}"),
|
||||
];
|
||||
if (is_null($this->userId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return null;
|
||||
return [
|
||||
new PrivateChannel("user.{$this->userId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,22 @@ class FileStorageChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
||||
@@ -3,32 +3,12 @@
|
||||
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 ProxyStatusChanged implements ShouldBroadcast
|
||||
class ProxyStatusChanged
|
||||
{
|
||||
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}"),
|
||||
];
|
||||
}
|
||||
public function __construct(public $data) {}
|
||||
}
|
||||
|
||||
35
app/Events/ProxyStatusChangedUI.php
Normal file
35
app/Events/ProxyStatusChangedUI.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;
|
||||
|
||||
class ProxyStatusChangedUI implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct(?int $teamId = null)
|
||||
{
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -12,21 +12,22 @@ class ScheduledTaskDone implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
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');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
||||
35
app/Events/ServerPackageUpdated.php
Normal file
35
app/Events/ServerPackageUpdated.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;
|
||||
|
||||
class ServerPackageUpdated implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
35
app/Events/ServiceChecked.php
Normal file
35
app/Events/ServiceChecked.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;
|
||||
|
||||
class ServiceChecked implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -13,27 +13,22 @@ class ServiceStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public ?string $userId = null;
|
||||
|
||||
public function __construct($userId = null)
|
||||
{
|
||||
if (is_null($userId)) {
|
||||
$userId = Auth::id() ?? null;
|
||||
public function __construct(
|
||||
public ?int $teamId = null
|
||||
) {
|
||||
if (is_null($this->teamId) && Auth::check() && Auth::user()->currentTeam()) {
|
||||
$this->teamId = Auth::user()->currentTeam()->id;
|
||||
}
|
||||
if (is_null($userId)) {
|
||||
return false;
|
||||
}
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): ?array
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (! is_null($this->userId)) {
|
||||
return [
|
||||
new PrivateChannel("user.{$this->userId}"),
|
||||
];
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return null;
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,21 @@ class TestEvent implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->teamId = auth()->user()->currentTeam()->id;
|
||||
if (auth()->check() && auth()->user()->currentTeam()) {
|
||||
$this->teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
||||
@@ -103,7 +103,11 @@ class SshMultiplexingHelper
|
||||
}
|
||||
|
||||
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
|
||||
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
|
||||
if ($server->isIpv6()) {
|
||||
$scp_command .= "{$source} {$server->user}@[{$server->ip}]:{$dest}";
|
||||
} else {
|
||||
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
|
||||
}
|
||||
|
||||
return $scp_command;
|
||||
}
|
||||
|
||||
233
app/Helpers/SslHelper.php
Normal file
233
app/Helpers/SslHelper.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
class SslHelper
|
||||
{
|
||||
private const DEFAULT_ORGANIZATION_NAME = 'Coolify';
|
||||
|
||||
private const DEFAULT_COUNTRY_NAME = 'XX';
|
||||
|
||||
private const DEFAULT_STATE_NAME = 'Default';
|
||||
|
||||
public static function generateSslCertificate(
|
||||
string $commonName,
|
||||
array $subjectAlternativeNames = [],
|
||||
?string $resourceType = null,
|
||||
?int $resourceId = null,
|
||||
?int $serverId = null,
|
||||
int $validityDays = 365,
|
||||
?string $caCert = null,
|
||||
?string $caKey = null,
|
||||
bool $isCaCertificate = false,
|
||||
?string $configurationDir = null,
|
||||
?string $mountPath = null,
|
||||
bool $isPemKeyFileRequired = false,
|
||||
): SslCertificate {
|
||||
$organizationName = self::DEFAULT_ORGANIZATION_NAME;
|
||||
$countryName = self::DEFAULT_COUNTRY_NAME;
|
||||
$stateName = self::DEFAULT_STATE_NAME;
|
||||
|
||||
try {
|
||||
$privateKey = openssl_pkey_new([
|
||||
'private_key_type' => OPENSSL_KEYTYPE_EC,
|
||||
'curve_name' => 'secp521r1',
|
||||
]);
|
||||
|
||||
if ($privateKey === false) {
|
||||
throw new \RuntimeException('Failed to generate private key: '.openssl_error_string());
|
||||
}
|
||||
|
||||
if (! openssl_pkey_export($privateKey, $privateKeyStr)) {
|
||||
throw new \RuntimeException('Failed to export private key: '.openssl_error_string());
|
||||
}
|
||||
|
||||
if (! is_null($serverId) && ! $isCaCertificate) {
|
||||
$server = Server::find($serverId);
|
||||
if ($server) {
|
||||
$ip = $server->getIp;
|
||||
if ($ip) {
|
||||
$type = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)
|
||||
? 'IP'
|
||||
: 'DNS';
|
||||
$subjectAlternativeNames = array_unique(
|
||||
array_merge($subjectAlternativeNames, ["$type:$ip"])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$basicConstraints = $isCaCertificate ? 'critical, CA:TRUE, pathlen:0' : 'critical, CA:FALSE';
|
||||
$keyUsage = $isCaCertificate ? 'critical, keyCertSign, cRLSign' : 'critical, digitalSignature, keyAgreement';
|
||||
|
||||
$subjectAltNameSection = '';
|
||||
$extendedKeyUsageSection = '';
|
||||
|
||||
if (! $isCaCertificate) {
|
||||
$extendedKeyUsageSection = "\nextendedKeyUsage = serverAuth, clientAuth";
|
||||
|
||||
$subjectAlternativeNames = array_values(
|
||||
array_unique(
|
||||
array_merge(["DNS:$commonName"], $subjectAlternativeNames)
|
||||
)
|
||||
);
|
||||
|
||||
$formattedSubjectAltNames = array_map(
|
||||
function ($index, $san) {
|
||||
[$type, $value] = explode(':', $san, 2);
|
||||
|
||||
return "{$type}.".($index + 1)." = $value";
|
||||
},
|
||||
array_keys($subjectAlternativeNames),
|
||||
$subjectAlternativeNames
|
||||
);
|
||||
|
||||
$subjectAltNameSection = "subjectAltName = @subject_alt_names\n\n[ subject_alt_names ]\n"
|
||||
.implode("\n", $formattedSubjectAltNames);
|
||||
}
|
||||
|
||||
$config = <<<CONF
|
||||
[ req ]
|
||||
prompt = no
|
||||
distinguished_name = distinguished_name
|
||||
req_extensions = req_ext
|
||||
|
||||
[ distinguished_name ]
|
||||
CN = $commonName
|
||||
|
||||
[ req_ext ]
|
||||
basicConstraints = $basicConstraints
|
||||
keyUsage = $keyUsage
|
||||
{$extendedKeyUsageSection}
|
||||
|
||||
[ v3_req ]
|
||||
basicConstraints = $basicConstraints
|
||||
keyUsage = $keyUsage
|
||||
{$extendedKeyUsageSection}
|
||||
subjectKeyIdentifier = hash
|
||||
{$subjectAltNameSection}
|
||||
CONF;
|
||||
|
||||
$tempConfig = tmpfile();
|
||||
fwrite($tempConfig, $config);
|
||||
$tempConfigPath = stream_get_meta_data($tempConfig)['uri'];
|
||||
|
||||
$csr = openssl_csr_new([
|
||||
'commonName' => $commonName,
|
||||
'organizationName' => $organizationName,
|
||||
'countryName' => $countryName,
|
||||
'stateOrProvinceName' => $stateName,
|
||||
], $privateKey, [
|
||||
'digest_alg' => 'sha512',
|
||||
'config' => $tempConfigPath,
|
||||
'req_extensions' => 'req_ext',
|
||||
]);
|
||||
|
||||
if ($csr === false) {
|
||||
throw new \RuntimeException('Failed to generate CSR: '.openssl_error_string());
|
||||
}
|
||||
|
||||
$certificate = openssl_csr_sign(
|
||||
$csr,
|
||||
$caCert ?? null,
|
||||
$caKey ?? $privateKey,
|
||||
$validityDays,
|
||||
[
|
||||
'digest_alg' => 'sha512',
|
||||
'config' => $tempConfigPath,
|
||||
'x509_extensions' => 'v3_req',
|
||||
],
|
||||
random_int(1, PHP_INT_MAX)
|
||||
);
|
||||
|
||||
if ($certificate === false) {
|
||||
throw new \RuntimeException('Failed to sign certificate: '.openssl_error_string());
|
||||
}
|
||||
|
||||
if (! openssl_x509_export($certificate, $certificateStr)) {
|
||||
throw new \RuntimeException('Failed to export certificate: '.openssl_error_string());
|
||||
}
|
||||
|
||||
SslCertificate::query()
|
||||
->where('resource_type', $resourceType)
|
||||
->where('resource_id', $resourceId)
|
||||
->where('server_id', $serverId)
|
||||
->delete();
|
||||
|
||||
$sslCertificate = SslCertificate::create([
|
||||
'ssl_certificate' => $certificateStr,
|
||||
'ssl_private_key' => $privateKeyStr,
|
||||
'resource_type' => $resourceType,
|
||||
'resource_id' => $resourceId,
|
||||
'server_id' => $serverId,
|
||||
'configuration_dir' => $configurationDir,
|
||||
'mount_path' => $mountPath,
|
||||
'valid_until' => CarbonImmutable::now()->addDays($validityDays),
|
||||
'is_ca_certificate' => $isCaCertificate,
|
||||
'common_name' => $commonName,
|
||||
'subject_alternative_names' => $subjectAlternativeNames,
|
||||
]);
|
||||
|
||||
if ($configurationDir && $mountPath && $resourceType && $resourceId) {
|
||||
$model = app($resourceType)->find($resourceId);
|
||||
|
||||
$model->fileStorages()
|
||||
->where('resource_type', $model->getMorphClass())
|
||||
->where('resource_id', $model->id)
|
||||
->get()
|
||||
->filter(function ($storage) use ($mountPath) {
|
||||
return in_array($storage->mount_path, [
|
||||
$mountPath.'/server.crt',
|
||||
$mountPath.'/server.key',
|
||||
$mountPath.'/server.pem',
|
||||
]);
|
||||
})
|
||||
->each(function ($storage) {
|
||||
$storage->delete();
|
||||
});
|
||||
|
||||
if ($isPemKeyFileRequired) {
|
||||
$model->fileStorages()->create([
|
||||
'fs_path' => $configurationDir.'/ssl/server.pem',
|
||||
'mount_path' => $mountPath.'/server.pem',
|
||||
'content' => $certificateStr."\n".$privateKeyStr,
|
||||
'is_directory' => false,
|
||||
'chmod' => '600',
|
||||
'resource_type' => $resourceType,
|
||||
'resource_id' => $resourceId,
|
||||
]);
|
||||
} else {
|
||||
$model->fileStorages()->create([
|
||||
'fs_path' => $configurationDir.'/ssl/server.crt',
|
||||
'mount_path' => $mountPath.'/server.crt',
|
||||
'content' => $certificateStr,
|
||||
'is_directory' => false,
|
||||
'chmod' => '644',
|
||||
'resource_type' => $resourceType,
|
||||
'resource_id' => $resourceId,
|
||||
]);
|
||||
|
||||
$model->fileStorages()->create([
|
||||
'fs_path' => $configurationDir.'/ssl/server.key',
|
||||
'mount_path' => $mountPath.'/server.key',
|
||||
'content' => $privateKeyStr,
|
||||
'is_directory' => false,
|
||||
'chmod' => '600',
|
||||
'resource_type' => $resourceType,
|
||||
'resource_id' => $resourceId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $sslCertificate;
|
||||
} catch (\Throwable $e) {
|
||||
throw new \RuntimeException('SSL Certificate generation failed: '.$e->getMessage(), 0, $e);
|
||||
} finally {
|
||||
fclose($tempConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use App\Models\Service;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -44,6 +45,7 @@ class ApplicationsController extends Controller
|
||||
'private_key_id',
|
||||
'value',
|
||||
'real_value',
|
||||
'http_basic_auth_password',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -182,6 +184,10 @@ class ApplicationsController extends Controller
|
||||
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
|
||||
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -298,6 +304,10 @@ class ApplicationsController extends Controller
|
||||
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
|
||||
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -414,6 +424,10 @@ class ApplicationsController extends Controller
|
||||
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
|
||||
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -514,6 +528,10 @@ class ApplicationsController extends Controller
|
||||
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
|
||||
'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.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -611,6 +629,10 @@ class ApplicationsController extends Controller
|
||||
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
|
||||
'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.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -674,6 +696,7 @@ class ApplicationsController extends Controller
|
||||
'description' => ['type' => 'string', 'description' => 'The application description.'],
|
||||
'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.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -710,7 +733,6 @@ class ApplicationsController extends Controller
|
||||
|
||||
private function create_application(Request $request, $type)
|
||||
{
|
||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration'];
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
@@ -720,6 +742,8 @@ class ApplicationsController extends Controller
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'string|max:255',
|
||||
'description' => 'string|nullable',
|
||||
@@ -728,6 +752,9 @@ class ApplicationsController extends Controller
|
||||
'environment_uuid' => 'string|nullable',
|
||||
'server_uuid' => 'string|required',
|
||||
'destination_uuid' => 'string',
|
||||
'is_http_basic_auth_enabled' => 'boolean',
|
||||
'http_basic_auth_username' => 'string|nullable',
|
||||
'http_basic_auth_password' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
@@ -756,6 +783,7 @@ class ApplicationsController extends Controller
|
||||
$githubAppUuid = $request->github_app_uuid;
|
||||
$useBuildServer = $request->use_build_server;
|
||||
$isStatic = $request->is_static;
|
||||
$connectToDockerNetwork = $request->connect_to_docker_network;
|
||||
$customNginxConfiguration = $request->custom_nginx_configuration;
|
||||
|
||||
if (! is_null($customNginxConfiguration)) {
|
||||
@@ -811,6 +839,11 @@ class ApplicationsController extends Controller
|
||||
'docker_compose_raw' => 'string|nullable',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
];
|
||||
// ports_exposes is not required for dockercompose
|
||||
if ($request->build_pack === 'dockercompose') {
|
||||
$validationRules['ports_exposes'] = 'string';
|
||||
$request->offsetSet('ports_exposes', '80');
|
||||
}
|
||||
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
||||
$validator = customApiValidator($request->all(), $validationRules);
|
||||
if ($validator->fails()) {
|
||||
@@ -822,10 +855,6 @@ class ApplicationsController extends Controller
|
||||
if (! $request->has('name')) {
|
||||
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
|
||||
}
|
||||
if ($request->build_pack === 'dockercompose') {
|
||||
$request->offsetSet('ports_exposes', '80');
|
||||
}
|
||||
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
@@ -848,7 +877,13 @@ class ApplicationsController extends Controller
|
||||
if ($dockerComposeDomainsJson->count() > 0) {
|
||||
$application->docker_compose_domains = $dockerComposeDomainsJson;
|
||||
}
|
||||
|
||||
$repository_url_parsed = Url::fromString($request->git_repository);
|
||||
$git_host = $repository_url_parsed->getHost();
|
||||
if ($git_host === 'github.com') {
|
||||
$application->source_type = GithubApp::class;
|
||||
$application->source_id = GithubApp::find(0)->id;
|
||||
}
|
||||
$application->git_repository = $repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2);
|
||||
$application->fqdn = $fqdn;
|
||||
$application->destination_id = $destination->id;
|
||||
$application->destination_type = $destination->getMorphClass();
|
||||
@@ -858,6 +893,10 @@ class ApplicationsController extends Controller
|
||||
$application->settings->is_static = $isStatic;
|
||||
$application->settings->save();
|
||||
}
|
||||
if (isset($connectToDockerNetwork)) {
|
||||
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
||||
$application->settings->save();
|
||||
}
|
||||
if (isset($useBuildServer)) {
|
||||
$application->settings->is_build_server_enabled = $useBuildServer;
|
||||
$application->settings->save();
|
||||
@@ -872,12 +911,17 @@ class ApplicationsController extends Controller
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
no_questions_asked: true,
|
||||
is_api: true,
|
||||
);
|
||||
if ($result['status'] === 'skipped') {
|
||||
return response()->json([
|
||||
'message' => $result['message'],
|
||||
], 200);
|
||||
}
|
||||
} else {
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
LoadComposeFile::dispatch($application);
|
||||
@@ -924,10 +968,31 @@ class ApplicationsController extends Controller
|
||||
if (! $githubApp) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
}
|
||||
$token = generateGithubInstallationToken($githubApp);
|
||||
if (! $token) {
|
||||
return response()->json(['message' => 'Failed to generate Github App token.'], 400);
|
||||
}
|
||||
|
||||
$repositories = collect();
|
||||
$page = 1;
|
||||
$repositories = loadRepositoryByPage($githubApp, $token, $page);
|
||||
if ($repositories['total_count'] > 0) {
|
||||
while (count($repositories['repositories']) < $repositories['total_count']) {
|
||||
$page++;
|
||||
$repositories = loadRepositoryByPage($githubApp, $token, $page);
|
||||
}
|
||||
}
|
||||
|
||||
$gitRepository = $request->git_repository;
|
||||
if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) {
|
||||
$gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', '');
|
||||
}
|
||||
$gitRepositoryFound = collect($repositories['repositories'])->firstWhere('full_name', $gitRepository);
|
||||
if (! $gitRepositoryFound) {
|
||||
return response()->json(['message' => 'Repository not found.'], 404);
|
||||
}
|
||||
$repository_project_id = data_get($gitRepositoryFound, 'id');
|
||||
|
||||
$application = new Application;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
|
||||
@@ -935,7 +1000,33 @@ class ApplicationsController extends Controller
|
||||
|
||||
$dockerComposeDomainsJson = collect();
|
||||
if ($request->has('docker_compose_domains')) {
|
||||
$yaml = Yaml::parse($application->docker_compose_raw);
|
||||
if (! $request->has('docker_compose_raw')) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The base64 encoded docker_compose_raw is required.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (! isBase64Encoded($request->docker_compose_raw)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
|
||||
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$yaml = Yaml::parse($dockerComposeRaw);
|
||||
$services = data_get($yaml, 'services');
|
||||
$dockerComposeDomains = collect($request->docker_compose_domains);
|
||||
if ($dockerComposeDomains->count() > 0) {
|
||||
@@ -958,6 +1049,8 @@ class ApplicationsController extends Controller
|
||||
$application->environment_id = $environment->id;
|
||||
$application->source_type = $githubApp->getMorphClass();
|
||||
$application->source_id = $githubApp->id;
|
||||
$application->repository_project_id = $repository_project_id;
|
||||
|
||||
$application->save();
|
||||
$application->refresh();
|
||||
if (isset($useBuildServer)) {
|
||||
@@ -973,12 +1066,17 @@ class ApplicationsController extends Controller
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
no_questions_asked: true,
|
||||
is_api: true,
|
||||
);
|
||||
if ($result['status'] === 'skipped') {
|
||||
return response()->json([
|
||||
'message' => $result['message'],
|
||||
], 200);
|
||||
}
|
||||
} else {
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
LoadComposeFile::dispatch($application);
|
||||
@@ -1034,7 +1132,34 @@ class ApplicationsController extends Controller
|
||||
|
||||
$dockerComposeDomainsJson = collect();
|
||||
if ($request->has('docker_compose_domains')) {
|
||||
$yaml = Yaml::parse($application->docker_compose_raw);
|
||||
if (! $request->has('docker_compose_raw')) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The base64 encoded docker_compose_raw is required.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (! isBase64Encoded($request->docker_compose_raw)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
|
||||
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
|
||||
$yaml = Yaml::parse($dockerComposeRaw);
|
||||
$services = data_get($yaml, 'services');
|
||||
$dockerComposeDomains = collect($request->docker_compose_domains);
|
||||
if ($dockerComposeDomains->count() > 0) {
|
||||
@@ -1070,12 +1195,17 @@ class ApplicationsController extends Controller
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
no_questions_asked: true,
|
||||
is_api: true,
|
||||
);
|
||||
if ($result['status'] === 'skipped') {
|
||||
return response()->json([
|
||||
'message' => $result['message'],
|
||||
], 200);
|
||||
}
|
||||
} else {
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
LoadComposeFile::dispatch($application);
|
||||
@@ -1159,12 +1289,17 @@ class ApplicationsController extends Controller
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
no_questions_asked: true,
|
||||
is_api: true,
|
||||
);
|
||||
if ($result['status'] === 'skipped') {
|
||||
return response()->json([
|
||||
'message' => $result['message'],
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
@@ -1223,12 +1358,17 @@ class ApplicationsController extends Controller
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
no_questions_asked: true,
|
||||
is_api: true,
|
||||
);
|
||||
if ($result['status'] === 'skipped') {
|
||||
return response()->json([
|
||||
'message' => $result['message'],
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
@@ -1291,11 +1431,6 @@ class ApplicationsController extends Controller
|
||||
$dockerCompose = base64_decode($request->docker_compose_raw);
|
||||
$dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
|
||||
|
||||
// $isValid = validateComposeFile($dockerComposeRaw, $server_id);
|
||||
// if ($isValid !== 'OK') {
|
||||
// return $this->dispatch('error', "Invalid docker-compose file.\n$isValid");
|
||||
// }
|
||||
|
||||
$service = new Service;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
$service->fill($request->all());
|
||||
@@ -1307,7 +1442,6 @@ class ApplicationsController extends Controller
|
||||
$service->destination_type = $destination->getMorphClass();
|
||||
$service->save();
|
||||
|
||||
$service->name = "service-$service->uuid";
|
||||
$service->parse(isNew: true);
|
||||
if ($instantDeploy) {
|
||||
StartService::dispatch($service);
|
||||
@@ -1388,6 +1522,108 @@ class ApplicationsController extends Controller
|
||||
return response()->json($this->removeSensitiveData($application));
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Get application logs.',
|
||||
description: 'Get application logs by UUID.',
|
||||
path: '/applications/{uuid}/logs',
|
||||
operationId: 'get-application-logs-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'lines',
|
||||
in: 'query',
|
||||
description: 'Number of lines to show from the end of the logs.',
|
||||
required: false,
|
||||
schema: new OA\Schema(
|
||||
type: 'integer',
|
||||
format: 'int32',
|
||||
default: 100,
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get application logs by UUID.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'logs' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function logs_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$uuid = $request->route('uuid');
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 400);
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id);
|
||||
|
||||
if ($containers->count() == 0) {
|
||||
return response()->json([
|
||||
'message' => 'Application is not running.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$container = $containers->first();
|
||||
|
||||
$status = getContainerStatus($application->destination->server, $container['Names']);
|
||||
if ($status !== 'running') {
|
||||
return response()->json([
|
||||
'message' => 'Application is not running.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$lines = $request->query->get('lines', 100) ?: 100;
|
||||
$logs = getContainerLogs($application->destination->server, $container['ID'], $lines);
|
||||
|
||||
return response()->json([
|
||||
'logs' => $logs,
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete',
|
||||
description: 'Delete application by UUID.',
|
||||
@@ -1483,6 +1719,18 @@ class ApplicationsController extends Controller
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Application updated.',
|
||||
required: true,
|
||||
@@ -1553,6 +1801,7 @@ class ApplicationsController extends Controller
|
||||
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
|
||||
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -1594,25 +1843,19 @@ class ApplicationsController extends Controller
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
if ($request->collect()->count() == 0) {
|
||||
return response()->json([
|
||||
'message' => 'Invalid request.',
|
||||
], 400);
|
||||
}
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json([
|
||||
'message' => 'Application not found',
|
||||
], 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', 'custom_nginx_configuration'];
|
||||
$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', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network'];
|
||||
|
||||
$validationRules = [
|
||||
'name' => 'string|max:255',
|
||||
@@ -1625,6 +1868,9 @@ class ApplicationsController extends Controller
|
||||
'docker_compose_custom_start_command' => 'string|nullable',
|
||||
'docker_compose_custom_build_command' => 'string|nullable',
|
||||
'custom_nginx_configuration' => 'string|nullable',
|
||||
'is_http_basic_auth_enabled' => 'boolean|nullable',
|
||||
'http_basic_auth_username' => 'string',
|
||||
'http_basic_auth_password' => 'string',
|
||||
];
|
||||
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
||||
$validator = customApiValidator($request->all(), $validationRules);
|
||||
@@ -1680,8 +1926,32 @@ class ApplicationsController extends Controller
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($request->has('is_http_basic_auth_enabled') && $request->is_http_basic_auth_enabled === true) {
|
||||
if (blank($application->http_basic_auth_username) || blank($application->http_basic_auth_password)) {
|
||||
$validationErrors = [];
|
||||
if (blank($request->http_basic_auth_username)) {
|
||||
$validationErrors['http_basic_auth_username'] = 'The http_basic_auth_username is required.';
|
||||
}
|
||||
if (blank($request->http_basic_auth_password)) {
|
||||
$validationErrors['http_basic_auth_password'] = 'The http_basic_auth_password is required.';
|
||||
}
|
||||
if (count($validationErrors) > 0) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validationErrors,
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($request->has('is_http_basic_auth_enabled') && $application->is_container_label_readonly_enabled === false) {
|
||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
$application->save();
|
||||
}
|
||||
|
||||
$domains = $request->domains;
|
||||
if ($request->has('domains') && $server->isProxyShouldRun()) {
|
||||
$requestHasDomains = $request->has('domains');
|
||||
if ($requestHasDomains && $server->isProxyShouldRun()) {
|
||||
$uuid = $request->uuid;
|
||||
$fqdn = $request->domains;
|
||||
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
|
||||
@@ -1713,7 +1983,34 @@ class ApplicationsController extends Controller
|
||||
|
||||
$dockerComposeDomainsJson = collect();
|
||||
if ($request->has('docker_compose_domains')) {
|
||||
$yaml = Yaml::parse($application->docker_compose_raw);
|
||||
if (! $request->has('docker_compose_raw')) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The base64 encoded docker_compose_raw is required.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (! isBase64Encoded($request->docker_compose_raw)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
|
||||
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
|
||||
$yaml = Yaml::parse($dockerComposeRaw);
|
||||
$services = data_get($yaml, 'services');
|
||||
$dockerComposeDomains = collect($request->docker_compose_domains);
|
||||
if ($dockerComposeDomains->count() > 0) {
|
||||
@@ -1728,6 +2025,7 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
$instantDeploy = $request->instant_deploy;
|
||||
$isStatic = $request->is_static;
|
||||
$connectToDockerNetwork = $request->connect_to_docker_network;
|
||||
$useBuildServer = $request->use_build_server;
|
||||
|
||||
if (isset($useBuildServer)) {
|
||||
@@ -1740,10 +2038,15 @@ class ApplicationsController extends Controller
|
||||
$application->settings->save();
|
||||
}
|
||||
|
||||
if (isset($connectToDockerNetwork)) {
|
||||
$application->settings->connect_to_docker_network = $connectToDockerNetwork;
|
||||
$application->settings->save();
|
||||
}
|
||||
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
|
||||
$data = $request->all();
|
||||
if ($request->has('domains') && $server->isProxyShouldRun()) {
|
||||
if ($requestHasDomains && $server->isProxyShouldRun()) {
|
||||
data_set($data, 'fqdn', $domains);
|
||||
}
|
||||
|
||||
@@ -1756,11 +2059,16 @@ class ApplicationsController extends Controller
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
is_api: true,
|
||||
);
|
||||
if ($result['status'] === 'skipped') {
|
||||
return response()->json([
|
||||
'message' => $result['message'],
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
@@ -2392,10 +2700,6 @@ class ApplicationsController extends Controller
|
||||
])->setStatusCode(201);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Something went wrong.',
|
||||
], 500);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
@@ -2577,13 +2881,21 @@ class ApplicationsController extends Controller
|
||||
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: $force,
|
||||
is_api: true,
|
||||
no_questions_asked: $instant_deploy
|
||||
);
|
||||
if ($result['status'] === 'skipped') {
|
||||
return response()->json(
|
||||
[
|
||||
'message' => $result['message'],
|
||||
],
|
||||
200
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
@@ -2738,12 +3050,17 @@ class ApplicationsController extends Controller
|
||||
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
restart_only: true,
|
||||
is_api: true,
|
||||
);
|
||||
if ($result['status'] === 'skipped') {
|
||||
return response()->json([
|
||||
'message' => $result['message'],
|
||||
], 200);
|
||||
}
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
@@ -2753,130 +3070,130 @@ class ApplicationsController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Execute Command',
|
||||
description: "Execute a command on the application's current container.",
|
||||
path: '/applications/{uuid}/execute',
|
||||
operationId: 'execute-command-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
description: 'Command to execute.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: "Execute a command on the application's current container.",
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Command executed.'],
|
||||
'response' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function execute_command_by_uuid(Request $request)
|
||||
{
|
||||
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
|
||||
$allowedFields = ['command'];
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$uuid = $request->route('uuid');
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 400);
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'command' => 'string|required',
|
||||
]);
|
||||
// #[OA\Post(
|
||||
// summary: 'Execute Command',
|
||||
// description: "Execute a command on the application's current container.",
|
||||
// path: '/applications/{uuid}/execute',
|
||||
// operationId: 'execute-command-application',
|
||||
// security: [
|
||||
// ['bearerAuth' => []],
|
||||
// ],
|
||||
// tags: ['Applications'],
|
||||
// parameters: [
|
||||
// new OA\Parameter(
|
||||
// name: 'uuid',
|
||||
// in: 'path',
|
||||
// description: 'UUID of the application.',
|
||||
// required: true,
|
||||
// schema: new OA\Schema(
|
||||
// type: 'string',
|
||||
// format: 'uuid',
|
||||
// )
|
||||
// ),
|
||||
// ],
|
||||
// requestBody: new OA\RequestBody(
|
||||
// required: true,
|
||||
// description: 'Command to execute.',
|
||||
// content: new OA\MediaType(
|
||||
// mediaType: 'application/json',
|
||||
// schema: new OA\Schema(
|
||||
// type: 'object',
|
||||
// properties: [
|
||||
// 'command' => ['type' => 'string', 'description' => 'Command to execute.'],
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// responses: [
|
||||
// new OA\Response(
|
||||
// response: 200,
|
||||
// description: "Execute a command on the application's current container.",
|
||||
// content: [
|
||||
// new OA\MediaType(
|
||||
// mediaType: 'application/json',
|
||||
// schema: new OA\Schema(
|
||||
// type: 'object',
|
||||
// properties: [
|
||||
// 'message' => ['type' => 'string', 'example' => 'Command executed.'],
|
||||
// 'response' => ['type' => 'string'],
|
||||
// ]
|
||||
// )
|
||||
// ),
|
||||
// ]
|
||||
// ),
|
||||
// new OA\Response(
|
||||
// response: 401,
|
||||
// ref: '#/components/responses/401',
|
||||
// ),
|
||||
// new OA\Response(
|
||||
// response: 400,
|
||||
// ref: '#/components/responses/400',
|
||||
// ),
|
||||
// new OA\Response(
|
||||
// response: 404,
|
||||
// ref: '#/components/responses/404',
|
||||
// ),
|
||||
// ]
|
||||
// )]
|
||||
// public function execute_command_by_uuid(Request $request)
|
||||
// {
|
||||
// // TODO: Need to review this from security perspective, to not allow arbitrary command execution
|
||||
// $allowedFields = ['command'];
|
||||
// $teamId = getTeamIdFromToken();
|
||||
// if (is_null($teamId)) {
|
||||
// return invalidTokenResponse();
|
||||
// }
|
||||
// $uuid = $request->route('uuid');
|
||||
// if (! $uuid) {
|
||||
// return response()->json(['message' => 'UUID is required.'], 400);
|
||||
// }
|
||||
// $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
// if (! $application) {
|
||||
// return response()->json(['message' => 'Application not found.'], 404);
|
||||
// }
|
||||
// $return = validateIncomingRequest($request);
|
||||
// if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
// return $return;
|
||||
// }
|
||||
// $validator = customApiValidator($request->all(), [
|
||||
// 'command' => 'string|required',
|
||||
// ]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
// $extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
// if ($validator->fails() || ! empty($extraFields)) {
|
||||
// $errors = $validator->errors();
|
||||
// if (! empty($extraFields)) {
|
||||
// foreach ($extraFields as $field) {
|
||||
// $errors->add($field, 'This field is not allowed.');
|
||||
// }
|
||||
// }
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
// return response()->json([
|
||||
// 'message' => 'Validation failed.',
|
||||
// 'errors' => $errors,
|
||||
// ], 422);
|
||||
// }
|
||||
|
||||
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
|
||||
$status = getContainerStatus($application->destination->server, $container['Names']);
|
||||
// $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
|
||||
// $status = getContainerStatus($application->destination->server, $container['Names']);
|
||||
|
||||
if ($status !== 'running') {
|
||||
return response()->json([
|
||||
'message' => 'Application is not running.',
|
||||
], 400);
|
||||
}
|
||||
// if ($status !== 'running') {
|
||||
// return response()->json([
|
||||
// 'message' => 'Application is not running.',
|
||||
// ], 400);
|
||||
// }
|
||||
|
||||
$commands = collect([
|
||||
executeInDocker($container['Names'], $request->command),
|
||||
]);
|
||||
// $commands = collect([
|
||||
// executeInDocker($container['Names'], $request->command),
|
||||
// ]);
|
||||
|
||||
$res = instant_remote_process(command: $commands, server: $application->destination->server);
|
||||
// $res = instant_remote_process(command: $commands, server: $application->destination->server);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Command executed.',
|
||||
'response' => $res,
|
||||
]);
|
||||
}
|
||||
// return response()->json([
|
||||
// 'message' => 'Command executed.',
|
||||
// 'response' => $res,
|
||||
// ]);
|
||||
// }
|
||||
|
||||
private function validateDataApplications(Request $request, Server $server)
|
||||
{
|
||||
|
||||
@@ -5,8 +5,10 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Actions\Database\StartDatabase;
|
||||
use App\Actions\Service\StartService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
@@ -131,7 +133,7 @@ class DeployController extends Controller
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Deploy',
|
||||
description: 'Deploy by tag or uuid. `Post` request also accepted.',
|
||||
description: 'Deploy by tag or uuid. `Post` request also accepted with `uuid` and `tag` json body.',
|
||||
path: '/deploy',
|
||||
operationId: 'deploy-by-tag-or-uuid',
|
||||
security: [
|
||||
@@ -142,6 +144,7 @@ class DeployController extends Controller
|
||||
new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')),
|
||||
new OA\Parameter(name: 'pr', in: 'query', description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
|
||||
responses: [
|
||||
@@ -184,26 +187,32 @@ class DeployController extends Controller
|
||||
public function deploy(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
$uuids = $request->query->get('uuid');
|
||||
$tags = $request->query->get('tag');
|
||||
$force = $request->query->get('force') ?? false;
|
||||
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$uuids = $request->input('uuid');
|
||||
$tags = $request->input('tag');
|
||||
$force = $request->input('force') ?? false;
|
||||
$pr = $request->input('pr') ? max((int) $request->input('pr'), 0) : 0;
|
||||
|
||||
if ($uuids && $tags) {
|
||||
return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400);
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
if ($tags && $pr) {
|
||||
return response()->json(['message' => 'You can only use tag or pr, not both.'], 400);
|
||||
}
|
||||
if ($tags) {
|
||||
return $this->by_tags($tags, $teamId, $force);
|
||||
} elseif ($uuids) {
|
||||
return $this->by_uuids($uuids, $teamId, $force);
|
||||
return $this->by_uuids($uuids, $teamId, $force, $pr);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'You must provide uuid or tag.'], 400);
|
||||
}
|
||||
|
||||
private function by_uuids(string $uuid, int $teamId, bool $force = false)
|
||||
private function by_uuids(string $uuid, int $teamId, bool $force = false, int $pr = 0)
|
||||
{
|
||||
$uuids = explode(',', $uuid);
|
||||
$uuids = collect(array_filter($uuids));
|
||||
@@ -216,7 +225,7 @@ class DeployController extends Controller
|
||||
foreach ($uuids as $uuid) {
|
||||
$resource = getResourceByUuid($uuid, $teamId);
|
||||
if ($resource) {
|
||||
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
|
||||
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force, $pr);
|
||||
if ($deployment_uuid) {
|
||||
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
|
||||
} else {
|
||||
@@ -281,7 +290,7 @@ class DeployController extends Controller
|
||||
return response()->json(['message' => 'No resources found with this tag.'], 404);
|
||||
}
|
||||
|
||||
public function deploy_resource($resource, bool $force = false): array
|
||||
public function deploy_resource($resource, bool $force = false, int $pr = 0): array
|
||||
{
|
||||
$message = null;
|
||||
$deployment_uuid = null;
|
||||
@@ -289,29 +298,133 @@ class DeployController extends Controller
|
||||
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
|
||||
}
|
||||
switch ($resource?->getMorphClass()) {
|
||||
case \App\Models\Application::class:
|
||||
case Application::class:
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $resource,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: $force,
|
||||
pull_request_id: $pr,
|
||||
);
|
||||
$message = "Application {$resource->name} deployment queued.";
|
||||
if ($result['status'] === 'skipped') {
|
||||
$message = $result['message'];
|
||||
} else {
|
||||
$message = "Application {$resource->name} deployment queued.";
|
||||
}
|
||||
break;
|
||||
case \App\Models\Service::class:
|
||||
case Service::class:
|
||||
StartService::run($resource);
|
||||
$message = "Service {$resource->name} started. It could take a while, be patient.";
|
||||
break;
|
||||
default:
|
||||
// Database resource
|
||||
StartDatabase::dispatch($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
$resource->started_at ??= now();
|
||||
$resource->save();
|
||||
|
||||
$message = "Database {$resource->name} started.";
|
||||
break;
|
||||
}
|
||||
|
||||
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List application deployments',
|
||||
description: 'List application deployments by using the app uuid',
|
||||
path: '/deployments/applications/{uuid}',
|
||||
operationId: 'list-deployments-by-app-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Deployments'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'skip',
|
||||
in: 'query',
|
||||
description: 'Number of records to skip.',
|
||||
required: false,
|
||||
schema: new OA\Schema(
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
default: 0,
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'take',
|
||||
in: 'query',
|
||||
description: 'Number of records to take.',
|
||||
required: false,
|
||||
schema: new OA\Schema(
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
default: 10,
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'List application deployments by using the app uuid.',
|
||||
content: [
|
||||
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/Application'),
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function get_application_deployments(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'skip' => ['nullable', 'integer', 'min:0'],
|
||||
'take' => ['nullable', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
$app_uuid = $request->route('uuid', null);
|
||||
$skip = $request->get('skip', 0);
|
||||
$take = $request->get('take', 10);
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$servers = Server::whereTeamId($teamId)->get();
|
||||
|
||||
if (is_null($app_uuid)) {
|
||||
return response()->json(['message' => 'Application uuid is required'], 400);
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $app_uuid)->first();
|
||||
|
||||
if (is_null($application)) {
|
||||
return response()->json(['message' => 'Application not found'], 404);
|
||||
}
|
||||
$deployments = $application->deployments($skip, $take);
|
||||
|
||||
return response()->json($deployments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +267,18 @@ class ProjectController extends Controller
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Projects'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the project.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
description: 'Project updated.',
|
||||
|
||||
@@ -368,6 +368,20 @@ class SecurityController extends Controller
|
||||
response: 404,
|
||||
description: 'Private Key not found.',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
description: 'Private Key is in use and cannot be deleted.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Private Key is in use and cannot be deleted.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
)]
|
||||
public function delete_key(Request $request)
|
||||
@@ -384,6 +398,14 @@ class SecurityController extends Controller
|
||||
if (is_null($key)) {
|
||||
return response()->json(['message' => 'Private Key not found.'], 404);
|
||||
}
|
||||
|
||||
if ($key->isInUse()) {
|
||||
return response()->json([
|
||||
'message' => 'Private Key is in use and cannot be deleted.',
|
||||
'details' => 'This private key is currently being used by servers, applications, or Git integrations.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$key->forceDelete();
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -809,6 +809,6 @@ class ServersController extends Controller
|
||||
}
|
||||
ValidateServer::dispatch($server);
|
||||
|
||||
return response()->json(['message' => 'Validation started.']);
|
||||
return response()->json(['message' => 'Validation started.'], 201);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ServicesController extends Controller
|
||||
{
|
||||
@@ -88,8 +89,8 @@ class ServicesController extends Controller
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create',
|
||||
description: 'Create a one-click service',
|
||||
summary: 'Create service',
|
||||
description: 'Create a one-click / custom service',
|
||||
path: '/services',
|
||||
operationId: 'create-service',
|
||||
security: [
|
||||
@@ -102,7 +103,7 @@ class ServicesController extends Controller
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'type'],
|
||||
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
|
||||
properties: [
|
||||
'type' => [
|
||||
'description' => 'The one-click service type',
|
||||
@@ -204,6 +205,7 @@ class ServicesController extends Controller
|
||||
'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
|
||||
'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -211,7 +213,7 @@ class ServicesController extends Controller
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Create a service.',
|
||||
description: 'Service created successfully.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
@@ -237,7 +239,7 @@ class ServicesController extends Controller
|
||||
)]
|
||||
public function create_service(Request $request)
|
||||
{
|
||||
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy'];
|
||||
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -249,12 +251,13 @@ class ServicesController extends Controller
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'type' => 'string|required',
|
||||
'type' => 'string|required_without:docker_compose_raw',
|
||||
'docker_compose_raw' => 'string|required_without:type',
|
||||
'project_uuid' => 'string|required',
|
||||
'environment_name' => 'string|nullable',
|
||||
'environment_uuid' => 'string|nullable',
|
||||
'server_uuid' => 'string|required',
|
||||
'destination_uuid' => 'string',
|
||||
'destination_uuid' => 'string|nullable',
|
||||
'name' => 'string|max:255',
|
||||
'description' => 'string|nullable',
|
||||
'instant_deploy' => 'boolean',
|
||||
@@ -372,12 +375,19 @@ class ServicesController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
} else {
|
||||
return response()->json(['message' => 'Invalid service type.', 'valid_service_types' => $serviceKeys], 400);
|
||||
}
|
||||
return response()->json(['message' => 'Service not found.', 'valid_service_types' => $serviceKeys], 404);
|
||||
} elseif (filled($request->docker_compose_raw)) {
|
||||
|
||||
return response()->json(['message' => 'Invalid service type.'], 400);
|
||||
$service = new Service;
|
||||
$result = $this->upsert_service($request, $service, $teamId);
|
||||
if ($result instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse($result))->setStatusCode(201);
|
||||
} else {
|
||||
return response()->json(['message' => 'No service type or docker_compose_raw provided.'], 400);
|
||||
}
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
@@ -511,6 +521,220 @@ class ServicesController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update',
|
||||
description: 'Update service by UUID.',
|
||||
path: '/services/{uuid}',
|
||||
operationId: 'update-service-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Services'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Service updated.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'],
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The service name.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The service description.'],
|
||||
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
|
||||
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
|
||||
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID.'],
|
||||
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the service should be deployed instantly.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'Connect the service to the predefined docker network.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Service updated.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'description' => 'Service UUID.'],
|
||||
'domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Service domains.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
$result = $this->upsert_service($request, $service, $teamId);
|
||||
if ($result instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse($result))->setStatusCode(200);
|
||||
}
|
||||
|
||||
private function upsert_service(Request $request, Service $service, string $teamId)
|
||||
{
|
||||
$allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'project_uuid' => 'string|required',
|
||||
'environment_name' => 'string|nullable',
|
||||
'environment_uuid' => 'string|nullable',
|
||||
'server_uuid' => 'string|required',
|
||||
'destination_uuid' => 'string',
|
||||
'name' => 'string|max:255',
|
||||
'description' => 'string|nullable',
|
||||
'instant_deploy' => 'boolean',
|
||||
'connect_to_docker_network' => 'boolean',
|
||||
'docker_compose_raw' => 'string|required',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
$environmentUuid = $request->environment_uuid;
|
||||
$environmentName = $request->environment_name;
|
||||
if (blank($environmentUuid) && blank($environmentName)) {
|
||||
return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
|
||||
}
|
||||
$serverUuid = $request->server_uuid;
|
||||
$instantDeploy = $request->instant_deploy ?? false;
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
|
||||
if (! $project) {
|
||||
return response()->json(['message' => 'Project not found.'], 404);
|
||||
}
|
||||
$environment = $project->environments()->where('name', $environmentName)->first();
|
||||
if (! $environment) {
|
||||
$environment = $project->environments()->where('uuid', $environmentUuid)->first();
|
||||
}
|
||||
if (! $environment) {
|
||||
return response()->json(['message' => 'Environment not found.'], 404);
|
||||
}
|
||||
$server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
|
||||
if (! $server) {
|
||||
return response()->json(['message' => 'Server not found.'], 404);
|
||||
}
|
||||
$destinations = $server->destinations();
|
||||
if ($destinations->count() == 0) {
|
||||
return response()->json(['message' => 'Server has no destinations.'], 400);
|
||||
}
|
||||
if ($destinations->count() > 1 && ! $request->has('destination_uuid')) {
|
||||
return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
|
||||
}
|
||||
$destination = $destinations->first();
|
||||
if (! isBase64Encoded($request->docker_compose_raw)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
|
||||
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerCompose = base64_decode($request->docker_compose_raw);
|
||||
$dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
|
||||
$connectToDockerNetwork = $request->connect_to_docker_network ?? false;
|
||||
|
||||
$service->name = $request->name ?? null;
|
||||
$service->description = $request->description ?? null;
|
||||
$service->docker_compose_raw = $dockerComposeRaw;
|
||||
$service->environment_id = $environment->id;
|
||||
$service->server_id = $server->id;
|
||||
$service->destination_id = $destination->id;
|
||||
$service->destination_type = $destination->getMorphClass();
|
||||
$service->connect_to_docker_network = $connectToDockerNetwork;
|
||||
$service->save();
|
||||
|
||||
$service->parse();
|
||||
if ($instantDeploy) {
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
|
||||
$domains = $service->applications()->get()->pluck('fqdn')->sort();
|
||||
$domains = $domains->map(function ($domain) {
|
||||
if (count(explode(':', $domain)) > 2) {
|
||||
return str($domain)->beforeLast(':')->value();
|
||||
}
|
||||
|
||||
return $domain;
|
||||
})->values();
|
||||
|
||||
return [
|
||||
'uuid' => $service->uuid,
|
||||
'domains' => $domains,
|
||||
];
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Envs',
|
||||
description: 'List all envs by service UUID.',
|
||||
@@ -1204,6 +1428,15 @@ class ServicesController extends Controller
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'latest',
|
||||
in: 'query',
|
||||
description: 'Pull latest images.',
|
||||
schema: new OA\Schema(
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -1249,7 +1482,8 @@ class ServicesController extends Controller
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
RestartService::dispatch($service);
|
||||
$pullLatest = $request->boolean('latest');
|
||||
RestartService::dispatch($service, $pullLatest);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
|
||||
@@ -54,7 +54,7 @@ class Controller extends BaseController
|
||||
'email' => Str::lower($arrayOfRequest['email']),
|
||||
]);
|
||||
$type = set_transanctional_email_settings();
|
||||
if (! $type) {
|
||||
if (blank($type)) {
|
||||
return response()->json(['message' => 'Transactional emails are not active'], 400);
|
||||
}
|
||||
$request->validate([Fortify::email() => 'required|email']);
|
||||
@@ -144,7 +144,7 @@ class Controller extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function revoke_invitation()
|
||||
public function revokeInvitation()
|
||||
{
|
||||
$invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
|
||||
$user = User::whereEmail($invitation->email)->firstOrFail();
|
||||
|
||||
@@ -37,7 +37,7 @@ class Bitbucket extends Controller
|
||||
$headers = $request->headers->all();
|
||||
$x_bitbucket_token = data_get($headers, 'x-hub-signature.0', '');
|
||||
$x_bitbucket_event = data_get($headers, 'x-event-key.0', '');
|
||||
$handled_events = collect(['repo:push', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']);
|
||||
$handled_events = collect(['repo:push', 'pullrequest:updated', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']);
|
||||
if (! $handled_events->contains($x_bitbucket_event)) {
|
||||
return response([
|
||||
'status' => 'failed',
|
||||
@@ -48,6 +48,7 @@ class Bitbucket extends Controller
|
||||
$branch = data_get($payload, 'push.changes.0.new.name');
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
$commit = data_get($payload, 'push.changes.0.new.target.hash');
|
||||
|
||||
if (! $branch) {
|
||||
return response([
|
||||
'status' => 'failed',
|
||||
@@ -55,7 +56,7 @@ class Bitbucket extends Controller
|
||||
]);
|
||||
}
|
||||
}
|
||||
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
|
||||
if ($x_bitbucket_event === 'pullrequest:updated' || $x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
|
||||
$branch = data_get($payload, 'pullrequest.destination.branch.name');
|
||||
$base_branch = data_get($payload, 'pullrequest.source.branch.name');
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
@@ -99,18 +100,26 @@ class Bitbucket extends Controller
|
||||
if ($x_bitbucket_event === 'repo:push') {
|
||||
if ($application->isDeployable()) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
commit: $commit,
|
||||
force_rebuild: false,
|
||||
is_webhook: true
|
||||
);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
if ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Deployment queued.',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
@@ -119,7 +128,7 @@ class Bitbucket extends Controller
|
||||
]);
|
||||
}
|
||||
}
|
||||
if ($x_bitbucket_event === 'pullrequest:created') {
|
||||
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:updated') {
|
||||
if ($application->isPRDeployable()) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
@@ -142,7 +151,7 @@ class Bitbucket extends Controller
|
||||
]);
|
||||
}
|
||||
}
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
@@ -151,11 +160,19 @@ class Bitbucket extends Controller
|
||||
is_webhook: true,
|
||||
git_type: 'bitbucket'
|
||||
);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
if ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
|
||||
@@ -116,19 +116,27 @@ class Gitea extends Controller
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: false,
|
||||
commit: data_get($payload, 'after', 'HEAD'),
|
||||
is_webhook: true,
|
||||
);
|
||||
$return_payloads->push([
|
||||
'status' => 'success',
|
||||
'message' => 'Deployment queued.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
if ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'status' => 'success',
|
||||
'message' => 'Deployment queued.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$paths = str($application->watch_paths)->explode("\n");
|
||||
$return_payloads->push([
|
||||
@@ -152,7 +160,7 @@ class Gitea extends Controller
|
||||
}
|
||||
}
|
||||
if ($x_gitea_event === 'pull_request') {
|
||||
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
|
||||
if ($action === 'opened' || $action === 'synchronized' || $action === 'reopened') {
|
||||
if ($application->isPRDeployable()) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
@@ -175,7 +183,7 @@ class Gitea extends Controller
|
||||
]);
|
||||
}
|
||||
}
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
@@ -184,11 +192,19 @@ class Gitea extends Controller
|
||||
is_webhook: true,
|
||||
git_type: 'gitea'
|
||||
);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
if ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
@@ -202,7 +218,6 @@ class Gitea extends Controller
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
// ray('Stopping container: ' . $container_name);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
|
||||
@@ -122,19 +122,29 @@ class Github extends Controller
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: false,
|
||||
commit: data_get($payload, 'after', 'HEAD'),
|
||||
is_webhook: true,
|
||||
);
|
||||
$return_payloads->push([
|
||||
'status' => 'success',
|
||||
'message' => 'Deployment queued.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
if ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Deployment queued.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $result['deployment_uuid'],
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$paths = str($application->watch_paths)->explode("\n");
|
||||
$return_payloads->push([
|
||||
@@ -181,7 +191,8 @@ class Github extends Controller
|
||||
]);
|
||||
}
|
||||
}
|
||||
queue_application_deployment(
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
@@ -190,11 +201,19 @@ class Github extends Controller
|
||||
is_webhook: true,
|
||||
git_type: 'github'
|
||||
);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
if ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
@@ -208,7 +227,6 @@ class Github extends Controller
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
// ray('Stopping container: ' . $container_name);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
@@ -342,7 +360,7 @@ class Github extends Controller
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
commit: data_get($payload, 'after', 'HEAD'),
|
||||
@@ -350,10 +368,11 @@ class Github extends Controller
|
||||
is_webhook: true,
|
||||
);
|
||||
$return_payloads->push([
|
||||
'status' => 'success',
|
||||
'message' => 'Deployment queued.',
|
||||
'status' => $result['status'],
|
||||
'message' => $result['message'],
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $result['deployment_uuid'],
|
||||
]);
|
||||
} else {
|
||||
$paths = str($application->watch_paths)->explode("\n");
|
||||
@@ -390,7 +409,7 @@ class Github extends Controller
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
}
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
@@ -399,11 +418,19 @@ class Github extends Controller
|
||||
is_webhook: true,
|
||||
git_type: 'github'
|
||||
);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
if ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
|
||||
@@ -142,19 +142,28 @@ class Gitlab extends Controller
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
commit: data_get($payload, 'after', 'HEAD'),
|
||||
force_rebuild: false,
|
||||
is_webhook: true,
|
||||
);
|
||||
$return_payloads->push([
|
||||
'status' => 'success',
|
||||
'message' => 'Deployment queued.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
if ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'status' => $result['status'],
|
||||
'message' => $result['message'],
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'status' => 'success',
|
||||
'message' => 'Deployment queued.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$paths = str($application->watch_paths)->explode("\n");
|
||||
$return_payloads->push([
|
||||
@@ -201,7 +210,7 @@ class Gitlab extends Controller
|
||||
]);
|
||||
}
|
||||
}
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
@@ -210,11 +219,19 @@ class Gitlab extends Controller
|
||||
is_webhook: true,
|
||||
git_type: 'gitlab'
|
||||
);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview Deployment queued',
|
||||
]);
|
||||
if ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview Deployment queued',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
@@ -227,7 +244,6 @@ class Gitlab extends Controller
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
// ray('Stopping container: ' . $container_name);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Jobs;
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Events\ApplicationStatusChanged;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\ApplicationPreview;
|
||||
@@ -19,6 +19,7 @@ use App\Notifications\Application\DeploymentFailed;
|
||||
use App\Notifications\Application\DeploymentSuccess;
|
||||
use App\Traits\ExecuteRemoteCommand;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
@@ -26,7 +27,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
@@ -253,6 +253,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Make sure the private key is stored in the filesystem
|
||||
$this->server->privateKey->storeInFileSystem();
|
||||
|
||||
// Generate custom host<->ip mapping
|
||||
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
||||
|
||||
@@ -325,15 +328,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
$this->write_deployment_configurations();
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
|
||||
'hidden' => true,
|
||||
'ignore_errors' => true,
|
||||
]
|
||||
);
|
||||
$this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}");
|
||||
$this->graceful_shutdown_container($this->deployment_uuid);
|
||||
|
||||
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
|
||||
ServiceStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,9 +361,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private function post_deployment()
|
||||
{
|
||||
if ($this->server->isProxyShouldRun()) {
|
||||
GetContainersStatus::dispatch($this->server);
|
||||
}
|
||||
GetContainersStatus::dispatch($this->server);
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
if ($this->pull_request_id !== 0) {
|
||||
if ($this->application->is_github_based()) {
|
||||
@@ -509,7 +505,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->env_filename) {
|
||||
$command .= " --env-file {$this->workdir}/{$this->env_filename}";
|
||||
}
|
||||
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull";
|
||||
if ($this->force_rebuild) {
|
||||
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull --no-cache";
|
||||
} else {
|
||||
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull";
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
|
||||
);
|
||||
@@ -900,100 +900,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id');
|
||||
}
|
||||
$ports = $this->application->main_port();
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$this->env_filename = ".env-pr-$this->pull_request_id";
|
||||
// Add SOURCE_COMMIT if not exists
|
||||
if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) {
|
||||
if (! is_null($this->commit)) {
|
||||
$envs->push("SOURCE_COMMIT={$this->commit}");
|
||||
} else {
|
||||
$envs->push('SOURCE_COMMIT=unknown');
|
||||
}
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
|
||||
$envs->push("COOLIFY_FQDN={$this->preview->fqdn}");
|
||||
$envs->push("COOLIFY_DOMAIN_URL={$this->preview->fqdn}");
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
|
||||
$url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
|
||||
$envs->push("COOLIFY_URL={$url}");
|
||||
$envs->push("COOLIFY_DOMAIN_FQDN={$url}");
|
||||
}
|
||||
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
|
||||
$envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}");
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
|
||||
}
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables_preview);
|
||||
|
||||
foreach ($sorted_environment_variables_preview as $env) {
|
||||
$real_value = $env->real_value;
|
||||
if ($env->version === '4.0.0-beta.239') {
|
||||
$real_value = $env->real_value;
|
||||
} else {
|
||||
if ($env->is_literal || $env->is_multiline) {
|
||||
$real_value = '\''.$real_value.'\'';
|
||||
} else {
|
||||
$real_value = escapeEnvVariables($env->real_value);
|
||||
}
|
||||
}
|
||||
$envs->push($env->key.'='.$real_value);
|
||||
}
|
||||
// Add PORT if not exists, use the first port as default
|
||||
if ($this->build_pack !== 'dockercompose') {
|
||||
if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
|
||||
$envs->push("PORT={$ports[0]}");
|
||||
}
|
||||
}
|
||||
// Add HOST if not exists
|
||||
if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) {
|
||||
$envs->push('HOST=0.0.0.0');
|
||||
}
|
||||
} else {
|
||||
$coolify_envs = $this->generate_coolify_env_variables();
|
||||
$coolify_envs->each(function ($item, $key) use ($envs) {
|
||||
$envs->push($key.'='.$item);
|
||||
});
|
||||
if ($this->pull_request_id === 0) {
|
||||
$this->env_filename = '.env';
|
||||
// Add SOURCE_COMMIT if not exists
|
||||
if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) {
|
||||
if (! is_null($this->commit)) {
|
||||
$envs->push("SOURCE_COMMIT={$this->commit}");
|
||||
} else {
|
||||
$envs->push('SOURCE_COMMIT=unknown');
|
||||
}
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
|
||||
if ((int) $this->application->compose_parsing_version >= 3) {
|
||||
$envs->push("COOLIFY_URL={$this->application->fqdn}");
|
||||
} else {
|
||||
$envs->push("COOLIFY_FQDN={$this->application->fqdn}");
|
||||
}
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
|
||||
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
|
||||
if ((int) $this->application->compose_parsing_version >= 3) {
|
||||
$envs->push("COOLIFY_FQDN={$url}");
|
||||
} else {
|
||||
$envs->push("COOLIFY_URL={$url}");
|
||||
}
|
||||
}
|
||||
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
|
||||
$envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}");
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
|
||||
}
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables);
|
||||
|
||||
foreach ($sorted_environment_variables as $env) {
|
||||
$real_value = $env->real_value;
|
||||
@@ -1018,6 +930,32 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) {
|
||||
$envs->push('HOST=0.0.0.0');
|
||||
}
|
||||
} else {
|
||||
$this->env_filename = ".env-pr-$this->pull_request_id";
|
||||
foreach ($sorted_environment_variables_preview as $env) {
|
||||
$real_value = $env->real_value;
|
||||
if ($env->version === '4.0.0-beta.239') {
|
||||
$real_value = $env->real_value;
|
||||
} else {
|
||||
if ($env->is_literal || $env->is_multiline) {
|
||||
$real_value = '\''.$real_value.'\'';
|
||||
} else {
|
||||
$real_value = escapeEnvVariables($env->real_value);
|
||||
}
|
||||
}
|
||||
$envs->push($env->key.'='.$real_value);
|
||||
}
|
||||
// Add PORT if not exists, use the first port as default
|
||||
if ($this->build_pack !== 'dockercompose') {
|
||||
if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
|
||||
$envs->push("PORT={$ports[0]}");
|
||||
}
|
||||
}
|
||||
// Add HOST if not exists
|
||||
if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) {
|
||||
$envs->push('HOST=0.0.0.0');
|
||||
}
|
||||
|
||||
}
|
||||
if ($envs->isEmpty()) {
|
||||
$this->env_filename = null;
|
||||
@@ -1204,11 +1142,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->application->custom_healthcheck_found) {
|
||||
$this->application_deployment_queue->addLogEntry('Custom healthcheck found, skipping default healthcheck.');
|
||||
}
|
||||
// ray('New container name: ', $this->container_name);
|
||||
if ($this->container_name) {
|
||||
$counter = 1;
|
||||
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
|
||||
if ($this->full_healthcheck_url) {
|
||||
if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
|
||||
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
|
||||
@@ -1363,13 +1300,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
'command' => "docker rm -f {$this->deployment_uuid}",
|
||||
'ignore_errors' => true,
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
$this->graceful_shutdown_container($this->deployment_uuid);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
$runCommand,
|
||||
@@ -1401,13 +1332,15 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
foreach ($destination_ids as $destination_id) {
|
||||
$destination = StandaloneDocker::find($destination_id);
|
||||
if (! $destination) {
|
||||
continue;
|
||||
}
|
||||
$server = $destination->server;
|
||||
if ($server->team_id !== $this->mainServer->team_id) {
|
||||
$this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!");
|
||||
|
||||
continue;
|
||||
}
|
||||
// ray('Deploying to additional destination: ', $server->name);
|
||||
$deployment_uuid = new Cuid2;
|
||||
queue_application_deployment(
|
||||
deployment_uuid: $deployment_uuid,
|
||||
@@ -1445,6 +1378,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private function check_git_if_build_needed()
|
||||
{
|
||||
if (is_object($this->source) && $this->source->getMorphClass() === \App\Models\GithubApp::class && $this->source->is_public === false) {
|
||||
$repository = githubApi($this->source, "repos/{$this->customRepository}");
|
||||
$data = data_get($repository, 'data');
|
||||
$repository_project_id = data_get($data, 'id');
|
||||
if (isset($repository_project_id)) {
|
||||
if (blank($this->application->repository_project_id) || $this->application->repository_project_id !== $repository_project_id) {
|
||||
$this->application->repository_project_id = $repository_project_id;
|
||||
$this->application->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->generate_git_import_commands();
|
||||
$local_branch = $this->branch;
|
||||
if ($this->pull_request_id !== 0) {
|
||||
@@ -1634,20 +1578,134 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');
|
||||
}
|
||||
|
||||
private function generate_coolify_env_variables(): Collection
|
||||
{
|
||||
$coolify_envs = collect([]);
|
||||
$local_branch = $this->branch;
|
||||
if ($this->pull_request_id !== 0) {
|
||||
// Add SOURCE_COMMIT if not exists
|
||||
if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) {
|
||||
if (! is_null($this->commit)) {
|
||||
$coolify_envs->put('SOURCE_COMMIT', $this->commit);
|
||||
} else {
|
||||
$coolify_envs->put('SOURCE_COMMIT', 'unknown');
|
||||
}
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
|
||||
if ((int) $this->application->compose_parsing_version >= 3) {
|
||||
$coolify_envs->put('COOLIFY_URL', $this->preview->fqdn);
|
||||
} else {
|
||||
$coolify_envs->put('COOLIFY_FQDN', $this->preview->fqdn);
|
||||
}
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
|
||||
$url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
|
||||
if ((int) $this->application->compose_parsing_version >= 3) {
|
||||
$coolify_envs->put('COOLIFY_FQDN', $url);
|
||||
} else {
|
||||
$coolify_envs->put('COOLIFY_URL', $url);
|
||||
}
|
||||
}
|
||||
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||
$coolify_envs->put('COOLIFY_BRANCH', $local_branch);
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
|
||||
$coolify_envs->put('COOLIFY_RESOURCE_UUID', $this->application->uuid);
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||
$coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name);
|
||||
}
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables_preview);
|
||||
|
||||
} else {
|
||||
// Add SOURCE_COMMIT if not exists
|
||||
if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) {
|
||||
if (! is_null($this->commit)) {
|
||||
$coolify_envs->put('SOURCE_COMMIT', $this->commit);
|
||||
} else {
|
||||
$coolify_envs->put('SOURCE_COMMIT', 'unknown');
|
||||
}
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
|
||||
if ((int) $this->application->compose_parsing_version >= 3) {
|
||||
$coolify_envs->put('COOLIFY_URL', $this->application->fqdn);
|
||||
} else {
|
||||
$coolify_envs->put('COOLIFY_FQDN', $this->application->fqdn);
|
||||
}
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
|
||||
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
|
||||
if ((int) $this->application->compose_parsing_version >= 3) {
|
||||
$coolify_envs->put('COOLIFY_FQDN', $url);
|
||||
} else {
|
||||
$coolify_envs->put('COOLIFY_URL', $url);
|
||||
}
|
||||
}
|
||||
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||
$coolify_envs->put('COOLIFY_BRANCH', $local_branch);
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
|
||||
$coolify_envs->put('COOLIFY_RESOURCE_UUID', $this->application->uuid);
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||
$coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name);
|
||||
}
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables);
|
||||
|
||||
}
|
||||
|
||||
return $coolify_envs;
|
||||
}
|
||||
|
||||
private function generate_env_variables()
|
||||
{
|
||||
$this->env_args = collect([]);
|
||||
$this->env_args->put('SOURCE_COMMIT', $this->commit);
|
||||
$coolify_envs = $this->generate_coolify_env_variables();
|
||||
if ($this->pull_request_id === 0) {
|
||||
foreach ($this->application->build_environment_variables as $env) {
|
||||
if (! is_null($env->real_value)) {
|
||||
$this->env_args->put($env->key, $env->real_value);
|
||||
if (str($env->real_value)->startsWith('$')) {
|
||||
$variable_key = str($env->real_value)->after('$');
|
||||
if ($variable_key->startsWith('COOLIFY_')) {
|
||||
$variable = $coolify_envs->get($variable_key->value());
|
||||
if (filled($variable)) {
|
||||
$this->env_args->prepend($variable, $variable_key->value());
|
||||
}
|
||||
} else {
|
||||
$variable = $this->application->environment_variables()->where('key', $variable_key)->first();
|
||||
if ($variable) {
|
||||
$this->env_args->prepend($variable->real_value, $env->key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->build_environment_variables_preview as $env) {
|
||||
if (! is_null($env->real_value)) {
|
||||
$this->env_args->put($env->key, $env->real_value);
|
||||
if (str($env->real_value)->startsWith('$')) {
|
||||
$variable_key = str($env->real_value)->after('$');
|
||||
if ($variable_key->startsWith('COOLIFY_')) {
|
||||
$variable = $coolify_envs->get($variable_key->value());
|
||||
if (filled($variable)) {
|
||||
$this->env_args->prepend($variable, $variable_key->value());
|
||||
}
|
||||
} else {
|
||||
$variable = $this->application->environment_variables_preview()->where('key', $variable_key)->first();
|
||||
if ($variable) {
|
||||
$this->env_args->prepend($variable->real_value, $env->key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1672,25 +1730,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$labels = $labels->filter(function ($value, $key) {
|
||||
return ! Str::startsWith($value, 'coolify.');
|
||||
});
|
||||
$found_caddy_labels = $labels->filter(function ($value, $key) {
|
||||
return Str::startsWith($value, 'caddy_');
|
||||
});
|
||||
if ($found_caddy_labels->count() === 0) {
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$domains = str(data_get($this->preview, 'fqdn'))->explode(',');
|
||||
} else {
|
||||
$domains = str(data_get($this->application, 'fqdn'))->explode(',');
|
||||
}
|
||||
$labels = $labels->merge(fqdnLabelsForCaddy(
|
||||
network: $this->application->destination->network,
|
||||
uuid: $this->application->uuid,
|
||||
domains: $domains,
|
||||
onlyPort: $onlyPort,
|
||||
is_force_https_enabled: $this->application->isForceHttpsEnabled(),
|
||||
is_gzip_enabled: $this->application->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $this->application->isStripprefixEnabled()
|
||||
));
|
||||
}
|
||||
$this->application->custom_labels = base64_encode($labels->implode("\n"));
|
||||
$this->application->save();
|
||||
} else {
|
||||
@@ -1716,8 +1755,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'save' => 'dockerfile_from_repo',
|
||||
'ignore_errors' => true,
|
||||
]);
|
||||
$dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
|
||||
$this->application->parseHealthcheckFromDockerfile($dockerfile);
|
||||
$this->application->parseHealthcheckFromDockerfile($this->saved_outputs->get('dockerfile_from_repo'));
|
||||
}
|
||||
$custom_network_aliases = [];
|
||||
if (is_array($this->application->custom_network_aliases) && count($this->application->custom_network_aliases) > 0) {
|
||||
$custom_network_aliases = $this->application->custom_network_aliases;
|
||||
}
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
@@ -1728,9 +1770,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'expose' => $ports,
|
||||
'networks' => [
|
||||
$this->destination->network => [
|
||||
'aliases' => [
|
||||
$this->container_name,
|
||||
],
|
||||
'aliases' => array_merge(
|
||||
[$this->container_name],
|
||||
$custom_network_aliases
|
||||
),
|
||||
],
|
||||
],
|
||||
'mem_limit' => $this->application->limits_memory,
|
||||
@@ -2020,11 +2063,17 @@ LABEL coolify.deploymentId={$this->deployment_uuid}
|
||||
COPY . .
|
||||
RUN rm -f /usr/share/nginx/html/nginx.conf
|
||||
RUN rm -f /usr/share/nginx/html/Dockerfile
|
||||
RUN rm -f /usr/share/nginx/html/docker-compose.yaml
|
||||
RUN rm -f /usr/share/nginx/html/.env
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
|
||||
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
|
||||
} else {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
if ($this->application->settings->is_spa) {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration('spa'));
|
||||
} else {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($this->application->build_pack === 'nixpacks') {
|
||||
@@ -2091,7 +2140,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
|
||||
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
|
||||
} else {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
if ($this->application->settings->is_spa) {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration('spa'));
|
||||
} else {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
}
|
||||
}
|
||||
}
|
||||
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
@@ -2200,43 +2253,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
|
||||
}
|
||||
|
||||
private function graceful_shutdown_container(string $containerName, int $timeout = 300)
|
||||
private function graceful_shutdown_container(string $containerName, int $timeout = 30)
|
||||
{
|
||||
try {
|
||||
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
|
||||
$startTime = time();
|
||||
while ($process->running()) {
|
||||
if (time() - $startTime >= $timeout) {
|
||||
$this->execute_remote_command(
|
||||
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
$isRunning = $this->execute_remote_command(
|
||||
["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
) === 'true';
|
||||
|
||||
if ($isRunning) {
|
||||
$this->execute_remote_command(
|
||||
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
}
|
||||
} catch (\Exception $error) {
|
||||
$this->execute_remote_command(
|
||||
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
||||
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
} catch (Exception $error) {
|
||||
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
|
||||
}
|
||||
|
||||
$this->remove_container($containerName);
|
||||
}
|
||||
|
||||
private function remove_container(string $containerName)
|
||||
{
|
||||
$this->execute_remote_command(
|
||||
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
}
|
||||
|
||||
private function stop_running_container(bool $force = false)
|
||||
@@ -2281,7 +2307,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
} else {
|
||||
if ($this->use_build_server) {
|
||||
$this->execute_remote_command(
|
||||
["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true],
|
||||
["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --pull always --build -d", 'hidden' => true],
|
||||
);
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
@@ -2408,20 +2434,21 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
private function next(string $status)
|
||||
{
|
||||
queue_next_deployment($this->application);
|
||||
// If the deployment is cancelled by the user, don't update the status
|
||||
if (
|
||||
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value &&
|
||||
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value
|
||||
) {
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => $status,
|
||||
]);
|
||||
}
|
||||
|
||||
// Never allow changing status from FAILED or CANCELLED_BY_USER to anything else
|
||||
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
|
||||
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => $status,
|
||||
]);
|
||||
|
||||
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||
if (! $this->only_this_server) {
|
||||
$this->deploy_to_additional_destinations();
|
||||
|
||||
@@ -20,7 +20,7 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
|
||||
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("'.config('constants.coolify.registry_url').'/coollabsio/coolify-helper")))\''], $this->server, false);
|
||||
$containerIds = collect(json_decode($containers))->pluck('ID');
|
||||
if ($containerIds->count() > 0) {
|
||||
foreach ($containerIds as $containerId) {
|
||||
|
||||
@@ -17,11 +17,13 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 60;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()];
|
||||
return [(new WithoutOverlapping('cleanup-instance-stuffs'))->expireAfter(60)->dontRelease()];
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
|
||||
@@ -54,6 +54,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public ?string $postgres_password = null;
|
||||
|
||||
public ?string $mongo_root_username = null;
|
||||
|
||||
public ?string $mongo_root_password = null;
|
||||
|
||||
public ?S3Storage $s3 = null;
|
||||
|
||||
public function __construct(public ScheduledDatabaseBackup $backup)
|
||||
@@ -189,6 +193,40 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
throw new \Exception('MARIADB_DATABASE or MYSQL_DATABASE not found');
|
||||
}
|
||||
}
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
$databasesToBackup = ['*'];
|
||||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
|
||||
// Try to extract MongoDB credentials from environment variables
|
||||
try {
|
||||
$commands = [];
|
||||
$commands[] = "docker exec $this->container_name env | grep MONGO_INITDB_";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
|
||||
if (filled($envs)) {
|
||||
$envs = str($envs)->explode("\n");
|
||||
$rootPassword = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MONGO_INITDB_ROOT_PASSWORD=');
|
||||
})->first();
|
||||
if ($rootPassword) {
|
||||
$this->mongo_root_password = str($rootPassword)->after('MONGO_INITDB_ROOT_PASSWORD=')->value();
|
||||
}
|
||||
$rootUsername = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MONGO_INITDB_ROOT_USERNAME=');
|
||||
})->first();
|
||||
if ($rootUsername) {
|
||||
$this->mongo_root_username = str($rootUsername)->after('MONGO_INITDB_ROOT_USERNAME=')->value();
|
||||
}
|
||||
}
|
||||
\Log::info('MongoDB credentials extracted from environment', [
|
||||
'has_username' => filled($this->mongo_root_username),
|
||||
'has_password' => filled($this->mongo_root_password),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning('Failed to extract MongoDB environment variables', ['error' => $e->getMessage()]);
|
||||
// Continue without env vars - will be handled in backup_standalone_mongodb method
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$databaseName = str($this->database->name)->slug()->value();
|
||||
@@ -200,7 +238,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (blank($databasesToBackup)) {
|
||||
if (str($databaseType)->contains('postgres')) {
|
||||
$databasesToBackup = [$this->database->postgres_db];
|
||||
} elseif (str($databaseType)->contains('mongodb')) {
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
$databasesToBackup = ['*'];
|
||||
} elseif (str($databaseType)->contains('mysql')) {
|
||||
$databasesToBackup = [$this->database->mysql_database];
|
||||
@@ -214,10 +252,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
// Format: db1,db2,db3
|
||||
$databasesToBackup = explode(',', $databasesToBackup);
|
||||
$databasesToBackup = array_map('trim', $databasesToBackup);
|
||||
} elseif (str($databaseType)->contains('mongodb')) {
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
// Format: db1:collection1,collection2|db2:collection3,collection4
|
||||
$databasesToBackup = explode('|', $databasesToBackup);
|
||||
$databasesToBackup = array_map('trim', $databasesToBackup);
|
||||
// Only explode if it's a string, not if it's already an array
|
||||
if (is_string($databasesToBackup)) {
|
||||
$databasesToBackup = explode('|', $databasesToBackup);
|
||||
$databasesToBackup = array_map('trim', $databasesToBackup);
|
||||
}
|
||||
} elseif (str($databaseType)->contains('mysql')) {
|
||||
// Format: db1,db2,db3
|
||||
$databasesToBackup = explode(',', $databasesToBackup);
|
||||
@@ -252,7 +293,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
]);
|
||||
$this->backup_standalone_postgresql($database);
|
||||
} elseif (str($databaseType)->contains('mongodb')) {
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
if ($database === '*') {
|
||||
$database = 'all';
|
||||
$databaseName = 'all';
|
||||
@@ -343,12 +384,23 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
try {
|
||||
$url = $this->database->internal_db_url;
|
||||
if (blank($url)) {
|
||||
// For service-based MongoDB, try to build URL from environment variables
|
||||
if (filled($this->mongo_root_username) && filled($this->mongo_root_password)) {
|
||||
// Use container name instead of server IP for service-based MongoDB
|
||||
$url = "mongodb://{$this->mongo_root_username}:{$this->mongo_root_password}@{$this->container_name}:27017";
|
||||
} else {
|
||||
// If no environment variables are available, throw an exception
|
||||
throw new \Exception('MongoDB credentials not found. Ensure MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD environment variables are available in the container.');
|
||||
}
|
||||
}
|
||||
\Log::info('MongoDB backup URL configured', ['has_url' => filled($url), 'using_env_vars' => blank($this->database->internal_db_url)]);
|
||||
if ($databaseWithCollections === 'all') {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
}
|
||||
} else {
|
||||
if (str($databaseWithCollections)->contains(':')) {
|
||||
@@ -361,15 +413,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if ($collectionsToExclude->count() === 0) {
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $databaseName --gzip --archive > $this->backup_location";
|
||||
}
|
||||
} else {
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,7 +442,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
$backupCommand = 'docker exec';
|
||||
if ($this->postgres_password) {
|
||||
$backupCommand .= " -e PGPASSWORD=$this->postgres_password";
|
||||
$backupCommand .= " -e PGPASSWORD=\"{$this->postgres_password}\"";
|
||||
}
|
||||
if ($this->backup->dump_all) {
|
||||
$backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
|
||||
@@ -415,9 +467,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
try {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if ($this->backup->dump_all) {
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" $database > $this->backup_location";
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
@@ -435,9 +487,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
try {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if ($this->backup->dump_all) {
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" $database > $this->backup_location";
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
@@ -484,6 +536,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
$fullImageName = $this->getFullImageName();
|
||||
|
||||
$containerExists = instant_remote_process(["docker ps -a -q -f name=backup-of-{$this->backup->uuid}"], $this->server, false);
|
||||
if (filled($containerExists)) {
|
||||
instant_remote_process(["docker rm -f backup-of-{$this->backup->uuid}"], $this->server, false);
|
||||
}
|
||||
|
||||
if (isDev()) {
|
||||
if ($this->database->name === 'coolify-db') {
|
||||
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
|
||||
@@ -495,7 +552,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
|
||||
}
|
||||
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
|
||||
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key \"$secret\"";
|
||||
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
|
||||
instant_remote_process($commands, $this->server);
|
||||
|
||||
|
||||
@@ -42,10 +42,8 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$persistentStorages = collect();
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
$persistentStorages = $this->resource?->persistentStorages()?->get();
|
||||
StopApplication::run($this->resource, previewDeployments: true);
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
@@ -56,44 +54,52 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
case 'standalone-keydb':
|
||||
case 'standalone-dragonfly':
|
||||
case 'standalone-clickhouse':
|
||||
$persistentStorages = $this->resource?->persistentStorages()?->get();
|
||||
StopDatabase::run($this->resource, true);
|
||||
break;
|
||||
case 'service':
|
||||
StopService::run($this->resource, true);
|
||||
DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
|
||||
break;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->deleteVolumes && $this->resource->type() !== 'service') {
|
||||
$this->resource?->delete_volumes($persistentStorages);
|
||||
}
|
||||
if ($this->deleteConfigurations) {
|
||||
$this->resource?->delete_configurations();
|
||||
$this->resource->deleteConfigurations();
|
||||
}
|
||||
if ($this->deleteVolumes) {
|
||||
$this->resource->deleteVolumes();
|
||||
$this->resource->persistentStorages()->delete();
|
||||
}
|
||||
$this->resource->fileStorages()->delete();
|
||||
|
||||
$isDatabase = $this->resource instanceof StandalonePostgresql
|
||||
|| $this->resource instanceof StandaloneRedis
|
||||
|| $this->resource instanceof StandaloneMongodb
|
||||
|| $this->resource instanceof StandaloneMysql
|
||||
|| $this->resource instanceof StandaloneMariadb
|
||||
|| $this->resource instanceof StandaloneKeydb
|
||||
|| $this->resource instanceof StandaloneDragonfly
|
||||
|| $this->resource instanceof StandaloneClickhouse;
|
||||
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
|
||||
if (($this->dockerCleanup || $isDatabase) && $server) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
|| $this->resource instanceof StandaloneRedis
|
||||
|| $this->resource instanceof StandaloneMongodb
|
||||
|| $this->resource instanceof StandaloneMysql
|
||||
|| $this->resource instanceof StandaloneMariadb
|
||||
|| $this->resource instanceof StandaloneKeydb
|
||||
|| $this->resource instanceof StandaloneDragonfly
|
||||
|| $this->resource instanceof StandaloneClickhouse;
|
||||
|
||||
if ($this->deleteConnectedNetworks && ! $isDatabase) {
|
||||
$this->resource?->delete_connected_networks($this->resource->uuid);
|
||||
if ($isDatabase) {
|
||||
$this->resource->sslCertificates()->delete();
|
||||
$this->resource->scheduledBackups()->delete();
|
||||
$this->resource->tags()->detach();
|
||||
}
|
||||
$this->resource->environment_variables()->delete();
|
||||
|
||||
if ($this->deleteConnectedNetworks && $this->resource->type() === 'application') {
|
||||
$this->resource->deleteConnectedNetworks();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->resource->forceDelete();
|
||||
if ($this->dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
|
||||
if ($server) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
}
|
||||
Artisan::queue('cleanup:stucked-resources');
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
|
||||
return [(new WithoutOverlapping('docker-cleanup-'.$this->server->uuid))->expireAfter(600)->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server, public bool $manualCleanup = false) {}
|
||||
|
||||
@@ -9,7 +9,6 @@ use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Server\StartLogDrain;
|
||||
use App\Actions\Shared\ComplexStatusCheck;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
@@ -71,7 +70,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
|
||||
return [(new WithoutOverlapping('push-server-update-'.$this->server->uuid))->expireAfter(30)->dontRelease()];
|
||||
}
|
||||
|
||||
public function backoff(): int
|
||||
@@ -122,7 +121,9 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) {
|
||||
return $application->additional_servers->count() > 0;
|
||||
});
|
||||
$this->allApplicationPreviewsIds = $this->previews->pluck('id');
|
||||
$this->allApplicationPreviewsIds = $this->previews->map(function ($preview) {
|
||||
return $preview->application_id.':'.$preview->pull_request_id;
|
||||
});
|
||||
$this->allDatabaseUuids = $this->databases->pluck('uuid');
|
||||
$this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid');
|
||||
$this->services->each(function ($service) {
|
||||
@@ -147,7 +148,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
if ($labels->has('coolify.applicationId')) {
|
||||
$applicationId = $labels->get('coolify.applicationId');
|
||||
$pullRequestId = data_get($labels, 'coolify.pullRequestId', '0');
|
||||
$pullRequestId = $labels->get('coolify.pullRequestId', '0');
|
||||
try {
|
||||
if ($pullRequestId === '0') {
|
||||
if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) {
|
||||
@@ -155,10 +156,11 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
$this->updateApplicationStatus($applicationId, $containerStatus);
|
||||
} else {
|
||||
if ($this->allApplicationPreviewsIds->contains($applicationId) && $this->isRunning($containerStatus)) {
|
||||
$this->foundApplicationPreviewsIds->push($applicationId);
|
||||
$previewKey = $applicationId.':'.$pullRequestId;
|
||||
if ($this->allApplicationPreviewsIds->contains($previewKey) && $this->isRunning($containerStatus)) {
|
||||
$this->foundApplicationPreviewsIds->push($previewKey);
|
||||
}
|
||||
$this->updateApplicationPreviewStatus($applicationId, $containerStatus);
|
||||
$this->updateApplicationPreviewStatus($applicationId, $pullRequestId, $containerStatus);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
@@ -211,18 +213,24 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (! $application) {
|
||||
return;
|
||||
}
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
if ($application->status !== $containerStatus) {
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus)
|
||||
private function updateApplicationPreviewStatus(string $applicationId, string $pullRequestId, string $containerStatus)
|
||||
{
|
||||
$application = $this->previews->where('id', $applicationId)->first();
|
||||
$application = $this->previews->where('application_id', $applicationId)
|
||||
->where('pull_request_id', $pullRequestId)
|
||||
->first();
|
||||
if (! $application) {
|
||||
return;
|
||||
}
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
if ($application->status !== $containerStatus) {
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function updateNotFoundApplicationStatus()
|
||||
@@ -232,8 +240,21 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$notFoundApplicationIds->each(function ($applicationId) {
|
||||
$application = Application::find($applicationId);
|
||||
if ($application) {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
// Don't mark as exited if already exited
|
||||
if (str($application->status)->startsWith('exited')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only protection: Verify we received any container data at all
|
||||
// If containers collection is completely empty, Sentinel might have failed
|
||||
if ($this->containers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($application->status !== 'exited') {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -243,11 +264,36 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
$notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds);
|
||||
if ($notFoundApplicationPreviewsIds->isNotEmpty()) {
|
||||
$notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) {
|
||||
$applicationPreview = ApplicationPreview::find($applicationPreviewId);
|
||||
$notFoundApplicationPreviewsIds->each(function ($previewKey) {
|
||||
// Parse the previewKey format "application_id:pull_request_id"
|
||||
$parts = explode(':', $previewKey);
|
||||
if (count($parts) !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
$applicationId = $parts[0];
|
||||
$pullRequestId = $parts[1];
|
||||
|
||||
$applicationPreview = $this->previews->where('application_id', $applicationId)
|
||||
->where('pull_request_id', $pullRequestId)
|
||||
->first();
|
||||
|
||||
if ($applicationPreview) {
|
||||
$applicationPreview->status = 'exited';
|
||||
$applicationPreview->save();
|
||||
// Don't mark as exited if already exited
|
||||
if (str($applicationPreview->status)->startsWith('exited')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only protection: Verify we received any container data at all
|
||||
// If containers collection is completely empty, Sentinel might have failed
|
||||
if ($this->containers->isEmpty()) {
|
||||
|
||||
return;
|
||||
}
|
||||
if ($applicationPreview->status !== 'exited') {
|
||||
$applicationPreview->status = 'exited';
|
||||
$applicationPreview->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -260,7 +306,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->foundProxy === false) {
|
||||
try {
|
||||
if (CheckProxy::run($this->server)) {
|
||||
StartProxy::run($this->server, false);
|
||||
StartProxy::run($this->server, async: false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
@@ -278,8 +324,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (! $database) {
|
||||
return;
|
||||
}
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
if ($database->status !== $containerStatus) {
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
}
|
||||
if ($this->isRunning($containerStatus) && $tcpProxy) {
|
||||
$tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) {
|
||||
return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running';
|
||||
@@ -299,8 +347,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$notFoundDatabaseUuids->each(function ($databaseUuid) {
|
||||
$database = $this->databases->where('uuid', $databaseUuid)->first();
|
||||
if ($database) {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
if ($database->status !== 'exited') {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
}
|
||||
if ($database->is_public) {
|
||||
StopDatabaseProxy::dispatch($database);
|
||||
}
|
||||
@@ -317,13 +367,20 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
if ($subType === 'application') {
|
||||
$application = $service->applications()->where('id', $subId)->first();
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
if ($application) {
|
||||
if ($application->status !== $containerStatus) {
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
} elseif ($subType === 'database') {
|
||||
$database = $service->databases()->where('id', $subId)->first();
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
} else {
|
||||
if ($database) {
|
||||
if ($database->status !== $containerStatus) {
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,8 +392,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$notFoundServiceApplicationIds->each(function ($serviceApplicationId) {
|
||||
$application = ServiceApplication::find($serviceApplicationId);
|
||||
if ($application) {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
if ($application->status !== 'exited') {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -344,8 +403,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) {
|
||||
$database = ServiceDatabase::find($serviceDatabaseId);
|
||||
if ($database) {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
if ($database->status !== 'exited') {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
78
app/Jobs/RegenerateSslCertJob.php
Normal file
78
app/Jobs/RegenerateSslCertJob.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Helpers\SSLHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\SslExpirationNotification;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RegenerateSslCertJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 3;
|
||||
|
||||
public $backoff = 60;
|
||||
|
||||
public function __construct(
|
||||
protected ?Team $team = null,
|
||||
protected ?int $server_id = null,
|
||||
protected bool $force_regeneration = false,
|
||||
) {}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$query = SslCertificate::query();
|
||||
|
||||
if ($this->server_id) {
|
||||
$query->where('server_id', $this->server_id);
|
||||
}
|
||||
|
||||
if (! $this->force_regeneration) {
|
||||
$query->where('valid_until', '<=', now()->addDays(14));
|
||||
}
|
||||
|
||||
$query->where('is_ca_certificate', false);
|
||||
|
||||
$regenerated = collect();
|
||||
|
||||
$query->cursor()->each(function ($certificate) use ($regenerated) {
|
||||
try {
|
||||
$caCert = SslCertificate::where('server_id', $certificate->server_id)
|
||||
->where('is_ca_certificate', true)
|
||||
->first();
|
||||
|
||||
if (! $caCert) {
|
||||
Log::error("No CA certificate found for server_id: {$certificate->server_id}");
|
||||
|
||||
return;
|
||||
}
|
||||
SSLHelper::generateSslCertificate(
|
||||
commonName: $certificate->common_name,
|
||||
subjectAlternativeNames: $certificate->subject_alternative_names,
|
||||
resourceType: $certificate->resource_type,
|
||||
resourceId: $certificate->resource_id,
|
||||
serverId: $certificate->server_id,
|
||||
configurationDir: $certificate->configuration_dir,
|
||||
mountPath: $certificate->mount_path,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
);
|
||||
$regenerated->push($certificate);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to regenerate SSL certificate: '.$e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
if ($regenerated->isNotEmpty()) {
|
||||
$this->team?->notify(new SslExpirationNotification($regenerated));
|
||||
}
|
||||
}
|
||||
}
|
||||
45
app/Jobs/RestartProxyJob.php
Normal file
45
app/Jobs/RestartProxyJob.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
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\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 60;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->expireAfter(60)->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
StopProxy::run($this->server);
|
||||
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
|
||||
StartProxy::run($this->server, force: true);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ class SendMessageToSlackJob implements ShouldQueue
|
||||
public function handle(): void
|
||||
{
|
||||
Http::post($this->webhookUrl, [
|
||||
'text' => $this->message->title,
|
||||
'blocks' => [
|
||||
[
|
||||
'type' => 'section',
|
||||
|
||||
@@ -28,7 +28,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
|
||||
return [(new WithoutOverlapping('server-check-'.$this->server->uuid))->expireAfter(60)->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
@@ -68,7 +68,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
StartProxy::run($this->server, async: false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
68
app/Jobs/ServerPatchCheckJob.php
Normal file
68
app/Jobs/ServerPatchCheckJob.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Server\CheckUpdates;
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Server\ServerPatchCheck;
|
||||
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\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ServerPatchCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 3;
|
||||
|
||||
public $timeout = 600;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('server-patch-check-'.$this->server->uuid))->expireAfter(600)->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
if ($this->server->serverStatus() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$team = data_get($this->server, 'team');
|
||||
if (! $team) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for updates
|
||||
$patchData = CheckUpdates::run($this->server);
|
||||
|
||||
if (isset($patchData['error'])) {
|
||||
$team->notify(new ServerPatchCheck($this->server, $patchData));
|
||||
|
||||
return; // Skip if there's an error checking for updates
|
||||
}
|
||||
|
||||
$totalUpdates = $patchData['total_updates'] ?? 0;
|
||||
|
||||
// Only send notification if there are updates available
|
||||
if ($totalUpdates > 0) {
|
||||
$team->notify(new ServerPatchCheck($this->server, $patchData));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Log error but don't fail the job
|
||||
\Illuminate\Support\Facades\Log::error('ServerPatchCheckJob failed: '.$e->getMessage(), [
|
||||
'server_id' => $this->server->id,
|
||||
'server_name' => $this->server->name,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,19 +73,21 @@ class StripeProcessJob implements ShouldQueue
|
||||
}
|
||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||
if ($subscription) {
|
||||
send_internal_notification('Old subscription activated for team: '.$teamId);
|
||||
// send_internal_notification('Old subscription activated for team: '.$teamId);
|
||||
$subscription->update([
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_past_due' => false,
|
||||
]);
|
||||
} else {
|
||||
send_internal_notification('New subscription for team: '.$teamId);
|
||||
// send_internal_notification('New subscription for team: '.$teamId);
|
||||
Subscription::create([
|
||||
'team_id' => $teamId,
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_past_due' => false,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
@@ -100,6 +102,7 @@ class StripeProcessJob implements ShouldQueue
|
||||
if ($subscription) {
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_past_due' => false,
|
||||
]);
|
||||
} else {
|
||||
throw new \RuntimeException("No subscription found for customer: {$customerId}");
|
||||
@@ -119,9 +122,7 @@ class StripeProcessJob implements ShouldQueue
|
||||
}
|
||||
if (! $subscription->stripe_invoice_paid) {
|
||||
SubscriptionInvoiceFailedJob::dispatch($team);
|
||||
send_internal_notification('Invoice payment failed: '.$customerId);
|
||||
} else {
|
||||
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
|
||||
// send_internal_notification('Invoice payment failed: '.$customerId);
|
||||
}
|
||||
break;
|
||||
case 'payment_intent.payment_failed':
|
||||
@@ -136,7 +137,7 @@ class StripeProcessJob implements ShouldQueue
|
||||
|
||||
return;
|
||||
}
|
||||
send_internal_notification('Subscription payment failed for customer: '.$customerId);
|
||||
// send_internal_notification('Subscription payment failed for customer: '.$customerId);
|
||||
break;
|
||||
case 'customer.subscription.created':
|
||||
$customerId = data_get($data, 'customer');
|
||||
@@ -158,7 +159,7 @@ class StripeProcessJob implements ShouldQueue
|
||||
}
|
||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||
if ($subscription) {
|
||||
send_internal_notification("Subscription already exists for team: {$teamId}");
|
||||
// send_internal_notification("Subscription already exists for team: {$teamId}");
|
||||
throw new \RuntimeException("Subscription already exists for team: {$teamId}");
|
||||
} else {
|
||||
Subscription::create([
|
||||
@@ -182,7 +183,7 @@ class StripeProcessJob implements ShouldQueue
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
if ($status === 'incomplete_expired') {
|
||||
send_internal_notification('Subscription incomplete expired');
|
||||
// send_internal_notification('Subscription incomplete expired');
|
||||
throw new \RuntimeException('Subscription incomplete expired');
|
||||
}
|
||||
if ($teamId) {
|
||||
@@ -224,9 +225,33 @@ class StripeProcessJob implements ShouldQueue
|
||||
]);
|
||||
}
|
||||
}
|
||||
if ($status === 'past_due') {
|
||||
if ($subscription->stripe_subscription_id === $subscriptionId) {
|
||||
$subscription->update([
|
||||
'stripe_past_due' => true,
|
||||
]);
|
||||
send_internal_notification('Past Due: '.$customerId.'Subscription ID: '.$subscriptionId);
|
||||
}
|
||||
}
|
||||
if ($status === 'unpaid') {
|
||||
if ($subscription->stripe_subscription_id === $subscriptionId) {
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
send_internal_notification('Unpaid: '.$customerId.'Subscription ID: '.$subscriptionId);
|
||||
}
|
||||
$team = data_get($subscription, 'team');
|
||||
if ($team) {
|
||||
$team->subscriptionEnded();
|
||||
} else {
|
||||
send_internal_notification('Subscription unpaid but no team found in Coolify for customer: '.$customerId);
|
||||
throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
|
||||
}
|
||||
}
|
||||
if ($status === 'active') {
|
||||
if ($subscription->stripe_subscription_id === $subscriptionId) {
|
||||
$subscription->update([
|
||||
'stripe_past_due' => false,
|
||||
'stripe_invoice_paid' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
66
app/Listeners/CloudflareTunnelChangedNotification.php
Normal file
66
app/Listeners/CloudflareTunnelChangedNotification.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\CloudflareTunnelChanged;
|
||||
use App\Events\CloudflareTunnelConfigured;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Sleep;
|
||||
|
||||
class CloudflareTunnelChangedNotification
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(CloudflareTunnelChanged $event): void
|
||||
{
|
||||
$server_id = data_get($event, 'data.server_id');
|
||||
$ssh_domain = data_get($event, 'data.ssh_domain');
|
||||
|
||||
$this->server = Server::where('id', $server_id)->firstOrFail();
|
||||
|
||||
// Check if cloudflare tunnel is running (container is healthy) - try 3 times with 5 second intervals
|
||||
$cloudflareHealthy = false;
|
||||
$attempts = 3;
|
||||
|
||||
for ($i = 1; $i <= $attempts; $i++) {
|
||||
\Log::debug("Cloudflare health check attempt {$i}/{$attempts}", ['server_id' => $server_id]);
|
||||
$result = instant_remote_process_with_timeout(['docker inspect coolify-cloudflared | jq -e ".[0].State.Health.Status == \"healthy\""'], $this->server, false, 10);
|
||||
|
||||
if (blank($result)) {
|
||||
\Log::debug("Cloudflare Tunnels container not found on attempt {$i}", ['server_id' => $server_id]);
|
||||
} elseif ($result === 'true') {
|
||||
\Log::debug("Cloudflare Tunnels container healthy on attempt {$i}", ['server_id' => $server_id]);
|
||||
$cloudflareHealthy = true;
|
||||
break;
|
||||
} else {
|
||||
\Log::debug("Cloudflare Tunnels container not healthy on attempt {$i}", ['server_id' => $server_id, 'result' => $result]);
|
||||
}
|
||||
|
||||
// Sleep between attempts (except after the last attempt)
|
||||
if ($i < $attempts) {
|
||||
Sleep::for(5)->seconds();
|
||||
}
|
||||
}
|
||||
|
||||
if (! $cloudflareHealthy) {
|
||||
\Log::error('Cloudflare Tunnels container failed all health checks.', ['server_id' => $server_id, 'attempts' => $attempts]);
|
||||
|
||||
return;
|
||||
}
|
||||
$this->server->settings->update([
|
||||
'is_cloudflare_tunnel' => true,
|
||||
]);
|
||||
|
||||
// Only update IP if it's not already set to the ssh_domain or if it's empty
|
||||
if ($this->server->ip !== $ssh_domain && ! empty($ssh_domain)) {
|
||||
\Log::debug('Cloudflare Tunnels configuration updated - updating IP address.', ['old_ip' => $this->server->ip, 'new_ip' => $ssh_domain]);
|
||||
$this->server->update(['ip' => $ssh_domain]);
|
||||
} else {
|
||||
\Log::debug('Cloudflare Tunnels configuration updated - IP address unchanged.', ['current_ip' => $this->server->ip]);
|
||||
}
|
||||
$teamId = $this->server->team_id;
|
||||
CloudflareTunnelConfigured::dispatch($teamId);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\ProxyStarted;
|
||||
use App\Models\Server;
|
||||
|
||||
class ProxyStartedNotification
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(ProxyStarted $event): void
|
||||
{
|
||||
$this->server = data_get($event, 'data');
|
||||
$this->server->setupDefaultRedirect();
|
||||
$this->server->setupDynamicProxyConfiguration();
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
}
|
||||
}
|
||||
42
app/Listeners/ProxyStatusChangedNotification.php
Normal file
42
app/Listeners/ProxyStatusChangedNotification.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
|
||||
|
||||
class ProxyStatusChangedNotification implements ShouldQueueAfterCommit
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(ProxyStatusChanged $event)
|
||||
{
|
||||
$serverId = $event->data;
|
||||
if (is_null($serverId)) {
|
||||
return;
|
||||
}
|
||||
$server = Server::where('id', $serverId)->first();
|
||||
if (is_null($server)) {
|
||||
return;
|
||||
}
|
||||
$proxyContainerName = 'coolify-proxy';
|
||||
$status = getContainerStatus($server, $proxyContainerName);
|
||||
$server->proxy->set('status', $status);
|
||||
$server->save();
|
||||
|
||||
ProxyStatusChangedUI::dispatch($server->team_id);
|
||||
if ($status === 'running') {
|
||||
$server->setupDefaultRedirect();
|
||||
$server->setupDynamicProxyConfiguration();
|
||||
$server->proxy->force_stop = false;
|
||||
$server->save();
|
||||
}
|
||||
if ($status === 'created') {
|
||||
instant_remote_process([
|
||||
'docker rm -f coolify-proxy',
|
||||
], $server);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,20 +14,25 @@ class ActivityMonitor extends Component
|
||||
|
||||
public $eventToDispatch = 'activityFinished';
|
||||
|
||||
public $eventData = null;
|
||||
|
||||
public $isPollingActive = false;
|
||||
|
||||
public bool $fullHeight = false;
|
||||
|
||||
public bool $showWaiting = false;
|
||||
public $activity;
|
||||
|
||||
protected $activity;
|
||||
public bool $showWaiting = true;
|
||||
|
||||
public static $eventDispatched = false;
|
||||
|
||||
protected $listeners = ['activityMonitor' => 'newMonitorActivity'];
|
||||
|
||||
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
|
||||
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null)
|
||||
{
|
||||
$this->activityId = $activityId;
|
||||
$this->eventToDispatch = $eventToDispatch;
|
||||
$this->eventData = $eventData;
|
||||
|
||||
$this->hydrateActivity();
|
||||
|
||||
@@ -51,15 +56,27 @@ class ActivityMonitor extends Component
|
||||
$causer_id = data_get($this->activity, 'causer_id');
|
||||
$user = User::find($causer_id);
|
||||
if ($user) {
|
||||
foreach ($user->teams as $team) {
|
||||
$teamId = $team->id;
|
||||
$this->eventToDispatch::dispatch($teamId);
|
||||
$teamId = $user->currentTeam()->id;
|
||||
if (! self::$eventDispatched) {
|
||||
if (filled($this->eventData)) {
|
||||
$this->eventToDispatch::dispatch($teamId, $this->eventData);
|
||||
} else {
|
||||
$this->eventToDispatch::dispatch($teamId);
|
||||
}
|
||||
self::$eventDispatched = true;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
$this->dispatch($this->eventToDispatch);
|
||||
if (! self::$eventDispatched) {
|
||||
if (filled($this->eventData)) {
|
||||
$this->dispatch($this->eventToDispatch, $this->eventData);
|
||||
} else {
|
||||
$this->dispatch($this->eventToDispatch);
|
||||
}
|
||||
self::$eventDispatched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Services\ConfigurationRepository;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
@@ -266,7 +267,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
public function validateServer()
|
||||
{
|
||||
try {
|
||||
config()->set('constants.ssh.mux_enabled', false);
|
||||
$this->disableSshMux();
|
||||
|
||||
// EC2 does not have `uptime` command, lol
|
||||
instant_remote_process(['ls /'], $this->createdServer, true);
|
||||
@@ -376,6 +377,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey();
|
||||
}
|
||||
|
||||
private function disableSshMux(): void
|
||||
{
|
||||
$configRepository = app(ConfigurationRepository::class);
|
||||
$configRepository->disableSshMux();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.boarding.index')->layout('layouts.boarding');
|
||||
|
||||
@@ -51,7 +51,7 @@ class Dashboard extends Component
|
||||
|
||||
public function navigateToProject($projectUuid)
|
||||
{
|
||||
return $this->redirect(collect($this->projects)->firstWhere('uuid', $projectUuid)->navigateTo(), true);
|
||||
return $this->redirect(collect($this->projects)->firstWhere('uuid', $projectUuid)->navigateTo(), navigate: false);
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -35,10 +35,18 @@ class Docker extends Component
|
||||
$this->network = new Cuid2;
|
||||
$this->servers = Server::isUsable()->get();
|
||||
if ($server_id) {
|
||||
$this->selectedServer = $this->servers->find($server_id) ?: $this->servers->first();
|
||||
$foundServer = $this->servers->find($server_id) ?: $this->servers->first();
|
||||
if (! $foundServer) {
|
||||
throw new \Exception('Server not found.');
|
||||
}
|
||||
$this->selectedServer = $foundServer;
|
||||
$this->serverId = $this->selectedServer->id;
|
||||
} else {
|
||||
$this->selectedServer = $this->servers->first();
|
||||
$foundServer = $this->servers->first();
|
||||
if (! $foundServer) {
|
||||
throw new \Exception('Server not found.');
|
||||
}
|
||||
$this->selectedServer = $foundServer;
|
||||
$this->serverId = $this->selectedServer->id;
|
||||
}
|
||||
$this->generateName();
|
||||
|
||||
@@ -36,7 +36,7 @@ class Help extends Component
|
||||
$type = set_transanctional_email_settings($settings);
|
||||
|
||||
// Sending feedback through Cloud API
|
||||
if ($type === false) {
|
||||
if (blank($type)) {
|
||||
$url = 'https://app.coolify.io/api/feedback';
|
||||
Http::post($url, [
|
||||
'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
//use Livewire\Component;
|
||||
// use Livewire\Component;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\User;
|
||||
use Livewire\Component;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class NewActivityMonitor extends Component
|
||||
{
|
||||
public ?string $header = null;
|
||||
|
||||
public $activityId;
|
||||
|
||||
public $eventToDispatch = 'activityFinished';
|
||||
|
||||
public $eventData = null;
|
||||
|
||||
public $isPollingActive = false;
|
||||
|
||||
protected $activity;
|
||||
|
||||
protected $listeners = ['newActivityMonitor' => 'newMonitorActivity'];
|
||||
|
||||
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null)
|
||||
{
|
||||
$this->activityId = $activityId;
|
||||
$this->eventToDispatch = $eventToDispatch;
|
||||
$this->eventData = $eventData;
|
||||
|
||||
$this->hydrateActivity();
|
||||
|
||||
$this->isPollingActive = true;
|
||||
}
|
||||
|
||||
public function hydrateActivity()
|
||||
{
|
||||
$this->activity = Activity::find($this->activityId);
|
||||
}
|
||||
|
||||
public function polling()
|
||||
{
|
||||
$this->hydrateActivity();
|
||||
// $this->setStatus(ProcessStatus::IN_PROGRESS);
|
||||
$exit_code = data_get($this->activity, 'properties.exitCode');
|
||||
if ($exit_code !== null) {
|
||||
// if ($exit_code === 0) {
|
||||
// // $this->setStatus(ProcessStatus::FINISHED);
|
||||
// } else {
|
||||
// // $this->setStatus(ProcessStatus::ERROR);
|
||||
// }
|
||||
$this->isPollingActive = false;
|
||||
if ($this->eventToDispatch !== null) {
|
||||
if (str($this->eventToDispatch)->startsWith('App\\Events\\')) {
|
||||
$causer_id = data_get($this->activity, 'causer_id');
|
||||
$user = User::find($causer_id);
|
||||
if ($user) {
|
||||
foreach ($user->teams as $team) {
|
||||
$teamId = $team->id;
|
||||
$this->eventToDispatch::dispatch($teamId);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (! is_null($this->eventData)) {
|
||||
$this->dispatch($this->eventToDispatch, $this->eventData);
|
||||
} else {
|
||||
$this->dispatch($this->eventToDispatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,12 @@ class Discord extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachableDiscordNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverPatchDiscordNotifications = false;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $discordPingEnabled = true;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -86,6 +92,9 @@ class Discord extends Component
|
||||
$this->settings->server_disk_usage_discord_notifications = $this->serverDiskUsageDiscordNotifications;
|
||||
$this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications;
|
||||
$this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications;
|
||||
$this->settings->server_patch_discord_notifications = $this->serverPatchDiscordNotifications;
|
||||
|
||||
$this->settings->discord_ping_enabled = $this->discordPingEnabled;
|
||||
|
||||
$this->settings->save();
|
||||
refreshSession();
|
||||
@@ -105,12 +114,31 @@ class Discord extends Component
|
||||
$this->serverDiskUsageDiscordNotifications = $this->settings->server_disk_usage_discord_notifications;
|
||||
$this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications;
|
||||
$this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications;
|
||||
$this->serverPatchDiscordNotifications = $this->settings->server_patch_discord_notifications;
|
||||
|
||||
$this->discordPingEnabled = $this->settings->discord_ping_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveDiscordPingEnabled()
|
||||
{
|
||||
try {
|
||||
$original = $this->discordPingEnabled;
|
||||
$this->validate([
|
||||
'discordPingEnabled' => 'required',
|
||||
]);
|
||||
$this->saveModel();
|
||||
} catch (\Throwable $e) {
|
||||
$this->discordPingEnabled = $original;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveDiscordEnabled()
|
||||
{
|
||||
try {
|
||||
$original = $this->discordEnabled;
|
||||
$this->validate([
|
||||
'discordWebhookUrl' => 'required',
|
||||
], [
|
||||
@@ -118,7 +146,7 @@ class Discord extends Component
|
||||
]);
|
||||
$this->saveModel();
|
||||
} catch (\Throwable $e) {
|
||||
$this->discordEnabled = false;
|
||||
$this->discordEnabled = $original;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@ class Email extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachableEmailNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverPatchEmailNotifications = false;
|
||||
|
||||
#[Validate(['nullable', 'email'])]
|
||||
public ?string $testEmailAddress = null;
|
||||
|
||||
@@ -146,6 +149,7 @@ class Email extends Component
|
||||
$this->settings->server_disk_usage_email_notifications = $this->serverDiskUsageEmailNotifications;
|
||||
$this->settings->server_reachable_email_notifications = $this->serverReachableEmailNotifications;
|
||||
$this->settings->server_unreachable_email_notifications = $this->serverUnreachableEmailNotifications;
|
||||
$this->settings->server_patch_email_notifications = $this->serverPatchEmailNotifications;
|
||||
$this->settings->save();
|
||||
|
||||
} else {
|
||||
@@ -177,6 +181,7 @@ class Email extends Component
|
||||
$this->serverDiskUsageEmailNotifications = $this->settings->server_disk_usage_email_notifications;
|
||||
$this->serverReachableEmailNotifications = $this->settings->server_reachable_email_notifications;
|
||||
$this->serverUnreachableEmailNotifications = $this->settings->server_unreachable_email_notifications;
|
||||
$this->serverPatchEmailNotifications = $this->settings->server_patch_email_notifications;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,10 +254,9 @@ class Email extends Component
|
||||
'smtpEncryption.required' => 'Encryption type is required.',
|
||||
]);
|
||||
|
||||
$this->settings->resend_enabled = false;
|
||||
$this->settings->use_instance_email_settings = false;
|
||||
$this->resendEnabled = false;
|
||||
$this->useInstanceEmailSettings = false;
|
||||
if ($this->smtpEnabled) {
|
||||
$this->settings->resend_enabled = $this->resendEnabled = false;
|
||||
}
|
||||
|
||||
$this->settings->smtp_enabled = $this->smtpEnabled;
|
||||
$this->settings->smtp_from_address = $this->smtpFromAddress;
|
||||
@@ -269,7 +273,7 @@ class Email extends Component
|
||||
} catch (\Throwable $e) {
|
||||
$this->smtpEnabled = false;
|
||||
|
||||
return handleError($e);
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,11 +292,9 @@ class Email extends Component
|
||||
'smtpFromAddress.email' => 'Please enter a valid email address.',
|
||||
'smtpFromName.required' => 'From Name is required.',
|
||||
]);
|
||||
|
||||
$this->settings->smtp_enabled = false;
|
||||
$this->settings->use_instance_email_settings = false;
|
||||
$this->smtpEnabled = false;
|
||||
$this->useInstanceEmailSettings = false;
|
||||
if ($this->resendEnabled) {
|
||||
$this->settings->smtp_enabled = $this->smtpEnabled = false;
|
||||
}
|
||||
|
||||
$this->settings->resend_enabled = $this->resendEnabled;
|
||||
$this->settings->resend_api_key = $this->resendApiKey;
|
||||
@@ -320,7 +322,7 @@ class Email extends Component
|
||||
'test-email:'.$this->team->id,
|
||||
$perMinute = 0,
|
||||
function () {
|
||||
$this->team?->notify(new Test($this->testEmailAddress, 'email'));
|
||||
$this->team?->notifyNow(new Test($this->testEmailAddress, 'email'));
|
||||
$this->dispatch('success', 'Test Email sent.');
|
||||
},
|
||||
$decaySeconds = 10,
|
||||
@@ -337,32 +339,29 @@ class Email extends Component
|
||||
public function copyFromInstanceSettings()
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
$this->smtpFromAddress = $settings->smtp_from_address;
|
||||
$this->smtpFromName = $settings->smtp_from_name;
|
||||
|
||||
if ($settings->smtp_enabled) {
|
||||
$this->smtpEnabled = true;
|
||||
$this->smtpFromAddress = $settings->smtp_from_address;
|
||||
$this->smtpFromName = $settings->smtp_from_name;
|
||||
$this->smtpRecipients = $settings->smtp_recipients;
|
||||
$this->smtpHost = $settings->smtp_host;
|
||||
$this->smtpPort = $settings->smtp_port;
|
||||
$this->smtpEncryption = $settings->smtp_encryption;
|
||||
$this->smtpUsername = $settings->smtp_username;
|
||||
$this->smtpPassword = $settings->smtp_password;
|
||||
$this->smtpTimeout = $settings->smtp_timeout;
|
||||
$this->resendEnabled = false;
|
||||
$this->saveModel();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->smtpRecipients = $settings->smtp_recipients;
|
||||
$this->smtpHost = $settings->smtp_host;
|
||||
$this->smtpPort = $settings->smtp_port;
|
||||
$this->smtpEncryption = $settings->smtp_encryption;
|
||||
$this->smtpUsername = $settings->smtp_username;
|
||||
$this->smtpPassword = $settings->smtp_password;
|
||||
$this->smtpTimeout = $settings->smtp_timeout;
|
||||
|
||||
if ($settings->resend_enabled) {
|
||||
$this->resendEnabled = true;
|
||||
$this->resendApiKey = $settings->resend_api_key;
|
||||
$this->smtpEnabled = false;
|
||||
$this->saveModel();
|
||||
|
||||
return;
|
||||
}
|
||||
$this->dispatch('error', 'Instance SMTP/Resend settings are not enabled.');
|
||||
$this->resendApiKey = $settings->resend_api_key;
|
||||
$this->saveModel();
|
||||
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -64,6 +64,9 @@ class Pushover extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachablePushoverNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverPatchPushoverNotifications = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -95,6 +98,7 @@ class Pushover extends Component
|
||||
$this->settings->server_disk_usage_pushover_notifications = $this->serverDiskUsagePushoverNotifications;
|
||||
$this->settings->server_reachable_pushover_notifications = $this->serverReachablePushoverNotifications;
|
||||
$this->settings->server_unreachable_pushover_notifications = $this->serverUnreachablePushoverNotifications;
|
||||
$this->settings->server_patch_pushover_notifications = $this->serverPatchPushoverNotifications;
|
||||
|
||||
$this->settings->save();
|
||||
refreshSession();
|
||||
@@ -115,6 +119,7 @@ class Pushover extends Component
|
||||
$this->serverDiskUsagePushoverNotifications = $this->settings->server_disk_usage_pushover_notifications;
|
||||
$this->serverReachablePushoverNotifications = $this->settings->server_reachable_pushover_notifications;
|
||||
$this->serverUnreachablePushoverNotifications = $this->settings->server_unreachable_pushover_notifications;
|
||||
$this->serverPatchPushoverNotifications = $this->settings->server_patch_pushover_notifications;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ class Slack extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachableSlackNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverPatchSlackNotifications = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -91,6 +94,7 @@ class Slack extends Component
|
||||
$this->settings->server_disk_usage_slack_notifications = $this->serverDiskUsageSlackNotifications;
|
||||
$this->settings->server_reachable_slack_notifications = $this->serverReachableSlackNotifications;
|
||||
$this->settings->server_unreachable_slack_notifications = $this->serverUnreachableSlackNotifications;
|
||||
$this->settings->server_patch_slack_notifications = $this->serverPatchSlackNotifications;
|
||||
|
||||
$this->settings->save();
|
||||
refreshSession();
|
||||
@@ -110,6 +114,7 @@ class Slack extends Component
|
||||
$this->serverDiskUsageSlackNotifications = $this->settings->server_disk_usage_slack_notifications;
|
||||
$this->serverReachableSlackNotifications = $this->settings->server_reachable_slack_notifications;
|
||||
$this->serverUnreachableSlackNotifications = $this->settings->server_unreachable_slack_notifications;
|
||||
$this->serverPatchSlackNotifications = $this->settings->server_patch_slack_notifications;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,9 @@ class Telegram extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachableTelegramNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverPatchTelegramNotifications = false;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $telegramNotificationsDeploymentSuccessThreadId = null;
|
||||
|
||||
@@ -100,6 +103,9 @@ class Telegram extends Component
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $telegramNotificationsServerUnreachableThreadId = null;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $telegramNotificationsServerPatchThreadId = null;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -131,6 +137,7 @@ class Telegram extends Component
|
||||
$this->settings->server_disk_usage_telegram_notifications = $this->serverDiskUsageTelegramNotifications;
|
||||
$this->settings->server_reachable_telegram_notifications = $this->serverReachableTelegramNotifications;
|
||||
$this->settings->server_unreachable_telegram_notifications = $this->serverUnreachableTelegramNotifications;
|
||||
$this->settings->server_patch_telegram_notifications = $this->serverPatchTelegramNotifications;
|
||||
|
||||
$this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId;
|
||||
$this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId;
|
||||
@@ -144,6 +151,7 @@ class Telegram extends Component
|
||||
$this->settings->telegram_notifications_server_disk_usage_thread_id = $this->telegramNotificationsServerDiskUsageThreadId;
|
||||
$this->settings->telegram_notifications_server_reachable_thread_id = $this->telegramNotificationsServerReachableThreadId;
|
||||
$this->settings->telegram_notifications_server_unreachable_thread_id = $this->telegramNotificationsServerUnreachableThreadId;
|
||||
$this->settings->telegram_notifications_server_patch_thread_id = $this->telegramNotificationsServerPatchThreadId;
|
||||
|
||||
$this->settings->save();
|
||||
} else {
|
||||
@@ -163,6 +171,7 @@ class Telegram extends Component
|
||||
$this->serverDiskUsageTelegramNotifications = $this->settings->server_disk_usage_telegram_notifications;
|
||||
$this->serverReachableTelegramNotifications = $this->settings->server_reachable_telegram_notifications;
|
||||
$this->serverUnreachableTelegramNotifications = $this->settings->server_unreachable_telegram_notifications;
|
||||
$this->serverPatchTelegramNotifications = $this->settings->server_patch_telegram_notifications;
|
||||
|
||||
$this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id;
|
||||
$this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id;
|
||||
@@ -176,6 +185,7 @@ class Telegram extends Component
|
||||
$this->telegramNotificationsServerDiskUsageThreadId = $this->settings->telegram_notifications_server_disk_usage_thread_id;
|
||||
$this->telegramNotificationsServerReachableThreadId = $this->settings->telegram_notifications_server_reachable_thread_id;
|
||||
$this->telegramNotificationsServerUnreachableThreadId = $this->settings->telegram_notifications_server_unreachable_thread_id;
|
||||
$this->telegramNotificationsServerPatchThreadId = $this->settings->telegram_notifications_server_patch_thread_id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ class Index extends Component
|
||||
$this->current_password = '';
|
||||
$this->new_password = '';
|
||||
$this->new_password_confirmation = '';
|
||||
$this->dispatch('reloadWindow');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,22 @@ class Configuration extends Component
|
||||
|
||||
public $servers;
|
||||
|
||||
protected $listeners = ['buildPackUpdated' => '$refresh'];
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
|
||||
"echo-private:team.{$teamId},ServiceStatusChanged" => '$refresh',
|
||||
'buildPackUpdated' => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentRoute = request()->route()->getName();
|
||||
|
||||
$project = currentTeam()
|
||||
->projects()
|
||||
->select('id', 'uuid', 'team_id')
|
||||
@@ -39,6 +50,14 @@ class Configuration extends Component
|
||||
$this->project = $project;
|
||||
$this->environment = $environment;
|
||||
$this->application = $application;
|
||||
|
||||
if ($this->application->deploymentType() === 'deploy_key' && $this->currentRoute === 'project.application.preview-deployments') {
|
||||
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
|
||||
}
|
||||
|
||||
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
|
||||
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -28,6 +28,15 @@ class Index extends Component
|
||||
|
||||
protected $queryString = ['pull_request_id'];
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
|
||||
@@ -18,7 +18,15 @@ class Show extends Component
|
||||
|
||||
public $isKeepAliveOn = true;
|
||||
|
||||
protected $listeners = ['refreshQueue'];
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
|
||||
'refreshQueue',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user