Merge branch 'coollabsio:main' into fix-redis-db-ui
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Application;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Models\Application;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -9,44 +10,35 @@ class StopApplication
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Application $application, bool $previewDeployments = false)
|
||||
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
|
||||
{
|
||||
if ($application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$servers = collect([]);
|
||||
$servers->push($application->destination->server);
|
||||
$application->additional_servers->map(function ($server) use ($servers) {
|
||||
$servers->push($server);
|
||||
});
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
$server = $application->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
if ($previewDeployments) {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
}
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
if ($containerName) {
|
||||
instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
|
||||
}
|
||||
}
|
||||
ray('Stopping application: '.$application->name);
|
||||
|
||||
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') {
|
||||
// remove network
|
||||
$uuid = $application->uuid;
|
||||
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
||||
$application->delete_connected_networks($application->uuid);
|
||||
}
|
||||
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Actions\CoolifyTask;
|
||||
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Process\ProcessResult;
|
||||
@@ -137,7 +138,7 @@ class RunRemoteProcess
|
||||
$command = $this->activity->getExtraProperty('command');
|
||||
$server = Server::whereUuid($server_uuid)->firstOrFail();
|
||||
|
||||
return generateSshCommand($server, $command);
|
||||
return SshMultiplexingHelper::generateSshCommand($server, $command);
|
||||
}
|
||||
|
||||
protected function handleOutput(string $type, string $output)
|
||||
|
||||
@@ -79,14 +79,7 @@ class StartClickhouse
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
$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;
|
||||
@@ -102,6 +95,11 @@ class StartClickhouse
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_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";
|
||||
@@ -162,6 +160,8 @@ class StartClickhouse
|
||||
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}");
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,14 +79,7 @@ class StartDragonfly
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
$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;
|
||||
@@ -102,6 +95,11 @@ class StartDragonfly
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_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";
|
||||
|
||||
@@ -78,14 +78,7 @@ class StartKeydb
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
$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;
|
||||
@@ -110,6 +103,10 @@ class StartKeydb
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
$docker_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";
|
||||
@@ -166,6 +163,8 @@ class StartKeydb
|
||||
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
|
||||
@@ -73,14 +73,7 @@ class StartMariadb
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
$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;
|
||||
@@ -104,6 +97,11 @@ class StartMariadb
|
||||
'read_only' => true,
|
||||
];
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_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";
|
||||
@@ -171,6 +169,8 @@ class StartMariadb
|
||||
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
|
||||
@@ -81,14 +81,7 @@ class StartMongodb
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
$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;
|
||||
@@ -121,6 +114,10 @@ class StartMongodb
|
||||
'read_only' => true,
|
||||
];
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_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";
|
||||
@@ -185,6 +182,8 @@ class StartMongodb
|
||||
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
|
||||
@@ -73,14 +73,7 @@ class StartMysql
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
$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;
|
||||
@@ -104,6 +97,11 @@ class StartMysql
|
||||
'read_only' => true,
|
||||
];
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_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";
|
||||
@@ -171,6 +169,8 @@ class StartMysql
|
||||
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
|
||||
@@ -80,14 +80,7 @@ class StartPostgresql
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
$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;
|
||||
@@ -126,6 +119,10 @@ class StartPostgresql
|
||||
'config_file=/etc/postgresql/postgresql.conf',
|
||||
];
|
||||
}
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
$docker_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";
|
||||
@@ -193,6 +190,8 @@ class StartPostgresql
|
||||
$environment_variables->push("POSTGRES_DB={$this->database->postgres_db}");
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
@@ -10,25 +11,65 @@ 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
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
|
||||
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';
|
||||
}
|
||||
|
||||
instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false);
|
||||
$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';
|
||||
}
|
||||
|
||||
private function stopContainer($database, string $containerName, int $timeout = 300): 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,7 +543,7 @@ class GetContainersStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
$exitedServices = $exitedServices->unique('id');
|
||||
$exitedServices = $exitedServices->unique('uuid');
|
||||
foreach ($exitedServices as $exitedService) {
|
||||
if (str($exitedService->status)->startsWith('exited')) {
|
||||
continue;
|
||||
|
||||
@@ -26,7 +26,7 @@ class CheckProxy
|
||||
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
|
||||
return false;
|
||||
}
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
|
||||
if (! $uptime) {
|
||||
throw new \Exception($error);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ class StartProxy
|
||||
"echo 'Pulling docker image.'",
|
||||
'docker compose pull',
|
||||
"echo 'Stopping existing coolify-proxy.'",
|
||||
'docker compose down -v --remove-orphans > /dev/null 2>&1',
|
||||
'docker stop -t 10 coolify-proxy || true',
|
||||
'docker rm coolify-proxy || true',
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d --remove-orphans',
|
||||
"echo 'Proxy started successfully.'",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -9,17 +10,30 @@ class CleanupDocker
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, bool $force = true)
|
||||
public function handle(Server $server)
|
||||
{
|
||||
// cleanup docker images, containers, and builder caches
|
||||
if ($force) {
|
||||
instant_remote_process(['docker image prune -af'], $server, false);
|
||||
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
|
||||
instant_remote_process(['docker builder prune -af'], $server, false);
|
||||
} else {
|
||||
instant_remote_process(['docker image prune -f'], $server, false);
|
||||
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
|
||||
instant_remote_process(['docker builder prune -f'], $server, false);
|
||||
|
||||
$commands = $this->getCommands();
|
||||
|
||||
foreach ($commands as $command) {
|
||||
instant_remote_process([$command], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
private function getCommands(): array
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
$helperImageVersion = data_get($settings, 'helper_version');
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$helperImageWithVersion = config('coolify.helper_image').':'.$helperImageVersion;
|
||||
|
||||
$commonCommands = [
|
||||
'docker container prune -f --filter "label=coolify.managed=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",
|
||||
];
|
||||
|
||||
return $commonCommands;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Events\CloudflareTunnelConfigured;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -40,12 +41,17 @@ class ConfigureCloudflared
|
||||
instant_remote_process($commands, $server);
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
$server->settings->is_cloudflare_tunnel = false;
|
||||
$server->settings->save();
|
||||
throw $e;
|
||||
} finally {
|
||||
CloudflareTunnelConfigured::dispatch($server->team_id);
|
||||
|
||||
$commands = collect([
|
||||
'rm -fr /tmp/cloudflared',
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,11 @@ class InstallLogDrain
|
||||
[FILTER]
|
||||
Name modify
|
||||
Match *
|
||||
Set server_name {$server->name}
|
||||
Set coolify.server_name {$server->name}
|
||||
Rename COOLIFY_APP_NAME coolify.app_name
|
||||
Rename COOLIFY_PROJECT_NAME coolify.project_name
|
||||
Rename COOLIFY_SERVER_IP coolify.server_ip
|
||||
Rename COOLIFY_ENVIRONMENT_NAME coolify.environment_name
|
||||
[OUTPUT]
|
||||
Name nrlogs
|
||||
Match *
|
||||
@@ -98,7 +102,11 @@ class InstallLogDrain
|
||||
[FILTER]
|
||||
Name modify
|
||||
Match *
|
||||
Set server_name {$server->name}
|
||||
Set coolify.server_name {$server->name}
|
||||
Rename COOLIFY_APP_NAME coolify.app_name
|
||||
Rename COOLIFY_PROJECT_NAME coolify.project_name
|
||||
Rename COOLIFY_SERVER_IP coolify.server_ip
|
||||
Rename COOLIFY_ENVIRONMENT_NAME coolify.environment_name
|
||||
[OUTPUT]
|
||||
Name http
|
||||
Match *
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class UpdateCoolify
|
||||
@@ -26,12 +25,7 @@ class UpdateCoolify
|
||||
if (! $this->server) {
|
||||
return;
|
||||
}
|
||||
CleanupDocker::dispatch($this->server, false)->onQueue('high');
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
|
||||
}
|
||||
CleanupDocker::dispatch($this->server)->onQueue('high');
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('version');
|
||||
if (! $manual_update) {
|
||||
@@ -62,10 +56,18 @@ class UpdateCoolify
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$all_servers = Server::all();
|
||||
$servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
|
||||
foreach ($servers as $server) {
|
||||
PullHelperImageJob::dispatch($server);
|
||||
}
|
||||
|
||||
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
|
||||
|
||||
remote_process([
|
||||
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion",
|
||||
], $this->server);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -9,11 +10,11 @@ class DeleteService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $service)
|
||||
public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
|
||||
{
|
||||
try {
|
||||
$server = data_get($service, 'server');
|
||||
if ($server->isFunctional()) {
|
||||
if ($deleteVolumes && $server->isFunctional()) {
|
||||
$storagesToDelete = collect([]);
|
||||
|
||||
$service->environment_variables()->delete();
|
||||
@@ -33,13 +34,29 @@ class DeleteService
|
||||
foreach ($storagesToDelete as $storage) {
|
||||
$commands[] = "docker volume rm -f $storage->name";
|
||||
}
|
||||
$commands[] = "docker rm -f $service->uuid";
|
||||
|
||||
instant_remote_process($commands, $server, false);
|
||||
// Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
|
||||
if (! empty($commands)) {
|
||||
foreach ($commands as $command) {
|
||||
$result = instant_remote_process([$command], $server, false);
|
||||
if ($result !== 0) {
|
||||
ray("Failed to execute: $command");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteConnectedNetworks) {
|
||||
$service->delete_connected_networks($service->uuid);
|
||||
}
|
||||
|
||||
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
} finally {
|
||||
if ($deleteConfigurations) {
|
||||
$service->delete_configurations();
|
||||
}
|
||||
foreach ($service->applications()->get() as $application) {
|
||||
$application->forceDelete();
|
||||
}
|
||||
@@ -50,6 +67,11 @@ class DeleteService
|
||||
$task->delete();
|
||||
}
|
||||
$service->tags()->detach();
|
||||
$service->forceDelete();
|
||||
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ class StartService
|
||||
$service->saveComposeConfigs();
|
||||
$commands[] = 'cd '.$service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
$commands[] = "echo 'Creating Docker network.'";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
||||
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';
|
||||
@@ -29,7 +31,7 @@ class StartService
|
||||
$network = $service->destination->network;
|
||||
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
|
||||
foreach ($serviceNames as $serviceName => $serviceConfig) {
|
||||
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
|
||||
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
|
||||
}
|
||||
}
|
||||
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -9,35 +10,27 @@ class StopService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $service)
|
||||
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
$server = $service->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
ray('Stopping service: '.$service->name);
|
||||
$applications = $service->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
instant_remote_process(command: ["docker stop --time=30 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
$application->update(['status' => 'exited']);
|
||||
|
||||
$containersToStop = $service->getContainersToStop();
|
||||
$service->stopContainers($containersToStop, $server);
|
||||
|
||||
if (! $isDeleteOperation) {
|
||||
$service->delete_connected_networks($service->uuid);
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
}
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($dbs as $db) {
|
||||
instant_remote_process(command: ["docker stop --time=30 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
|
||||
$db->update(['status' => 'exited']);
|
||||
}
|
||||
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
|
||||
instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class CleanupQueue extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:queue';
|
||||
|
||||
protected $description = 'Cleanup Queue';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Running queue cleanup...\n";
|
||||
$prefix = config('database.redis.options.prefix');
|
||||
$keys = Redis::connection()->keys('*:laravel*');
|
||||
foreach ($keys as $key) {
|
||||
$keyWithoutPrefix = str_replace($prefix, '', $key);
|
||||
Redis::connection()->del($keyWithoutPrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
app/Console/Commands/CleanupRedis.php
Normal file
31
app/Console/Commands/CleanupRedis.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class CleanupRedis extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:redis';
|
||||
|
||||
protected $description = 'Cleanup Redis';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Cleanup Redis keys.\n";
|
||||
$prefix = config('database.redis.options.prefix');
|
||||
|
||||
$keys = Redis::connection()->keys('*:laravel*');
|
||||
collect($keys)->each(function ($key) use ($prefix) {
|
||||
$keyWithoutPrefix = str_replace($prefix, '', $key);
|
||||
Redis::connection()->del($keyWithoutPrefix);
|
||||
});
|
||||
|
||||
$queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
|
||||
collect($queueOverlaps)->each(function ($key) {
|
||||
Redis::connection()->del($key);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CleanupHelperContainersJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
@@ -33,6 +37,16 @@ class CleanupStuckedResources extends Command
|
||||
private function cleanup_stucked_resources()
|
||||
{
|
||||
|
||||
try {
|
||||
$servers = Server::all()->filter(function ($server) {
|
||||
return $server->isFunctional();
|
||||
});
|
||||
foreach ($servers as $server) {
|
||||
CleanupHelperContainersJob::dispatch($server);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($applications as $application) {
|
||||
@@ -42,6 +56,17 @@ class CleanupStuckedResources extends Command
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$applicationsPreviews = ApplicationPreview::get();
|
||||
foreach ($applicationsPreviews as $applicationPreview) {
|
||||
if (! data_get($applicationPreview, 'application')) {
|
||||
echo "Deleting stuck application preview: {$applicationPreview->uuid}\n";
|
||||
$applicationPreview->delete();
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($postgresqls as $postgresql) {
|
||||
@@ -153,6 +178,18 @@ class CleanupStuckedResources extends Command
|
||||
echo "Error in cleaning stuck scheduledtasks: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$scheduled_backups = ScheduledDatabaseBackup::all();
|
||||
foreach ($scheduled_backups as $scheduled_backup) {
|
||||
if (! $scheduled_backup->server()) {
|
||||
echo "Deleting stuck scheduledbackup: {$scheduled_backup->name}\n";
|
||||
$scheduled_backup->delete();
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck scheduledbackups: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
// Cleanup any resources that are not attached to any environment or destination or server
|
||||
try {
|
||||
$applications = Application::all();
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Console\Commands;
|
||||
use App\Actions\Server\StopSentinel;
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\CleanupHelperContainersJob;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
@@ -18,7 +17,7 @@ use Illuminate\Support\Facades\Http;
|
||||
|
||||
class Init extends Command
|
||||
{
|
||||
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}';
|
||||
protected $signature = 'app:init {--force-cloud}';
|
||||
|
||||
protected $description = 'Cleanup instance related stuffs';
|
||||
|
||||
@@ -26,9 +25,63 @@ class Init extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (isCloud() && ! $this->option('force-cloud')) {
|
||||
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->servers = Server::all();
|
||||
$this->alive();
|
||||
get_public_ips();
|
||||
if (isCloud()) {
|
||||
|
||||
} else {
|
||||
$this->send_alive_signal();
|
||||
get_public_ips();
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
$this->disable_metrics();
|
||||
$this->replace_slash_in_environment_name();
|
||||
$this->restore_coolify_db_backup();
|
||||
//
|
||||
$this->update_traefik_labels();
|
||||
if (! isCloud() || $this->option('force-cloud')) {
|
||||
$this->cleanup_unused_network_from_coolify_proxy();
|
||||
}
|
||||
if (isCloud()) {
|
||||
$this->cleanup_unnecessary_dynamic_proxy_configuration();
|
||||
} else {
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
}
|
||||
$this->call('cleanup:redis');
|
||||
$this->call('cleanup:stucked-resources');
|
||||
|
||||
if (isCloud()) {
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
if ($response->successful()) {
|
||||
$services = $response->json();
|
||||
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$localhost = $this->servers->where('id', 0)->first();
|
||||
$localhost->setupDynamicProxyConfiguration();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
$settings = InstanceSettings::get();
|
||||
if (! is_null(env('AUTOUPDATE', null))) {
|
||||
if (env('AUTOUPDATE') == true) {
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function disable_metrics()
|
||||
{
|
||||
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
|
||||
foreach ($this->servers as $server) {
|
||||
if ($server->settings->is_metrics_enabled === true) {
|
||||
@@ -39,62 +92,6 @@ class Init extends Command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$full_cleanup = $this->option('full-cleanup');
|
||||
$cleanup_deployments = $this->option('cleanup-deployments');
|
||||
$cleanup_proxy_networks = $this->option('cleanup-proxy-networks');
|
||||
$this->replace_slash_in_environment_name();
|
||||
if ($cleanup_deployments) {
|
||||
echo "Running cleanup deployments.\n";
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
|
||||
return;
|
||||
}
|
||||
if ($cleanup_proxy_networks) {
|
||||
echo "Running cleanup proxy networks.\n";
|
||||
$this->cleanup_unused_network_from_coolify_proxy();
|
||||
|
||||
return;
|
||||
}
|
||||
if ($full_cleanup) {
|
||||
// Required for falsely deleted coolify db
|
||||
$this->restore_coolify_db_backup();
|
||||
$this->update_traefik_labels();
|
||||
$this->cleanup_unused_network_from_coolify_proxy();
|
||||
$this->cleanup_unnecessary_dynamic_proxy_configuration();
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
$this->cleanup_stucked_helper_containers();
|
||||
$this->call('cleanup:queue');
|
||||
$this->call('cleanup:stucked-resources');
|
||||
if (! isCloud()) {
|
||||
try {
|
||||
$localhost = $this->servers->where('id', 0)->first();
|
||||
$localhost->setupDynamicProxyConfiguration();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
$settings = InstanceSettings::get();
|
||||
if (! is_null(env('AUTOUPDATE', null))) {
|
||||
if (env('AUTOUPDATE') == true) {
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
}
|
||||
}
|
||||
if (isCloud()) {
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
if ($response->successful()) {
|
||||
$services = $response->json();
|
||||
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
$this->cleanup_stucked_helper_containers();
|
||||
$this->call('cleanup:stucked-resources');
|
||||
}
|
||||
|
||||
private function update_traefik_labels()
|
||||
@@ -108,25 +105,23 @@ class Init extends Command
|
||||
|
||||
private function cleanup_unnecessary_dynamic_proxy_configuration()
|
||||
{
|
||||
if (isCloud()) {
|
||||
foreach ($this->servers as $server) {
|
||||
try {
|
||||
if (! $server->isFunctional()) {
|
||||
continue;
|
||||
}
|
||||
if ($server->id === 0) {
|
||||
continue;
|
||||
}
|
||||
$file = $server->proxyPath().'/dynamic/coolify.yaml';
|
||||
|
||||
return instant_remote_process([
|
||||
"rm -f $file",
|
||||
], $server, false);
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
|
||||
foreach ($this->servers as $server) {
|
||||
try {
|
||||
if (! $server->isFunctional()) {
|
||||
continue;
|
||||
}
|
||||
if ($server->id === 0) {
|
||||
continue;
|
||||
}
|
||||
$file = $server->proxyPath().'/dynamic/coolify.yaml';
|
||||
|
||||
return instant_remote_process([
|
||||
"rm -f $file",
|
||||
], $server, false);
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,39 +167,32 @@ class Init extends Command
|
||||
|
||||
private function restore_coolify_db_backup()
|
||||
{
|
||||
try {
|
||||
$database = StandalonePostgresql::withTrashed()->find(0);
|
||||
if ($database && $database->trashed()) {
|
||||
echo "Restoring coolify db backup\n";
|
||||
$database->restore();
|
||||
$scheduledBackup = ScheduledDatabaseBackup::find(0);
|
||||
if (! $scheduledBackup) {
|
||||
ScheduledDatabaseBackup::create([
|
||||
'id' => 0,
|
||||
'enabled' => true,
|
||||
'save_s3' => false,
|
||||
'frequency' => '0 0 * * *',
|
||||
'database_id' => $database->id,
|
||||
'database_type' => 'App\Models\StandalonePostgresql',
|
||||
'team_id' => 0,
|
||||
]);
|
||||
if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
|
||||
try {
|
||||
$database = StandalonePostgresql::withTrashed()->find(0);
|
||||
if ($database && $database->trashed()) {
|
||||
echo "Restoring coolify db backup\n";
|
||||
$database->restore();
|
||||
$scheduledBackup = ScheduledDatabaseBackup::find(0);
|
||||
if (! $scheduledBackup) {
|
||||
ScheduledDatabaseBackup::create([
|
||||
'id' => 0,
|
||||
'enabled' => true,
|
||||
'save_s3' => false,
|
||||
'frequency' => '0 0 * * *',
|
||||
'database_id' => $database->id,
|
||||
'database_type' => 'App\Models\StandalonePostgresql',
|
||||
'team_id' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup_stucked_helper_containers()
|
||||
{
|
||||
foreach ($this->servers as $server) {
|
||||
if ($server->isFunctional()) {
|
||||
CleanupHelperContainersJob::dispatch($server);
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function alive()
|
||||
private function send_alive_signal()
|
||||
{
|
||||
$id = config('app.id');
|
||||
$version = config('version');
|
||||
@@ -222,23 +210,7 @@ class Init extends Command
|
||||
echo "Error in alive: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
// private function cleanup_ssh()
|
||||
// {
|
||||
|
||||
// TODO: it will cleanup id.root@host.docker.internal
|
||||
// try {
|
||||
// $files = Storage::allFiles('ssh/keys');
|
||||
// foreach ($files as $file) {
|
||||
// Storage::delete($file);
|
||||
// }
|
||||
// $files = Storage::allFiles('ssh/mux');
|
||||
// foreach ($files as $file) {
|
||||
// Storage::delete($file);
|
||||
// }
|
||||
// } catch (\Throwable $e) {
|
||||
// echo "Error in cleaning ssh: {$e->getMessage()}\n";
|
||||
// }
|
||||
// }
|
||||
private function cleanup_in_progress_application_deployments()
|
||||
{
|
||||
// Cleanup any failed deployments
|
||||
@@ -260,11 +232,13 @@ class Init extends Command
|
||||
|
||||
private function replace_slash_in_environment_name()
|
||||
{
|
||||
$environments = Environment::all();
|
||||
foreach ($environments as $environment) {
|
||||
if (str_contains($environment->name, '/')) {
|
||||
$environment->name = str_replace('/', '-', $environment->name);
|
||||
$environment->save();
|
||||
if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
|
||||
$environments = Environment::all();
|
||||
foreach ($environments as $environment) {
|
||||
if (str_contains($environment->name, '/')) {
|
||||
$environment->name = str_replace('/', '-', $environment->name);
|
||||
$environment->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
app/Console/Commands/OpenApi.php
Normal file
26
app/Console/Commands/OpenApi.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class OpenApi extends Command
|
||||
{
|
||||
protected $signature = 'openapi';
|
||||
|
||||
protected $description = 'Generate OpenApi file.';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
// Generate OpenAPI documentation
|
||||
echo "Generating OpenAPI documentation.\n";
|
||||
$process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
|
||||
$error = $process->errorOutput();
|
||||
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
|
||||
$error = preg_replace('/^\h*\v+/m', '', $error);
|
||||
echo $error;
|
||||
echo $process->output();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class SyncBunny extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'sync:bunny {--templates} {--release}';
|
||||
protected $signature = 'sync:bunny {--templates} {--release} {--nightly}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -33,6 +33,7 @@ class SyncBunny extends Command
|
||||
$that = $this;
|
||||
$only_template = $this->option('templates');
|
||||
$only_version = $this->option('release');
|
||||
$nightly = $this->option('nightly');
|
||||
$bunny_cdn = 'https://cdn.coollabs.io';
|
||||
$bunny_cdn_path = 'coolify';
|
||||
$bunny_cdn_storage_name = 'coolcdn';
|
||||
@@ -45,9 +46,15 @@ class SyncBunny extends Command
|
||||
$upgrade_script = 'upgrade.sh';
|
||||
$production_env = '.env.production';
|
||||
$service_template = 'service-templates.json';
|
||||
|
||||
$versions = 'versions.json';
|
||||
|
||||
$compose_file_location = "$parent_dir/$compose_file";
|
||||
$compose_file_prod_location = "$parent_dir/$compose_file_prod";
|
||||
$install_script_location = "$parent_dir/scripts/install.sh";
|
||||
$upgrade_script_location = "$parent_dir/scripts/upgrade.sh";
|
||||
$production_env_location = "$parent_dir/.env.production";
|
||||
$versions_location = "$parent_dir/$versions";
|
||||
|
||||
PendingRequest::macro('storage', function ($fileName) use ($that) {
|
||||
$headers = [
|
||||
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
|
||||
@@ -73,8 +80,26 @@ class SyncBunny extends Command
|
||||
]);
|
||||
});
|
||||
try {
|
||||
if ($nightly) {
|
||||
$bunny_cdn_path = 'coolify-nightly';
|
||||
|
||||
$compose_file_location = "$parent_dir/other/nightly/$compose_file";
|
||||
$compose_file_prod_location = "$parent_dir/other/nightly/$compose_file_prod";
|
||||
$production_env_location = "$parent_dir/other/nightly/$production_env";
|
||||
$upgrade_script_location = "$parent_dir/other/nightly/$upgrade_script";
|
||||
$install_script_location = "$parent_dir/other/nightly/$install_script";
|
||||
$versions_location = "$parent_dir/other/nightly/$versions";
|
||||
}
|
||||
if (! $only_template && ! $only_version) {
|
||||
$this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
|
||||
if ($nightly) {
|
||||
$this->info('About to sync files NIGHTLY (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
|
||||
} else {
|
||||
$this->info('About to sync files PRODUCTION (docker-compose.yml, docker-compose.prod.yml, upgrade.sh, install.sh, etc) to BunnyCDN.');
|
||||
}
|
||||
$confirmed = confirm('Are you sure you want to sync?');
|
||||
if (! $confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($only_template) {
|
||||
$this->info('About to sync service-templates.json to BunnyCDN.');
|
||||
@@ -90,8 +115,12 @@ class SyncBunny extends Command
|
||||
|
||||
return;
|
||||
} elseif ($only_version) {
|
||||
$this->info('About to sync versions.json to BunnyCDN.');
|
||||
$file = file_get_contents("$parent_dir/$versions");
|
||||
if ($nightly) {
|
||||
$this->info('About to sync NIGHLTY versions.json to BunnyCDN.');
|
||||
} else {
|
||||
$this->info('About to sync PRODUCTION versions.json to BunnyCDN.');
|
||||
}
|
||||
$file = file_get_contents($versions_location);
|
||||
$json = json_decode($file, true);
|
||||
$actual_version = data_get($json, 'coolify.v4.version');
|
||||
|
||||
@@ -100,7 +129,7 @@ class SyncBunny extends Command
|
||||
return;
|
||||
}
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
$pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
|
||||
$pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
|
||||
]);
|
||||
$this->info('versions.json uploaded & purged...');
|
||||
@@ -109,11 +138,11 @@ class SyncBunny extends Command
|
||||
}
|
||||
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
$pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
|
||||
$pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
|
||||
$pool->storage(fileName: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
|
||||
$pool->storage(fileName: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
|
||||
$pool->storage(fileName: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
|
||||
$pool->storage(fileName: "$compose_file_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
|
||||
$pool->storage(fileName: "$compose_file_prod_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
|
||||
$pool->storage(fileName: "$production_env_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
|
||||
$pool->storage(fileName: "$upgrade_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
|
||||
$pool->storage(fileName: "$install_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
|
||||
]);
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file"),
|
||||
|
||||
@@ -4,9 +4,9 @@ namespace App\Console;
|
||||
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\CleanupStaleMultiplexedConnections;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullCoolifyImageJob;
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
@@ -30,22 +30,26 @@ class Kernel extends ConsoleKernel
|
||||
$this->all_servers = Server::all();
|
||||
$settings = InstanceSettings::get();
|
||||
|
||||
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||
|
||||
if (isDev()) {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyMinute();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
|
||||
|
||||
// Server Jobs
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->check_resources($schedule);
|
||||
$this->check_scheduled_tasks($schedule);
|
||||
$schedule->command('uploads:clear')->everyTwoMinutes();
|
||||
|
||||
$schedule->command('telescope:prune')->daily();
|
||||
|
||||
$schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
|
||||
} else {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->command('cleanup:unreachable-servers')->daily();
|
||||
$schedule->job(new PullCoolifyImageJob)->cron($settings->update_check_frequency)->onOneServer();
|
||||
$schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->onOneServer();
|
||||
$schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
|
||||
$schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
|
||||
$this->schedule_updates($schedule);
|
||||
|
||||
@@ -66,10 +70,20 @@ class Kernel extends ConsoleKernel
|
||||
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
|
||||
foreach ($servers as $server) {
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$schedule->job(new PullSentinelImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
|
||||
$schedule->job(function () use ($server) {
|
||||
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false);
|
||||
$sentinel_found = json_decode($sentinel_found, true);
|
||||
$status = data_get($sentinel_found, '0.State.Status', 'exited');
|
||||
if ($status !== 'running') {
|
||||
PullSentinelImageJob::dispatch($server);
|
||||
}
|
||||
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
|
||||
}
|
||||
$schedule->job(new PullHelperImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
|
||||
}
|
||||
$schedule->job(new PullHelperImageJob)
|
||||
->cron($settings->update_check_frequency)
|
||||
->timezone($settings->instance_timezone)
|
||||
->onOneServer();
|
||||
}
|
||||
|
||||
private function schedule_updates($schedule)
|
||||
@@ -77,11 +91,17 @@ class Kernel extends ConsoleKernel
|
||||
$settings = InstanceSettings::get();
|
||||
|
||||
$updateCheckFrequency = $settings->update_check_frequency;
|
||||
$schedule->job(new CheckForUpdatesJob)->cron($updateCheckFrequency)->onOneServer();
|
||||
$schedule->job(new CheckForUpdatesJob)
|
||||
->cron($updateCheckFrequency)
|
||||
->timezone($settings->instance_timezone)
|
||||
->onOneServer();
|
||||
|
||||
if ($settings->is_auto_update_enabled) {
|
||||
$autoUpdateFrequency = $settings->auto_update_frequency;
|
||||
$schedule->job(new UpdateCoolifyJob)->cron($autoUpdateFrequency)->onOneServer();
|
||||
$schedule->job(new UpdateCoolifyJob)
|
||||
->cron($autoUpdateFrequency)
|
||||
->timezone($settings->instance_timezone)
|
||||
->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +116,12 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
|
||||
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->onOneServer();
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
if ($server->settings->force_docker_cleanup) {
|
||||
$schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
|
||||
} else {
|
||||
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,12 +142,19 @@ class Kernel extends ConsoleKernel
|
||||
continue;
|
||||
}
|
||||
|
||||
$server = $scheduled_backup->server();
|
||||
|
||||
if (! $server) {
|
||||
continue;
|
||||
}
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
}
|
||||
$schedule->job(new DatabaseBackupJob(
|
||||
backup: $scheduled_backup
|
||||
))->cron($scheduled_backup->frequency)->onOneServer();
|
||||
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,12 +187,19 @@ class Kernel extends ConsoleKernel
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$server = $scheduled_task->server();
|
||||
if (! $server) {
|
||||
continue;
|
||||
}
|
||||
$serverTimezone = $server->settings->server_timezone ?: config('app.timezone');
|
||||
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
|
||||
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
|
||||
}
|
||||
$schedule->job(new ScheduledTaskJob(
|
||||
task: $scheduled_task
|
||||
))->cron($scheduled_task->frequency)->onOneServer();
|
||||
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
34
app/Events/CloudflareTunnelConfigured.php
Normal file
34
app/Events/CloudflareTunnelConfigured.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CloudflareTunnelConfigured implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
184
app/Helpers/SshMultiplexingHelper.php
Normal file
184
app/Helpers/SshMultiplexingHelper.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class SshMultiplexingHelper
|
||||
{
|
||||
public static function serverSshConfiguration(Server $server)
|
||||
{
|
||||
$privateKey = PrivateKey::findOrFail($server->private_key_id);
|
||||
$sshKeyLocation = $privateKey->getKeyLocation();
|
||||
$muxFilename = '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid;
|
||||
|
||||
return [
|
||||
'sshKeyLocation' => $sshKeyLocation,
|
||||
'muxFilename' => $muxFilename,
|
||||
];
|
||||
}
|
||||
|
||||
public static function ensureMultiplexedConnection(Server $server)
|
||||
{
|
||||
if (! self::isMultiplexingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
|
||||
self::validateSshKey($sshKeyLocation);
|
||||
|
||||
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||
}
|
||||
$checkCommand .= "{$server->user}@{$server->ip}";
|
||||
$process = Process::run($checkCommand);
|
||||
|
||||
if ($process->exitCode() !== 0) {
|
||||
self::establishNewMultiplexedConnection($server);
|
||||
}
|
||||
}
|
||||
|
||||
public static function establishNewMultiplexedConnection(Server $server)
|
||||
{
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$connectionTimeout = config('constants.ssh.connection_timeout');
|
||||
$serverInterval = config('constants.ssh.server_interval');
|
||||
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||
|
||||
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||
}
|
||||
|
||||
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
|
||||
$establishCommand .= "{$server->user}@{$server->ip}";
|
||||
|
||||
$establishProcess = Process::run($establishCommand);
|
||||
|
||||
if ($establishProcess->exitCode() !== 0) {
|
||||
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
|
||||
}
|
||||
}
|
||||
|
||||
public static function removeMuxFile(Server $server)
|
||||
{
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||
}
|
||||
$closeCommand .= "{$server->user}@{$server->ip}";
|
||||
Process::run($closeCommand);
|
||||
}
|
||||
|
||||
public static function generateScpCommand(Server $server, string $source, string $dest)
|
||||
{
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$timeout = config('constants.ssh.command_timeout');
|
||||
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||
|
||||
$scp_command = "timeout $timeout scp ";
|
||||
|
||||
if (self::isMultiplexingEnabled()) {
|
||||
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||
self::ensureMultiplexedConnection($server);
|
||||
}
|
||||
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||
}
|
||||
|
||||
$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}";
|
||||
|
||||
return $scp_command;
|
||||
}
|
||||
|
||||
public static function generateSshCommand(Server $server, string $command)
|
||||
{
|
||||
if ($server->settings->force_disabled) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$timeout = config('constants.ssh.command_timeout');
|
||||
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||
|
||||
$ssh_command = "timeout $timeout ssh ";
|
||||
|
||||
if (self::isMultiplexingEnabled()) {
|
||||
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||
self::ensureMultiplexedConnection($server);
|
||||
}
|
||||
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
|
||||
}
|
||||
|
||||
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
|
||||
|
||||
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
|
||||
$delimiter = Hash::make($command);
|
||||
$command = str_replace($delimiter, '', $command);
|
||||
|
||||
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
|
||||
.$command.PHP_EOL
|
||||
.$delimiter;
|
||||
|
||||
return $ssh_command;
|
||||
}
|
||||
|
||||
private static function isMultiplexingEnabled(): bool
|
||||
{
|
||||
return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
|
||||
}
|
||||
|
||||
private static function validateSshKey(string $sshKeyLocation): void
|
||||
{
|
||||
$checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
|
||||
$keyCheckProcess = Process::run($checkKeyCommand);
|
||||
|
||||
if ($keyCheckProcess->exitCode() !== 0) {
|
||||
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
|
||||
}
|
||||
}
|
||||
|
||||
private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
|
||||
{
|
||||
$options = "-i {$sshKeyLocation} "
|
||||
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
|
||||
.'-o PasswordAuthentication=no '
|
||||
."-o ConnectTimeout=$connectionTimeout "
|
||||
."-o ServerAliveInterval=$serverInterval "
|
||||
.'-o RequestTTY=no '
|
||||
.'-o LogLevel=ERROR ';
|
||||
|
||||
// Bruh
|
||||
if ($isScp) {
|
||||
$options .= "-P {$server->port} ";
|
||||
} else {
|
||||
$options .= "-p {$server->port} ";
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'List',
|
||||
description: 'List all applications.',
|
||||
path: '/applications',
|
||||
operationId: 'list-applications',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -101,6 +102,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Create (Public)',
|
||||
description: 'Create new application based on a public git repository.',
|
||||
path: '/applications/public',
|
||||
operationId: 'create-public-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -201,7 +203,8 @@ class ApplicationsController extends Controller
|
||||
#[OA\Post(
|
||||
summary: 'Create (Private - GH App)',
|
||||
description: 'Create new application based on a private repository through a Github App.',
|
||||
path: '/applications/private-gh-app',
|
||||
path: '/applications/private-github-app',
|
||||
operationId: 'create-private-github-app-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -303,6 +306,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Create (Private - Deploy Key)',
|
||||
description: 'Create new application based on a private repository through a Deploy Key.',
|
||||
path: '/applications/private-deploy-key',
|
||||
operationId: 'create-private-deploy-key-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -404,6 +408,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Create (Dockerfile)',
|
||||
description: 'Create new application based on a simple Dockerfile.',
|
||||
path: '/applications/dockerfile',
|
||||
operationId: 'create-dockerfile-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -490,6 +495,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Create (Docker Image)',
|
||||
description: 'Create new application based on a prebuilt docker image',
|
||||
path: '/applications/dockerimage',
|
||||
operationId: 'create-dockerimage-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -573,6 +579,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Create (Docker Compose)',
|
||||
description: 'Create new application based on a docker-compose file.',
|
||||
path: '/applications/dockercompose',
|
||||
operationId: 'create-dockercompose-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1171,6 +1178,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Get',
|
||||
description: 'Get application by UUID.',
|
||||
path: '/applications/{uuid}',
|
||||
operationId: 'get-application-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1235,6 +1243,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Delete',
|
||||
description: 'Delete application by UUID.',
|
||||
path: '/applications/{uuid}',
|
||||
operationId: 'delete-application-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1321,6 +1330,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Update',
|
||||
description: 'Update application by UUID.',
|
||||
path: '/applications/{uuid}',
|
||||
operationId: 'update-application-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1450,7 +1460,7 @@ class ApplicationsController extends Controller
|
||||
], 404);
|
||||
}
|
||||
$server = $application->destination->server;
|
||||
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect'];
|
||||
$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'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
sharedDataApplications(),
|
||||
@@ -1526,6 +1536,10 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
$request->offsetUnset('docker_compose_domains');
|
||||
}
|
||||
$instantDeploy = $request->instant_deploy;
|
||||
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
|
||||
$data = $request->all();
|
||||
data_set($data, 'fqdn', $domains);
|
||||
if ($dockerComposeDomainsJson->count() > 0) {
|
||||
@@ -1534,6 +1548,16 @@ class ApplicationsController extends Controller
|
||||
$application->fill($data);
|
||||
$application->save();
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
is_api: true,
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $application->uuid,
|
||||
]);
|
||||
@@ -1543,6 +1567,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'List Envs',
|
||||
description: 'List all envs by application UUID.',
|
||||
path: '/applications/{uuid}/envs',
|
||||
operationId: 'list-envs-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1625,6 +1650,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Update Env',
|
||||
description: 'Update env by application UUID.',
|
||||
path: '/applications/{uuid}/envs',
|
||||
operationId: 'update-env-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1807,6 +1833,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Update Envs (Bulk)',
|
||||
description: 'Update multiple envs by application UUID.',
|
||||
path: '/applications/{uuid}/envs/bulk',
|
||||
operationId: 'update-envs-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1998,6 +2025,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Create Env',
|
||||
description: 'Create env by application UUID.',
|
||||
path: '/applications/{uuid}/envs',
|
||||
operationId: 'create-env-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -2157,6 +2185,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Delete Env',
|
||||
description: 'Delete env by UUID.',
|
||||
path: '/applications/{uuid}/envs/{env_uuid}',
|
||||
operationId: 'delete-env-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -2242,6 +2271,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Start',
|
||||
description: 'Start application. `Post` request is also accepted.',
|
||||
path: '/applications/{uuid}/start',
|
||||
operationId: 'start-application-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -2345,6 +2375,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Stop',
|
||||
description: 'Stop application. `Post` request is also accepted.',
|
||||
path: '/applications/{uuid}/stop',
|
||||
operationId: 'stop-application-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -2417,6 +2448,7 @@ class ApplicationsController extends Controller
|
||||
summary: 'Restart',
|
||||
description: 'Restart application. `Post` request is also accepted.',
|
||||
path: '/applications/{uuid}/restart',
|
||||
operationId: 'restart-application-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -2497,6 +2529,131 @@ 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',
|
||||
]);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
$commands = collect([
|
||||
executeInDocker($container['Names'], $request->command),
|
||||
]);
|
||||
|
||||
$res = instant_remote_process(command: $commands, server: $application->destination->server);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Command executed.',
|
||||
'response' => $res,
|
||||
]);
|
||||
}
|
||||
|
||||
private function validateDataApplications(Request $request, Server $server)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
|
||||
@@ -46,6 +46,7 @@ class DatabasesController extends Controller
|
||||
summary: 'List',
|
||||
description: 'List all databases.',
|
||||
path: '/databases',
|
||||
operationId: 'list-databases',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -91,6 +92,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Get',
|
||||
description: 'Get database by UUID.',
|
||||
path: '/databases/{uuid}',
|
||||
operationId: 'get-database-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -151,6 +153,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Update',
|
||||
description: 'Update database by UUID.',
|
||||
path: '/databases/{uuid}',
|
||||
operationId: 'update-database-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -510,6 +513,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Create (PostgreSQL)',
|
||||
description: 'Create a new PostgreSQL database.',
|
||||
path: '/databases/postgresql',
|
||||
operationId: 'create-database-postgresql',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -575,6 +579,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Create (Clickhouse)',
|
||||
description: 'Create a new Clickhouse database.',
|
||||
path: '/databases/clickhouse',
|
||||
operationId: 'create-database-clickhouse',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -636,6 +641,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Create (DragonFly)',
|
||||
description: 'Create a new DragonFly database.',
|
||||
path: '/databases/dragonfly',
|
||||
operationId: 'create-database-dragonfly',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -696,6 +702,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Create (Redis)',
|
||||
description: 'Create a new Redis database.',
|
||||
path: '/databases/redis',
|
||||
operationId: 'create-database-redis',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -757,6 +764,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Create (KeyDB)',
|
||||
description: 'Create a new KeyDB database.',
|
||||
path: '/databases/keydb',
|
||||
operationId: 'create-database-keydb',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -818,6 +826,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Create (MariaDB)',
|
||||
description: 'Create a new MariaDB database.',
|
||||
path: '/databases/mariadb',
|
||||
operationId: 'create-database-mariadb',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -882,6 +891,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Create (MySQL)',
|
||||
description: 'Create a new MySQL database.',
|
||||
path: '/databases/mysql',
|
||||
operationId: 'create-database-mysql',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -945,6 +955,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Create (MongoDB)',
|
||||
description: 'Create a new MongoDB database.',
|
||||
path: '/databases/mongodb',
|
||||
operationId: 'create-database-mongodb',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1514,6 +1525,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Delete',
|
||||
description: 'Delete database by UUID.',
|
||||
path: '/databases/{uuid}',
|
||||
operationId: 'delete-database-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1597,6 +1609,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Start',
|
||||
description: 'Start database. `Post` request is also accepted.',
|
||||
path: '/databases/{uuid}/start',
|
||||
operationId: 'start-database-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1672,6 +1685,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Stop',
|
||||
description: 'Stop database. `Post` request is also accepted.',
|
||||
path: '/databases/{uuid}/stop',
|
||||
operationId: 'stop-database-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -1747,6 +1761,7 @@ class DatabasesController extends Controller
|
||||
summary: 'Restart',
|
||||
description: 'Restart database. `Post` request is also accepted.',
|
||||
path: '/databases/{uuid}/restart',
|
||||
operationId: 'restart-database-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
|
||||
@@ -32,6 +32,7 @@ class DeployController extends Controller
|
||||
summary: 'List',
|
||||
description: 'List currently running deployments',
|
||||
path: '/deployments',
|
||||
operationId: 'list-deployments',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -79,12 +80,13 @@ class DeployController extends Controller
|
||||
summary: 'Get',
|
||||
description: 'Get deployment by UUID.',
|
||||
path: '/deployments/{uuid}',
|
||||
operationId: 'get-deployment-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Deployments'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -134,6 +136,7 @@ class DeployController extends Controller
|
||||
summary: 'Deploy',
|
||||
description: 'Deploy by tag or uuid. `Post` request also accepted.',
|
||||
path: '/deploy',
|
||||
operationId: 'deploy-by-tag-or-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -147,7 +150,7 @@ class DeployController extends Controller
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get deployment(s) Uuid\'s',
|
||||
description: 'Get deployment(s) UUID\'s',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EnvironmentVariablesController extends Controller
|
||||
{
|
||||
public function delete_env_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$env = EnvironmentVariable::where('uuid', $request->env_uuid)->first();
|
||||
if (! $env) {
|
||||
return response()->json([
|
||||
'message' => 'Environment variable not found.',
|
||||
], 404);
|
||||
}
|
||||
$found_app = $env->resource()->whereRelation('environment.project.team', 'id', $teamId)->first();
|
||||
if (! $found_app) {
|
||||
return response()->json([
|
||||
'message' => 'Environment variable not found.',
|
||||
], 404);
|
||||
}
|
||||
$env->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Environment variable deleted.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ class OtherController extends Controller
|
||||
summary: 'Version',
|
||||
description: 'Get Coolify version.',
|
||||
path: '/version',
|
||||
operationId: 'version',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -43,6 +44,7 @@ class OtherController extends Controller
|
||||
summary: 'Enable API',
|
||||
description: 'Enable API (only with root permissions).',
|
||||
path: '/enable',
|
||||
operationId: 'enable-api',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -94,6 +96,7 @@ class OtherController extends Controller
|
||||
summary: 'Disable API',
|
||||
description: 'Disable API (only with root permissions).',
|
||||
path: '/disable',
|
||||
operationId: 'disable-api',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -158,6 +161,7 @@ class OtherController extends Controller
|
||||
summary: 'Healthcheck',
|
||||
description: 'Healthcheck endpoint.',
|
||||
path: '/healthcheck',
|
||||
operationId: 'healthcheck',
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
|
||||
@@ -11,8 +11,9 @@ class ProjectController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
summary: 'List',
|
||||
description: 'list projects.',
|
||||
description: 'List projects.',
|
||||
path: '/projects',
|
||||
operationId: 'list-projects',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -46,7 +47,7 @@ class ProjectController extends Controller
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$projects = Project::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
|
||||
$projects = Project::whereTeamId($teamId)->select('id', 'name', 'description', 'uuid')->get();
|
||||
|
||||
return response()->json(serializeApiResponse($projects),
|
||||
);
|
||||
@@ -54,8 +55,9 @@ class ProjectController extends Controller
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Get',
|
||||
description: 'Get project by Uuid.',
|
||||
description: 'Get project by UUID.',
|
||||
path: '/projects/{uuid}',
|
||||
operationId: 'get-project-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -102,6 +104,7 @@ class ProjectController extends Controller
|
||||
summary: 'Environment',
|
||||
description: 'Get environment by name.',
|
||||
path: '/projects/{uuid}/{environment_name}',
|
||||
operationId: 'get-environment-by-name',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -136,12 +139,15 @@ class ProjectController extends Controller
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'Uuid is required.'], 422);
|
||||
return response()->json(['message' => 'UUID is required.'], 422);
|
||||
}
|
||||
if (! $request->environment_name) {
|
||||
return response()->json(['message' => 'Environment name is required.'], 422);
|
||||
}
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $project) {
|
||||
return response()->json(['message' => 'Project not found.'], 404);
|
||||
}
|
||||
$environment = $project->environments()->whereName($request->environment_name)->first();
|
||||
if (! $environment) {
|
||||
return response()->json(['message' => 'Environment not found.'], 404);
|
||||
@@ -155,6 +161,7 @@ class ProjectController extends Controller
|
||||
summary: 'Create',
|
||||
description: 'Create Project.',
|
||||
path: '/projects',
|
||||
operationId: 'create-project',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -167,7 +174,7 @@ class ProjectController extends Controller
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'description' => 'The name of the project.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the project.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The description of the project.'],
|
||||
],
|
||||
),
|
||||
@@ -250,6 +257,7 @@ class ProjectController extends Controller
|
||||
summary: 'Update',
|
||||
description: 'Update Project.',
|
||||
path: '/projects/{uuid}',
|
||||
operationId: 'update-project-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -333,7 +341,7 @@ class ProjectController extends Controller
|
||||
}
|
||||
$uuid = $request->uuid;
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'Uuid is required.'], 422);
|
||||
return response()->json(['message' => 'UUID is required.'], 422);
|
||||
}
|
||||
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($uuid)->first();
|
||||
@@ -355,6 +363,7 @@ class ProjectController extends Controller
|
||||
summary: 'Delete',
|
||||
description: 'Delete project by UUID.',
|
||||
path: '/projects/{uuid}',
|
||||
operationId: 'delete-project-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -408,7 +417,7 @@ class ProjectController extends Controller
|
||||
}
|
||||
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'Uuid is required.'], 422);
|
||||
return response()->json(['message' => 'UUID is required.'], 422);
|
||||
}
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $project) {
|
||||
|
||||
@@ -13,6 +13,7 @@ class ResourcesController extends Controller
|
||||
summary: 'List',
|
||||
description: 'Get all resources.',
|
||||
path: '/resources',
|
||||
operationId: 'list-resources',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
|
||||
@@ -26,6 +26,7 @@ class SecurityController extends Controller
|
||||
summary: 'List',
|
||||
description: 'List all private keys.',
|
||||
path: '/security/keys',
|
||||
operationId: 'list-private-keys',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -68,12 +69,13 @@ class SecurityController extends Controller
|
||||
summary: 'Get',
|
||||
description: 'Get key by UUID.',
|
||||
path: '/security/keys/{uuid}',
|
||||
operationId: 'get-private-key-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Private Keys'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -124,6 +126,7 @@ class SecurityController extends Controller
|
||||
summary: 'Create',
|
||||
description: 'Create a new private key.',
|
||||
path: '/security/keys',
|
||||
operationId: 'create-private-key',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -217,6 +220,7 @@ class SecurityController extends Controller
|
||||
summary: 'Update',
|
||||
description: 'Update a private key.',
|
||||
path: '/security/keys',
|
||||
operationId: 'update-private-key',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -313,12 +317,13 @@ class SecurityController extends Controller
|
||||
summary: 'Delete',
|
||||
description: 'Delete a private key.',
|
||||
path: '/security/keys/{uuid}',
|
||||
operationId: 'delete-private-key-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Private Keys'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
|
||||
@@ -46,6 +46,7 @@ class ServersController extends Controller
|
||||
summary: 'List',
|
||||
description: 'List all servers.',
|
||||
path: '/servers',
|
||||
operationId: 'list-servers',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -100,12 +101,13 @@ class ServersController extends Controller
|
||||
summary: 'Get',
|
||||
description: 'Get server by UUID.',
|
||||
path: '/servers/{uuid}',
|
||||
operationId: 'get-server-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Servers'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -177,12 +179,13 @@ class ServersController extends Controller
|
||||
summary: 'Resources',
|
||||
description: 'Get resources by server.',
|
||||
path: '/servers/{uuid}/resources',
|
||||
operationId: 'get-resources-by-server-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Servers'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -254,12 +257,13 @@ class ServersController extends Controller
|
||||
summary: 'Domains',
|
||||
description: 'Get domains by server.',
|
||||
path: '/servers/{uuid}/domains',
|
||||
operationId: 'get-domains-by-server-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Servers'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
@@ -401,6 +405,7 @@ class ServersController extends Controller
|
||||
summary: 'Create',
|
||||
description: 'Create Server.',
|
||||
path: '/servers',
|
||||
operationId: 'create-server',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -545,6 +550,7 @@ class ServersController extends Controller
|
||||
summary: 'Update',
|
||||
description: 'Update Server.',
|
||||
path: '/servers/{uuid}',
|
||||
operationId: 'update-server-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -655,6 +661,7 @@ class ServersController extends Controller
|
||||
summary: 'Delete',
|
||||
description: 'Delete server by UUID.',
|
||||
path: '/servers/{uuid}',
|
||||
operationId: 'delete-server-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -727,6 +734,7 @@ class ServersController extends Controller
|
||||
summary: 'Validate',
|
||||
description: 'Validate server by UUID.',
|
||||
path: '/servers/{uuid}/validate',
|
||||
operationId: 'validate-server-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
|
||||
@@ -38,6 +38,7 @@ class ServicesController extends Controller
|
||||
summary: 'List',
|
||||
description: 'List all services.',
|
||||
path: '/services',
|
||||
operationId: 'list-services',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -88,6 +89,7 @@ class ServicesController extends Controller
|
||||
summary: 'Create',
|
||||
description: 'Create a one-click service',
|
||||
path: '/services',
|
||||
operationId: 'create-service',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -365,6 +367,7 @@ class ServicesController extends Controller
|
||||
summary: 'Get',
|
||||
description: 'Get service by UUID.',
|
||||
path: '/services/{uuid}',
|
||||
operationId: 'get-service-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -375,7 +378,7 @@ class ServicesController extends Controller
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get a service by Uuid.',
|
||||
description: 'Get a service by UUID.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
@@ -422,6 +425,7 @@ class ServicesController extends Controller
|
||||
summary: 'Delete',
|
||||
description: 'Delete service by UUID.',
|
||||
path: '/services/{uuid}',
|
||||
operationId: 'delete-service-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -432,7 +436,7 @@ class ServicesController extends Controller
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Delete a service by Uuid',
|
||||
description: 'Delete a service by UUID',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
@@ -479,10 +483,521 @@ class ServicesController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Envs',
|
||||
description: 'List all envs by service UUID.',
|
||||
path: '/services/{uuid}/envs',
|
||||
operationId: 'list-envs-by-service-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',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'All environment variables by service UUID.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
|
||||
)
|
||||
),
|
||||
]),
|
||||
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 envs(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
$envs = $service->environment_variables->map(function ($env) {
|
||||
$env->makeHidden([
|
||||
'application_id',
|
||||
'standalone_clickhouse_id',
|
||||
'standalone_dragonfly_id',
|
||||
'standalone_keydb_id',
|
||||
'standalone_mariadb_id',
|
||||
'standalone_mongodb_id',
|
||||
'standalone_mysql_id',
|
||||
'standalone_postgresql_id',
|
||||
'standalone_redis_id',
|
||||
]);
|
||||
$env = $this->removeSensitiveData($env);
|
||||
|
||||
return $env;
|
||||
});
|
||||
|
||||
return response()->json($envs);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update Env',
|
||||
description: 'Update env by service UUID.',
|
||||
path: '/services/{uuid}/envs',
|
||||
operationId: 'update-env-by-service-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: 'Env updated.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['key', 'value'],
|
||||
properties: [
|
||||
'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
|
||||
'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
|
||||
'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'],
|
||||
'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'],
|
||||
'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
|
||||
'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
|
||||
'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Environment variable updated.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Environment variable updated.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
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_env_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'key' => 'string|required',
|
||||
'value' => 'string|nullable',
|
||||
'is_build_time' => 'boolean',
|
||||
'is_literal' => 'boolean',
|
||||
'is_multiline' => 'boolean',
|
||||
'is_shown_once' => 'boolean',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$env = $service->environment_variables()->where('key', $request->key)->first();
|
||||
if (! $env) {
|
||||
return response()->json(['message' => 'Environment variable not found.'], 404);
|
||||
}
|
||||
|
||||
$env->fill($request->all());
|
||||
$env->save();
|
||||
|
||||
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update Envs (Bulk)',
|
||||
description: 'Update multiple envs by service UUID.',
|
||||
path: '/services/{uuid}/envs/bulk',
|
||||
operationId: 'update-envs-by-service-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: 'Bulk envs updated.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['data'],
|
||||
properties: [
|
||||
'data' => [
|
||||
'type' => 'array',
|
||||
'items' => new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
|
||||
'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
|
||||
'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'],
|
||||
'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'],
|
||||
'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
|
||||
'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
|
||||
'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Environment variables updated.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Environment variables updated.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
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 create_bulk_envs(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
$bulk_data = $request->get('data');
|
||||
if (! $bulk_data) {
|
||||
return response()->json(['message' => 'Bulk data is required.'], 400);
|
||||
}
|
||||
|
||||
$updatedEnvs = collect();
|
||||
foreach ($bulk_data as $item) {
|
||||
$validator = customApiValidator($item, [
|
||||
'key' => 'string|required',
|
||||
'value' => 'string|nullable',
|
||||
'is_build_time' => 'boolean',
|
||||
'is_literal' => 'boolean',
|
||||
'is_multiline' => 'boolean',
|
||||
'is_shown_once' => 'boolean',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$env = $service->environment_variables()->updateOrCreate(
|
||||
['key' => $item['key']],
|
||||
$item
|
||||
);
|
||||
|
||||
$updatedEnvs->push($this->removeSensitiveData($env));
|
||||
}
|
||||
|
||||
return response()->json($updatedEnvs)->setStatusCode(201);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Env',
|
||||
description: 'Create env by service UUID.',
|
||||
path: '/services/{uuid}/envs',
|
||||
operationId: 'create-env-by-service-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(
|
||||
required: true,
|
||||
description: 'Env created.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
|
||||
'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
|
||||
'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'],
|
||||
'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'],
|
||||
'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
|
||||
'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
|
||||
'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Environment variable created.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'example' => 'nc0k04gk8g0cgsk440g0koko'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
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 create_env(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'key' => 'string|required',
|
||||
'value' => 'string|nullable',
|
||||
'is_build_time' => 'boolean',
|
||||
'is_literal' => 'boolean',
|
||||
'is_multiline' => 'boolean',
|
||||
'is_shown_once' => 'boolean',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$existingEnv = $service->environment_variables()->where('key', $request->key)->first();
|
||||
if ($existingEnv) {
|
||||
return response()->json([
|
||||
'message' => 'Environment variable already exists. Use PATCH request to update it.',
|
||||
], 409);
|
||||
}
|
||||
|
||||
$env = $service->environment_variables()->create($request->all());
|
||||
|
||||
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete Env',
|
||||
description: 'Delete env by UUID.',
|
||||
path: '/services/{uuid}/envs/{env_uuid}',
|
||||
operationId: 'delete-env-by-service-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',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'env_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the environment variable.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Environment variable deleted.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Environment variable deleted.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
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 delete_env_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
$env = EnvironmentVariable::where('uuid', $request->env_uuid)
|
||||
->where('service_id', $service->id)
|
||||
->first();
|
||||
|
||||
if (! $env) {
|
||||
return response()->json(['message' => 'Environment variable not found.'], 404);
|
||||
}
|
||||
|
||||
$env->forceDelete();
|
||||
|
||||
return response()->json(['message' => 'Environment variable deleted.']);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Start',
|
||||
description: 'Start service. `Post` request is also accepted.',
|
||||
path: '/services/{uuid}/start',
|
||||
operationId: 'start-service-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -558,6 +1073,7 @@ class ServicesController extends Controller
|
||||
summary: 'Stop',
|
||||
description: 'Stop service. `Post` request is also accepted.',
|
||||
path: '/services/{uuid}/stop',
|
||||
operationId: 'stop-service-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -633,6 +1149,7 @@ class ServicesController extends Controller
|
||||
summary: 'Restart',
|
||||
description: 'Restart service. `Post` request is also accepted.',
|
||||
path: '/services/{uuid}/restart',
|
||||
operationId: 'restart-service-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
|
||||
@@ -32,6 +32,7 @@ class TeamController extends Controller
|
||||
summary: 'List',
|
||||
description: 'Get all teams.',
|
||||
path: '/teams',
|
||||
operationId: 'list-teams',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -79,6 +80,7 @@ class TeamController extends Controller
|
||||
summary: 'Get',
|
||||
description: 'Get team by TeamId.',
|
||||
path: '/teams/{id}',
|
||||
operationId: 'get-team-by-id',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -129,6 +131,7 @@ class TeamController extends Controller
|
||||
summary: 'Members',
|
||||
description: 'Get members by TeamId.',
|
||||
path: '/teams/{id}/members',
|
||||
operationId: 'get-members-by-team-id',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -189,6 +192,7 @@ class TeamController extends Controller
|
||||
summary: 'Authenticated Team',
|
||||
description: 'Get currently authenticated team.',
|
||||
path: '/teams/current',
|
||||
operationId: 'get-current-team',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
@@ -225,6 +229,7 @@ class TeamController extends Controller
|
||||
summary: 'Authenticated Team Members',
|
||||
description: 'Get currently authenticated team members.',
|
||||
path: '/teams/current/members',
|
||||
operationId: 'get-current-team-members',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Models\ApplicationPreview;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
@@ -26,6 +27,7 @@ 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;
|
||||
@@ -109,10 +111,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private bool $is_debug_enabled;
|
||||
|
||||
private $build_args;
|
||||
private Collection|string $build_args;
|
||||
|
||||
private $env_args;
|
||||
|
||||
private $environment_variables;
|
||||
|
||||
private $env_nixpacks_args;
|
||||
|
||||
private $docker_compose;
|
||||
@@ -157,7 +161,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private ?string $coolify_variables = null;
|
||||
|
||||
private bool $preserveRepository = true;
|
||||
private bool $preserveRepository = false;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
@@ -166,6 +170,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
|
||||
$this->application = Application::find($this->application_deployment_queue->application_id);
|
||||
$this->build_pack = data_get($this->application, 'build_pack');
|
||||
$this->build_args = collect([]);
|
||||
|
||||
$this->application_deployment_queue_id = $application_deployment_queue_id;
|
||||
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
|
||||
@@ -198,11 +203,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
|
||||
if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) {
|
||||
$this->container_name = $this->application->settings->custom_internal_name;
|
||||
if ($this->pull_request_id === 0) {
|
||||
$this->container_name = $this->application->settings->custom_internal_name;
|
||||
} else {
|
||||
$this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}";
|
||||
}
|
||||
}
|
||||
ray('New container name: ', $this->container_name);
|
||||
ray('New container name: ', $this->container_name)->green();
|
||||
|
||||
savePrivateKeyToFs($this->server);
|
||||
$this->saved_outputs = collect();
|
||||
|
||||
// Set preview fqdn
|
||||
@@ -276,6 +284,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->original_server = $this->server;
|
||||
} else {
|
||||
$this->build_server = $buildServers->random();
|
||||
$this->application_deployment_queue->build_server_id = $this->build_server->id;
|
||||
$this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name}).");
|
||||
$this->original_server = $this->server;
|
||||
$this->use_build_server = true;
|
||||
@@ -414,15 +423,42 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->clone_repository();
|
||||
if ($this->preserveRepository) {
|
||||
foreach ($this->application->fileStorages as $fileStorage) {
|
||||
$path = $fileStorage->fs_path;
|
||||
$saveName = 'file_stat_'.$fileStorage->id;
|
||||
$realPathInGit = str($path)->replace($this->application->workdir(), $this->workdir)->value();
|
||||
// check if the file is a directory or a file inside the repository
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "stat -c '%F' {$realPathInGit}"), 'hidden' => true, 'ignore_errors' => true, 'save' => $saveName]
|
||||
);
|
||||
if ($this->saved_outputs->has($saveName)) {
|
||||
$fileStat = $this->saved_outputs->get($saveName);
|
||||
if ($fileStat->value() === 'directory' && ! $fileStorage->is_directory) {
|
||||
$fileStorage->is_directory = true;
|
||||
$fileStorage->content = null;
|
||||
$fileStorage->save();
|
||||
$fileStorage->deleteStorageOnServer();
|
||||
$fileStorage->saveStorageOnServer();
|
||||
} elseif ($fileStat->value() === 'regular file' && $fileStorage->is_directory) {
|
||||
$fileStorage->is_directory = false;
|
||||
$fileStorage->is_based_on_git = true;
|
||||
$fileStorage->save();
|
||||
$fileStorage->deleteStorageOnServer();
|
||||
$fileStorage->saveStorageOnServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->generate_image_names();
|
||||
$this->cleanup_git();
|
||||
$this->application->loadComposeFile(isInit: false);
|
||||
if ($this->application->settings->is_raw_compose_deployment_enabled) {
|
||||
$this->application->parseRawCompose();
|
||||
$this->application->oldRawParser();
|
||||
$yaml = $composeFile = $this->application->docker_compose_raw;
|
||||
$this->save_environment_variables();
|
||||
} else {
|
||||
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id, preview_id: data_get($this, 'preview.id'));
|
||||
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
|
||||
$this->save_environment_variables();
|
||||
if (! is_null($this->env_filename)) {
|
||||
$services = collect($composeFile['services']);
|
||||
@@ -439,11 +475,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
return;
|
||||
}
|
||||
$yaml = Yaml::dump($composeFile->toArray(), 10);
|
||||
$yaml = Yaml::dump(convertToArray($composeFile), 10);
|
||||
}
|
||||
$this->docker_compose_base64 = base64_encode($yaml);
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
// Build new container to limit downtime.
|
||||
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
|
||||
@@ -473,13 +510,18 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
// TODO
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
"docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true", 'hidden' => true, 'ignore_errors' => true,
|
||||
"docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true",
|
||||
'hidden' => true,
|
||||
'ignore_errors' => true,
|
||||
], [
|
||||
"docker network connect {$networkId} coolify-proxy || true", 'hidden' => true, 'ignore_errors' => true,
|
||||
"docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true",
|
||||
'hidden' => true,
|
||||
'ignore_errors' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// Start compose file
|
||||
$server_workdir = $this->application->workdir();
|
||||
if ($this->application->settings->is_raw_compose_deployment_enabled) {
|
||||
if ($this->docker_compose_custom_start_command) {
|
||||
$this->write_deployment_configurations();
|
||||
@@ -488,7 +530,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
);
|
||||
} else {
|
||||
$this->write_deployment_configurations();
|
||||
$server_workdir = $this->application->workdir();
|
||||
$this->docker_compose_location = '/docker-compose.yaml';
|
||||
|
||||
$command = "{$this->coolify_variables} docker compose";
|
||||
@@ -508,15 +549,26 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
);
|
||||
} else {
|
||||
$command = "{$this->coolify_variables} docker compose";
|
||||
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} up -d";
|
||||
if ($this->preserveRepository) {
|
||||
if ($this->env_filename) {
|
||||
$command .= " --env-file {$server_workdir}/{$this->env_filename}";
|
||||
}
|
||||
$command .= " --project-name {$this->application->uuid} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d";
|
||||
$this->write_deployment_configurations();
|
||||
|
||||
$this->write_deployment_configurations();
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
|
||||
);
|
||||
$this->execute_remote_command(
|
||||
['command' => $command, 'hidden' => true],
|
||||
);
|
||||
} else {
|
||||
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} up -d";
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
|
||||
);
|
||||
$this->write_deployment_configurations();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,15 +662,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
[
|
||||
"mkdir -p $this->configuration_dir",
|
||||
],
|
||||
// removing this now as we are using docker cp
|
||||
// [
|
||||
// "rm -rf $this->configuration_dir/{*,.*}",
|
||||
// ],
|
||||
[
|
||||
"docker cp {$this->deployment_uuid}:{$this->workdir}/. {$this->configuration_dir}",
|
||||
],
|
||||
);
|
||||
}
|
||||
foreach ($this->application->fileStorages as $fileStorage) {
|
||||
if (! $fileStorage->is_based_on_git && ! $fileStorage->is_directory) {
|
||||
$fileStorage->saveStorageOnServer();
|
||||
}
|
||||
}
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->build_server;
|
||||
}
|
||||
@@ -698,7 +751,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"),
|
||||
'hidden' => true,
|
||||
],
|
||||
);
|
||||
if ($this->application->docker_registry_image_tag) {
|
||||
@@ -706,10 +760,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with {$this->application->docker_registry_image_tag} tag.");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"),
|
||||
'ignore_errors' => true,
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"),
|
||||
'ignore_errors' => true,
|
||||
'hidden' => true,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -806,14 +864,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
private function check_image_locally_or_remotely()
|
||||
{
|
||||
$this->execute_remote_command([
|
||||
"docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found',
|
||||
"docker images -q {$this->production_image_name} 2>/dev/null",
|
||||
'hidden' => true,
|
||||
'save' => 'local_image_found',
|
||||
]);
|
||||
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
|
||||
$this->execute_remote_command([
|
||||
"docker pull {$this->production_image_name} 2>/dev/null", 'ignore_errors' => true, 'hidden' => true,
|
||||
"docker pull {$this->production_image_name} 2>/dev/null",
|
||||
'ignore_errors' => true,
|
||||
'hidden' => true,
|
||||
]);
|
||||
$this->execute_remote_command([
|
||||
"docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found',
|
||||
"docker images -q {$this->production_image_name} 2>/dev/null",
|
||||
'hidden' => true,
|
||||
'save' => 'local_image_found',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -846,17 +910,24 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
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->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||
$envs->push("COOLIFY_BRANCH={$local_branch}");
|
||||
}
|
||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
|
||||
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_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') {
|
||||
@@ -891,18 +962,31 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
|
||||
$envs->push("COOLIFY_FQDN={$this->application->fqdn}");
|
||||
if ($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://', '');
|
||||
$envs->push("COOLIFY_URL={$url}");
|
||||
if ($this->application->compose_parsing_version === '3') {
|
||||
$envs->push("COOLIFY_FQDN={$url}");
|
||||
} else {
|
||||
$envs->push("COOLIFY_URL={$url}");
|
||||
}
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||
$envs->push("COOLIFY_BRANCH={$local_branch}");
|
||||
}
|
||||
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
|
||||
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_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;
|
||||
if ($env->version === '4.0.0-beta.239') {
|
||||
@@ -979,17 +1063,58 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->environment_variables = $envs;
|
||||
}
|
||||
|
||||
private function elixir_finetunes()
|
||||
{
|
||||
if ($this->pull_request_id === 0) {
|
||||
$envType = 'environment_variables';
|
||||
} else {
|
||||
$envType = 'environment_variables_preview';
|
||||
}
|
||||
$mix_env = $this->application->{$envType}->where('key', 'MIX_ENV')->first();
|
||||
if ($mix_env) {
|
||||
if ($mix_env->is_build_time === false) {
|
||||
$this->application_deployment_queue->addLogEntry('MIX_ENV environment variable is not set as build time.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please set MIX_ENV environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry('MIX_ENV environment variable not found.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please add MIX_ENV environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
$secret_key_base = $this->application->{$envType}->where('key', 'SECRET_KEY_BASE')->first();
|
||||
if ($secret_key_base) {
|
||||
if ($secret_key_base->is_build_time === false) {
|
||||
$this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable is not set as build time.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please set SECRET_KEY_BASE environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable not found.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please add SECRET_KEY_BASE environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
$database_url = $this->application->{$envType}->where('key', 'DATABASE_URL')->first();
|
||||
if ($database_url) {
|
||||
if ($database_url->is_build_time === false) {
|
||||
$this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable is not set as build time.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please set DATABASE_URL environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable not found.', type: 'error');
|
||||
$this->application_deployment_queue->addLogEntry('Please add DATABASE_URL environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
|
||||
}
|
||||
}
|
||||
|
||||
private function laravel_finetunes()
|
||||
{
|
||||
if ($this->pull_request_id === 0) {
|
||||
$nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
|
||||
$nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
|
||||
$envType = 'environment_variables';
|
||||
} else {
|
||||
$nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
|
||||
$nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
|
||||
$envType = 'environment_variables_preview';
|
||||
}
|
||||
$nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
|
||||
$nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
|
||||
|
||||
if (! $nixpacks_php_fallback_path) {
|
||||
$nixpacks_php_fallback_path = new EnvironmentVariable;
|
||||
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
|
||||
@@ -1209,7 +1334,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private function prepare_builder_image()
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$helperImage = "{$helperImage}:{$settings->helper_version}";
|
||||
// Get user home directory
|
||||
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
|
||||
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
|
||||
@@ -1329,10 +1456,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
|
||||
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
|
||||
'hidden' => true,
|
||||
'save' => 'git_commit_sha',
|
||||
],
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
@@ -1361,7 +1488,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
$importCommands, 'hidden' => true,
|
||||
$importCommands,
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
$this->create_workdir();
|
||||
@@ -1445,6 +1573,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value);
|
||||
data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value);
|
||||
}
|
||||
if ($this->nixpacks_type === 'elixir') {
|
||||
$this->elixir_finetunes();
|
||||
}
|
||||
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
|
||||
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
|
||||
if ($this->nixpacks_type === 'rust') {
|
||||
@@ -1571,7 +1702,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
// Check for custom HEALTHCHECK
|
||||
if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile_from_repo', 'ignore_errors' => true,
|
||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"),
|
||||
'hidden' => true,
|
||||
'save' => 'dockerfile_from_repo',
|
||||
'ignore_errors' => true,
|
||||
]);
|
||||
$dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
|
||||
$this->application->parseHealthcheckFromDockerfile($dockerfile);
|
||||
@@ -1674,14 +1808,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$docker_compose['services'][$this->container_name]['labels'] = $labels;
|
||||
}
|
||||
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$this->container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
$docker_compose['services'][$this->container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
if ($this->application->settings->is_gpu_enabled) {
|
||||
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
|
||||
@@ -1708,13 +1835,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
|
||||
$docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array;
|
||||
}
|
||||
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages;
|
||||
if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) {
|
||||
$docker_compose['services'][$this->container_name]['volumes'] = [];
|
||||
}
|
||||
$docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_storages);
|
||||
}
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$this->container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) {
|
||||
$docker_compose['services'][$this->container_name]['volumes'] = [];
|
||||
}
|
||||
$docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
})->toArray());
|
||||
}
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
@@ -1837,13 +1971,23 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry.");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker pull {$image}"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "docker pull {$image}"),
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function build_image()
|
||||
{
|
||||
// Add Coolify related variables to the build args
|
||||
$this->environment_variables->filter(function ($key, $value) {
|
||||
return str($key)->startsWith('COOLIFY_');
|
||||
})->each(function ($key, $value) {
|
||||
$this->build_args->push("--build-arg '{$key}'");
|
||||
});
|
||||
|
||||
$this->build_args = $this->build_args->implode(' ');
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('----------------------------------------');
|
||||
if ($this->application->build_pack === 'static') {
|
||||
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
|
||||
@@ -1887,12 +2031,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
|
||||
if ($this->force_rebuild) {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
$build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
|
||||
}
|
||||
@@ -1900,10 +2046,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
|
||||
@@ -1917,10 +2069,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -1957,10 +2115,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"),
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
@@ -1974,10 +2138,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
@@ -1986,22 +2156,30 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
|
||||
if ($this->force_rebuild) {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
$build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
}
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
|
||||
@@ -2015,10 +2193,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
|
||||
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -2027,24 +2211,43 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $timeout in seconds
|
||||
*/
|
||||
private function graceful_shutdown_container(string $containerName, int $timeout = 30)
|
||||
private function graceful_shutdown_container(string $containerName, int $timeout = 300)
|
||||
{
|
||||
try {
|
||||
$this->execute_remote_command(
|
||||
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
||||
["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
$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) {
|
||||
// report error if needed
|
||||
$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)
|
||||
@@ -2114,15 +2317,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$this->build_args->push("--build-arg {$env->key}={$value}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->build_args = $this->build_args->implode(' ');
|
||||
ray($this->build_args);
|
||||
}
|
||||
|
||||
private function add_build_env_variables_to_dockerfile()
|
||||
{
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile',
|
||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"),
|
||||
'hidden' => true,
|
||||
'save' => 'dockerfile',
|
||||
]);
|
||||
$dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
|
||||
if ($this->pull_request_id === 0) {
|
||||
@@ -2140,7 +2342,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
} else {
|
||||
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
|
||||
}
|
||||
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
|
||||
}
|
||||
}
|
||||
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
|
||||
@@ -2168,7 +2369,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$exec = "docker exec {$containerName} {$cmd}";
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
'command' => $exec, 'hidden' => true,
|
||||
'command' => $exec,
|
||||
'hidden' => true,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2195,7 +2397,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
try {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
'command' => $exec, 'hidden' => true, 'save' => 'post-deployment-command-output',
|
||||
'command' => $exec,
|
||||
'hidden' => true,
|
||||
'save' => 'post-deployment-command-output',
|
||||
],
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Traits\ExecuteRemoteCommand;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ApplicationRestartJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 3600;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public string $applicationDeploymentQueueId;
|
||||
|
||||
public function __construct(string $applicationDeploymentQueueId)
|
||||
{
|
||||
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
ray('Restarting application');
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||
@@ -25,12 +26,14 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
|
||||
$latest_version = data_get($versions, 'coolify.v4.version');
|
||||
$current_version = config('version');
|
||||
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// New version available
|
||||
$settings->update(['new_version_available' => true]);
|
||||
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
|
||||
} else {
|
||||
$settings->update(['new_version_available' => false]);
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Server\InstallLogDrain;
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Container\ContainerRestarted;
|
||||
use App\Notifications\Container\ContainerStopped;
|
||||
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;
|
||||
use Illuminate\Support\Sleep;
|
||||
|
||||
class CheckLogDrainContainerJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->server->id;
|
||||
}
|
||||
|
||||
public function healthcheck()
|
||||
{
|
||||
$status = instant_remote_process(["docker inspect --format='{{json .State.Status}}' coolify-log-drain"], $this->server, false);
|
||||
if (str($status)->contains('running')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
// ray("checking log drain statuses for {$this->server->id}");
|
||||
try {
|
||||
if (! $this->server->isFunctional()) {
|
||||
return;
|
||||
}
|
||||
$containers = instant_remote_process(['docker container ls -q'], $this->server, false);
|
||||
if (! $containers) {
|
||||
return;
|
||||
}
|
||||
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
|
||||
$foundLogDrainContainer = $containers->filter(function ($value, $key) {
|
||||
return data_get($value, 'Name') === '/coolify-log-drain';
|
||||
})->first();
|
||||
if (! $foundLogDrainContainer || ! $this->healthcheck()) {
|
||||
ray('Log drain container not found or unhealthy. Restarting...');
|
||||
InstallLogDrain::run($this->server);
|
||||
Sleep::for(10)->seconds();
|
||||
if ($this->healthcheck()) {
|
||||
if ($this->server->log_drain_notification_sent) {
|
||||
$this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
|
||||
$this->server->update(['log_drain_notification_sent' => false]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (! $this->server->log_drain_notification_sent) {
|
||||
ray('Log drain container still unhealthy. Sending notification...');
|
||||
// $this->server->team?->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null));
|
||||
$this->server->update(['log_drain_notification_sent' => true]);
|
||||
}
|
||||
} else {
|
||||
if ($this->server->log_drain_notification_sent) {
|
||||
$this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
|
||||
$this->server->update(['log_drain_notification_sent' => false]);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if (! isCloud()) {
|
||||
send_internal_notification("CheckLogDrainContainerJob failed on ({$this->server->id}) with: ".$e->getMessage());
|
||||
}
|
||||
ray($e->getMessage());
|
||||
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,10 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
|
||||
{
|
||||
try {
|
||||
ray('Cleaning up helper containers on '.$this->server->name);
|
||||
$containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerId = data_get($container, 'ID');
|
||||
$containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
|
||||
$containerIds = collect(json_decode($containers))->pluck('ID');
|
||||
if ($containerIds->count() > 0) {
|
||||
foreach ($containerIds as $containerId) {
|
||||
ray('Removing container '.$containerId);
|
||||
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
|
||||
}
|
||||
|
||||
82
app/Jobs/CleanupStaleMultiplexedConnections.php
Normal file
82
app/Jobs/CleanupStaleMultiplexedConnections.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
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\Process;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CleanupStaleMultiplexedConnections implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->cleanupStaleConnections();
|
||||
$this->cleanupNonExistentServerConnections();
|
||||
}
|
||||
|
||||
private function cleanupStaleConnections()
|
||||
{
|
||||
$muxFiles = Storage::disk('ssh-mux')->files();
|
||||
|
||||
foreach ($muxFiles as $muxFile) {
|
||||
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
|
||||
$server = Server::where('uuid', $serverUuid)->first();
|
||||
|
||||
if (! $server) {
|
||||
$this->removeMultiplexFile($muxFile);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
|
||||
$checkCommand = "ssh -O check -o ControlPath={$muxSocket} {$server->user}@{$server->ip} 2>/dev/null";
|
||||
$checkProcess = Process::run($checkCommand);
|
||||
|
||||
if ($checkProcess->exitCode() !== 0) {
|
||||
$this->removeMultiplexFile($muxFile);
|
||||
} else {
|
||||
$muxContent = Storage::disk('ssh-mux')->get($muxFile);
|
||||
$establishedAt = Carbon::parse(substr($muxContent, 37));
|
||||
$expirationTime = $establishedAt->addSeconds(config('constants.ssh.mux_persist_time'));
|
||||
|
||||
if (Carbon::now()->isAfter($expirationTime)) {
|
||||
$this->removeMultiplexFile($muxFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupNonExistentServerConnections()
|
||||
{
|
||||
$muxFiles = Storage::disk('ssh-mux')->files();
|
||||
$existingServerUuids = Server::pluck('uuid')->toArray();
|
||||
|
||||
foreach ($muxFiles as $muxFile) {
|
||||
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
|
||||
if (! in_array($serverUuid, $existingServerUuids)) {
|
||||
$this->removeMultiplexFile($muxFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function extractServerUuidFromMuxFile($muxFile)
|
||||
{
|
||||
return substr($muxFile, 4);
|
||||
}
|
||||
|
||||
private function removeMultiplexFile($muxFile)
|
||||
{
|
||||
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
|
||||
$closeCommand = "ssh -O exit -o ControlPath={$muxSocket} localhost 2>/dev/null";
|
||||
Process::run($closeCommand);
|
||||
Storage::disk('ssh-mux')->delete($muxFile);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ 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 ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||
@@ -25,16 +24,6 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->server->uuid;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
GetContainersStatus::run($this->server);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Jobs;
|
||||
|
||||
use App\Actions\Database\StopDatabase;
|
||||
use App\Events\BackupCreated;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledDatabaseBackupExecution;
|
||||
@@ -22,9 +23,9 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
@@ -56,6 +57,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public ?string $backup_output = null;
|
||||
|
||||
public ?string $postgres_password = null;
|
||||
|
||||
public ?S3Storage $s3 = null;
|
||||
|
||||
public function __construct($backup)
|
||||
@@ -76,21 +79,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [new WithoutOverlapping($this->backup->id)];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->backup->id;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
BackupCreated::dispatch($this->team->id);
|
||||
|
||||
// Check if team is exists
|
||||
if (is_null($this->team)) {
|
||||
$this->backup->update(['status' => 'failed']);
|
||||
@@ -99,6 +90,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
BackupCreated::dispatch($this->team->id);
|
||||
|
||||
$status = str(data_get($this->database, 'status'));
|
||||
if (! $status->startsWith('running') && $this->database->id !== 0) {
|
||||
ray('database not running');
|
||||
@@ -134,6 +128,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
$databasesToBackup = $this->database->postgres_user;
|
||||
}
|
||||
$this->postgres_password = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('POSTGRES_PASSWORD=');
|
||||
})->first();
|
||||
if ($this->postgres_password) {
|
||||
$this->postgres_password = str($this->postgres_password)->after('POSTGRES_PASSWORD=')->value();
|
||||
}
|
||||
|
||||
} elseif (str($databaseType)->contains('mysql')) {
|
||||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
@@ -336,7 +337,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$url = $this->database->internal_db_url;
|
||||
if ($databaseWithCollections === 'all') {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if (str($this->database->image)->startsWith('mongo:4.0')) {
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$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";
|
||||
@@ -351,13 +352,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
if ($collectionsToExclude->count() === 0) {
|
||||
if (str($this->database->image)->startsWith('mongo:4.0')) {
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$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";
|
||||
}
|
||||
} else {
|
||||
if (str($this->database->image)->startsWith('mongo:4.0')) {
|
||||
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";
|
||||
@@ -381,7 +382,14 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
try {
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
$commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
|
||||
$backupCommand = 'docker exec';
|
||||
if ($this->postgres_password) {
|
||||
$backupCommand .= " -e PGPASSWORD=$this->postgres_password";
|
||||
}
|
||||
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
|
||||
|
||||
$commands[] = $backupCommand;
|
||||
ray($commands);
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
@@ -452,7 +460,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->backup->number_of_backups_locally === 0) {
|
||||
$deletable = $this->backup->executions()->where('status', 'success');
|
||||
} else {
|
||||
$deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally - 1);
|
||||
$deletable = $this->backup->executions()->where('status', 'success')->skip($this->backup->number_of_backups_locally - 1);
|
||||
}
|
||||
foreach ($deletable->get() as $execution) {
|
||||
delete_backup_locally($execution->filename, $this->server);
|
||||
@@ -460,6 +468,34 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// private function upload_to_s3(): void
|
||||
// {
|
||||
// try {
|
||||
// if (is_null($this->s3)) {
|
||||
// return;
|
||||
// }
|
||||
// $key = $this->s3->key;
|
||||
// $secret = $this->s3->secret;
|
||||
// // $region = $this->s3->region;
|
||||
// $bucket = $this->s3->bucket;
|
||||
// $endpoint = $this->s3->endpoint;
|
||||
// $this->s3->testConnection(shouldSave: true);
|
||||
// $configName = new Cuid2;
|
||||
|
||||
// $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
|
||||
// $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
|
||||
// $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
|
||||
// instant_remote_process($commands, $this->server);
|
||||
// $this->add_to_backup_output('Uploaded to S3.');
|
||||
// } catch (\Throwable $e) {
|
||||
// $this->add_to_backup_output($e->getMessage());
|
||||
// throw $e;
|
||||
// } finally {
|
||||
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
|
||||
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
|
||||
// instant_remote_process($removeConfigCommands, $this->server, false);
|
||||
// }
|
||||
// }
|
||||
private function upload_to_s3(): void
|
||||
{
|
||||
try {
|
||||
@@ -477,12 +513,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
$network = $this->database->destination->network;
|
||||
}
|
||||
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
|
||||
|
||||
$this->ensureHelperImageAvailable();
|
||||
|
||||
$fullImageName = $this->getFullImageName();
|
||||
$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 cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
|
||||
instant_remote_process($commands, $this->server);
|
||||
$this->add_to_backup_output('Uploaded to S3.');
|
||||
ray('Uploaded to S3. '.$this->backup_location.' to s3://'.$bucket.$this->backup_dir);
|
||||
} catch (\Throwable $e) {
|
||||
$this->add_to_backup_output($e->getMessage());
|
||||
throw $e;
|
||||
@@ -491,4 +530,42 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
instant_remote_process([$command], $this->server);
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureHelperImageAvailable(): void
|
||||
{
|
||||
$fullImageName = $this->getFullImageName();
|
||||
|
||||
$imageExists = $this->checkImageExists($fullImageName);
|
||||
|
||||
if (! $imageExists) {
|
||||
$this->pullHelperImage($fullImageName);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkImageExists(string $fullImageName): bool
|
||||
{
|
||||
$result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
|
||||
|
||||
return trim($result) === 'exists';
|
||||
}
|
||||
|
||||
private function pullHelperImage(string $fullImageName): void
|
||||
{
|
||||
try {
|
||||
instant_remote_process(["docker pull {$fullImageName}"], $this->server);
|
||||
} catch (\Exception $e) {
|
||||
$errorMessage = 'Failed to pull helper image: '.$e->getMessage();
|
||||
$this->add_to_backup_output($errorMessage);
|
||||
throw new \RuntimeException($errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private function getFullImageName(): string
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$latestVersion = $settings->helper_version;
|
||||
|
||||
return "{$helperImage}:{$latestVersion}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Database\DailyBackup;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class DatabaseBackupStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
// $teams = Team::all();
|
||||
// foreach ($teams as $team) {
|
||||
// $scheduled_backups = $team->scheduledDatabaseBackups()->get();
|
||||
// if ($scheduled_backups->isEmpty()) {
|
||||
// continue;
|
||||
// }
|
||||
// foreach ($scheduled_backups as $scheduled_backup) {
|
||||
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
|
||||
// if ($last_days_backups->isEmpty()) {
|
||||
// continue;
|
||||
// }
|
||||
// $failed = $last_days_backups->where('status', 'failed');
|
||||
// }
|
||||
// }
|
||||
|
||||
// $scheduled_backups = ScheduledDatabaseBackup::all();
|
||||
// $databases = collect();
|
||||
// $teams = collect();
|
||||
// foreach ($scheduled_backups as $scheduled_backup) {
|
||||
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
|
||||
// if ($last_days_backups->isEmpty()) {
|
||||
// continue;
|
||||
// }
|
||||
// $failed = $last_days_backups->where('status', 'failed');
|
||||
// $database = $scheduled_backup->database;
|
||||
// $team = $database->team();
|
||||
// $teams->put($team->id, $team);
|
||||
// $databases->put("{$team->id}:{$database->name}", [
|
||||
// 'failed_count' => $failed->count(),
|
||||
// ]);
|
||||
// }
|
||||
// foreach ($databases as $name => $database) {
|
||||
// [$team_id, $name] = explode(':', $name);
|
||||
// $team = $teams->get($team_id);
|
||||
// $team?->notify(new DailyBackup($databases));
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Jobs;
|
||||
|
||||
use App\Actions\Application\StopApplication;
|
||||
use App\Actions\Database\StopDatabase;
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Actions\Service\DeleteService;
|
||||
use App\Actions\Service\StopService;
|
||||
use App\Models\Application;
|
||||
@@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(
|
||||
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
|
||||
public bool $deleteConfigurations = false,
|
||||
public bool $deleteVolumes = false) {}
|
||||
public bool $deleteConfigurations,
|
||||
public bool $deleteVolumes,
|
||||
public bool $dockerCleanup,
|
||||
public bool $deleteConnectedNetworks
|
||||
) {}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
case 'standalone-dragonfly':
|
||||
case 'standalone-clickhouse':
|
||||
$persistentStorages = $this->resource?->persistentStorages()?->get();
|
||||
StopDatabase::run($this->resource);
|
||||
StopDatabase::run($this->resource, true);
|
||||
break;
|
||||
case 'service':
|
||||
StopService::run($this->resource);
|
||||
DeleteService::run($this->resource);
|
||||
StopService::run($this->resource, true);
|
||||
DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->deleteConfigurations) {
|
||||
$this->resource?->delete_configurations();
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
if ($this->deleteConnectedNetworks && ! $isDatabase) {
|
||||
$this->resource?->delete_connected_networks($this->resource->uuid);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e->getMessage());
|
||||
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->resource->forceDelete();
|
||||
if ($this->dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
Artisan::queue('cleanup:stucked-resources');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 300;
|
||||
public $timeout = 600;
|
||||
|
||||
public int|string|null $usageBefore = null;
|
||||
public $tries = 1;
|
||||
|
||||
public ?string $usageBefore = null;
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
@@ -29,9 +31,9 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (! $this->server->isFunctional()) {
|
||||
return;
|
||||
}
|
||||
if ($this->server->settings->is_force_cleanup_enabled) {
|
||||
if ($this->server->settings->force_docker_cleanup) {
|
||||
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
|
||||
CleanupDocker::run(server: $this->server, force: true);
|
||||
CleanupDocker::run(server: $this->server);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -39,12 +41,12 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->usageBefore = $this->server->getDiskUsage();
|
||||
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
|
||||
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
|
||||
CleanupDocker::run(server: $this->server, force: true);
|
||||
CleanupDocker::run(server: $this->server);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
|
||||
CleanupDocker::run(server: $this->server, force: false);
|
||||
if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
|
||||
CleanupDocker::run(server: $this->server);
|
||||
$usageAfter = $this->server->getDiskUsage();
|
||||
if ($usageAfter < $this->usageBefore) {
|
||||
$this->server->team?->notify(new DockerCleanup($this->server, 'Saved '.($this->usageBefore - $usageAfter).'% disk space.'));
|
||||
@@ -56,7 +58,8 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
Log::info('No need to clean up '.$this->server->name);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e->getMessage());
|
||||
CleanupDocker::run(server: $this->server);
|
||||
Log::error('DockerCleanupJob failed: '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
@@ -25,16 +24,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public GithubApp $github_app) {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->github_app->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->github_app->uuid;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Server\UpdateCoolify;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class InstanceAutoUpdateJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 600;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
UpdateCoolify::run();
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PullCoolifyImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
if (isDev() || isCloud()) {
|
||||
return;
|
||||
}
|
||||
$settings = InstanceSettings::get();
|
||||
$server = Server::findOrFail(0);
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
|
||||
}
|
||||
$latest_version = get_latest_version_of_coolify();
|
||||
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$latest_version}"], $server, false);
|
||||
|
||||
$current_version = config('version');
|
||||
if (! $settings->is_auto_update_enabled) {
|
||||
return;
|
||||
}
|
||||
if ($latest_version === $current_version) {
|
||||
return;
|
||||
}
|
||||
if (version_compare($latest_version, $current_version, '<')) {
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
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;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
@@ -17,25 +18,25 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return $this->server->uuid;
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$helperImage = config('coolify.helper_image');
|
||||
ray("Pulling {$helperImage}");
|
||||
instant_remote_process(["docker pull -q {$helperImage}"], $this->server, false);
|
||||
ray('PullHelperImageJob done');
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
$settings = InstanceSettings::get();
|
||||
$latest_version = data_get($versions, 'coolify.helper.version');
|
||||
$current_version = $settings->helper_version;
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// New version available
|
||||
// $helperImage = config('coolify.helper_image');
|
||||
// instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
|
||||
$settings->update(['helper_version' => $latest_version]);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage());
|
||||
ray($e->getMessage());
|
||||
|
||||
@@ -9,7 +9,6 @@ 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 PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
@@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return $this->server->uuid;
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function handle(): void
|
||||
|
||||
@@ -13,7 +13,6 @@ use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ScheduledTaskJob implements ShouldQueue
|
||||
@@ -36,6 +35,8 @@ class ScheduledTaskJob implements ShouldQueue
|
||||
|
||||
public array $containers = [];
|
||||
|
||||
public string $server_timezone;
|
||||
|
||||
public function __construct($task)
|
||||
{
|
||||
$this->task = $task;
|
||||
@@ -47,20 +48,27 @@ class ScheduledTaskJob implements ShouldQueue
|
||||
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
|
||||
}
|
||||
$this->team = Team::find($task->team_id);
|
||||
$this->server_timezone = $this->getServerTimezone();
|
||||
}
|
||||
|
||||
public function middleware(): array
|
||||
private function getServerTimezone(): string
|
||||
{
|
||||
return [new WithoutOverlapping($this->task->id)];
|
||||
}
|
||||
if ($this->resource instanceof Application) {
|
||||
$timezone = $this->resource->destination->server->settings->server_timezone;
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->task->id;
|
||||
return $timezone;
|
||||
} elseif ($this->resource instanceof Service) {
|
||||
$timezone = $this->resource->server->settings->server_timezone;
|
||||
|
||||
return $timezone;
|
||||
}
|
||||
|
||||
return 'UTC';
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
|
||||
try {
|
||||
$this->task_log = ScheduledTaskExecution::create([
|
||||
'scheduled_task_id' => $this->task->id,
|
||||
@@ -121,6 +129,7 @@ class ScheduledTaskJob implements ShouldQueue
|
||||
$this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
|
||||
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
|
||||
throw $e;
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
@@ -24,7 +23,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 3;
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 60;
|
||||
|
||||
public $containers;
|
||||
|
||||
@@ -43,16 +44,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
// public function middleware(): array
|
||||
// {
|
||||
// return [(new WithoutOverlapping($this->server->uuid))];
|
||||
// }
|
||||
|
||||
// public function uniqueId(): int
|
||||
// {
|
||||
// return $this->server->uuid;
|
||||
// }
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
@@ -79,7 +70,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
|
||||
$this->checkLogDrainContainer();
|
||||
$this->checkSentinel();
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
@@ -90,24 +80,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
}
|
||||
|
||||
private function checkSentinel()
|
||||
{
|
||||
if ($this->server->isSentinelEnabled()) {
|
||||
$sentinelContainerFound = $this->containers->filter(function ($value, $key) {
|
||||
return data_get($value, 'Name') === '/coolify-sentinel';
|
||||
})->first();
|
||||
if ($sentinelContainerFound) {
|
||||
$status = data_get($sentinelContainerFound, 'State.Status');
|
||||
if ($status !== 'running') {
|
||||
PullSentinelImageJob::dispatch($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function serverStatus()
|
||||
{
|
||||
['uptime' => $uptime] = $this->server->validateConnection();
|
||||
['uptime' => $uptime] = $this->server->validateConnection(false);
|
||||
if ($uptime) {
|
||||
if ($this->server->unreachable_notification_sent === true) {
|
||||
$this->server->update(['unreachable_notification_sent' => false]);
|
||||
@@ -140,6 +115,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private function checkLogDrainContainer()
|
||||
{
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
return;
|
||||
}
|
||||
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
|
||||
return data_get($value, 'Name') === '/coolify-log-drain';
|
||||
})->first();
|
||||
|
||||
@@ -10,7 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\Middleware\;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
@@ -26,16 +26,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public Team $team) {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->team->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->team->uuid;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -8,7 +8,6 @@ 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 ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||
@@ -26,16 +25,6 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->server->uuid;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (! $this->server->isServerReady($this->tries)) {
|
||||
|
||||
@@ -73,6 +73,8 @@ class Index extends Component
|
||||
}
|
||||
$this->privateKeyName = generate_random_name();
|
||||
$this->remoteServerName = generate_random_name();
|
||||
$this->remoteServerPort = $this->remoteServerPort;
|
||||
$this->remoteServerUser = $this->remoteServerUser;
|
||||
if (isDev()) {
|
||||
$this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
@@ -139,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
if (! $this->createdServer) {
|
||||
return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
|
||||
}
|
||||
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
|
||||
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
|
||||
|
||||
return $this->validateServer('localhost');
|
||||
} elseif ($this->selectedServerType === 'remote') {
|
||||
@@ -154,6 +156,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
if ($this->servers->count() > 0) {
|
||||
$this->selectedExistingServer = $this->servers->first()->id;
|
||||
$this->updateServerDetails();
|
||||
$this->currentState = 'select-existing-server';
|
||||
|
||||
return;
|
||||
@@ -172,10 +175,19 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
return;
|
||||
}
|
||||
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
|
||||
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
|
||||
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
|
||||
$this->updateServerDetails();
|
||||
$this->currentState = 'validate-server';
|
||||
}
|
||||
|
||||
private function updateServerDetails()
|
||||
{
|
||||
if ($this->createdServer) {
|
||||
$this->remoteServerPort = $this->createdServer->port;
|
||||
$this->remoteServerUser = $this->createdServer->user;
|
||||
}
|
||||
}
|
||||
|
||||
public function getProxyType()
|
||||
{
|
||||
// Set Default Proxy Type
|
||||
@@ -219,27 +231,35 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
public function savePrivateKey()
|
||||
{
|
||||
$this->validate([
|
||||
'privateKeyName' => 'required',
|
||||
'privateKey' => 'required',
|
||||
'privateKeyName' => 'required|string|max:255',
|
||||
'privateKeyDescription' => 'nullable|string|max:255',
|
||||
'privateKey' => 'required|string',
|
||||
]);
|
||||
$this->createdPrivateKey = PrivateKey::create([
|
||||
'name' => $this->privateKeyName,
|
||||
'description' => $this->privateKeyDescription,
|
||||
'private_key' => $this->privateKey,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
$this->createdPrivateKey->save();
|
||||
$this->currentState = 'create-server';
|
||||
|
||||
try {
|
||||
$privateKey = PrivateKey::createAndStore([
|
||||
'name' => $this->privateKeyName,
|
||||
'description' => $this->privateKeyDescription,
|
||||
'private_key' => $this->privateKey,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
|
||||
$this->createdPrivateKey = $privateKey;
|
||||
$this->currentState = 'create-server';
|
||||
} catch (\Exception $e) {
|
||||
$this->addError('privateKey', 'Failed to save private key: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function saveServer()
|
||||
{
|
||||
$this->validate([
|
||||
'remoteServerName' => 'required',
|
||||
'remoteServerHost' => 'required',
|
||||
'remoteServerName' => 'required|string',
|
||||
'remoteServerHost' => 'required|string',
|
||||
'remoteServerPort' => 'required|integer',
|
||||
'remoteServerUser' => 'required',
|
||||
'remoteServerUser' => 'required|string',
|
||||
]);
|
||||
|
||||
$this->privateKey = formatPrivateKey($this->privateKey);
|
||||
$foundServer = Server::whereIp($this->remoteServerHost)->first();
|
||||
if ($foundServer) {
|
||||
@@ -269,7 +289,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
public function validateServer()
|
||||
{
|
||||
try {
|
||||
config()->set('coolify.mux_enabled', false);
|
||||
config()->set('constants.ssh.mux_enabled', false);
|
||||
|
||||
// EC2 does not have `uptime` command, lol
|
||||
instant_remote_process(['ls /'], $this->createdServer, true);
|
||||
@@ -277,9 +297,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
$this->createdServer->settings()->update([
|
||||
'is_reachable' => true,
|
||||
]);
|
||||
$this->serverReachable = true;
|
||||
} catch (\Throwable $e) {
|
||||
$this->serverReachable = false;
|
||||
$this->createdServer->delete();
|
||||
$this->createdServer->settings()->update([
|
||||
'is_reachable' => false,
|
||||
]);
|
||||
|
||||
return handleError(error: $e, livewire: $this);
|
||||
}
|
||||
@@ -296,6 +319,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
]);
|
||||
$this->getProxyType();
|
||||
} catch (\Throwable $e) {
|
||||
$this->createdServer->settings()->update([
|
||||
'is_usable' => false,
|
||||
]);
|
||||
|
||||
return handleError(error: $e, livewire: $this);
|
||||
}
|
||||
}
|
||||
@@ -349,6 +376,21 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
);
|
||||
}
|
||||
|
||||
public function saveAndValidateServer()
|
||||
{
|
||||
$this->validate([
|
||||
'remoteServerPort' => 'required|integer|min:1|max:65535',
|
||||
'remoteServerUser' => 'required|string',
|
||||
]);
|
||||
|
||||
$this->createdServer->update([
|
||||
'port' => $this->remoteServerPort,
|
||||
'user' => $this->remoteServerUser,
|
||||
'timezone' => 'UTC',
|
||||
]);
|
||||
$this->validateServer();
|
||||
}
|
||||
|
||||
private function createNewPrivateKey()
|
||||
{
|
||||
$this->privateKeyName = generate_random_name();
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\CommandCenter;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
public $servers = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->servers = Server::isReachable()->get();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.command-center.index');
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ class Dashboard extends Component
|
||||
|
||||
public function cleanup_queue()
|
||||
{
|
||||
$this->dispatch('success', 'Cleanup started.');
|
||||
Artisan::queue('cleanup:application-deployment-queue', [
|
||||
'--team-id' => currentTeam()->id,
|
||||
]);
|
||||
@@ -50,15 +49,6 @@ class Dashboard extends Component
|
||||
])->sortBy('id')->groupBy('server_name')->toArray();
|
||||
}
|
||||
|
||||
// public function getIptables()
|
||||
// {
|
||||
// $servers = Server::ownedByCurrentTeam()->get();
|
||||
// foreach ($servers as $server) {
|
||||
// checkRequiredCommands($server);
|
||||
// $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server);
|
||||
// ray($iptables);
|
||||
// }
|
||||
// }
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard');
|
||||
|
||||
@@ -38,7 +38,7 @@ class Form extends Component
|
||||
}
|
||||
$this->destination->delete();
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
return redirect()->route('destination.all');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,28 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class NavbarDeleteTeam extends Component
|
||||
{
|
||||
public function delete()
|
||||
public $team;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->team = currentTeam()->name;
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$currentTeam = currentTeam();
|
||||
$currentTeam->delete();
|
||||
|
||||
|
||||
@@ -66,9 +66,9 @@ class Advanced extends Component
|
||||
$this->dispatch('resetDefaultLabels', false);
|
||||
}
|
||||
if ($this->application->settings->is_raw_compose_deployment_enabled) {
|
||||
$this->application->parseRawCompose();
|
||||
$this->application->oldRawParser();
|
||||
} else {
|
||||
$this->application->parseCompose();
|
||||
$this->application->parse();
|
||||
}
|
||||
$this->application->settings->save();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
@@ -96,6 +96,12 @@ class Advanced extends Component
|
||||
} else {
|
||||
$this->application->settings->custom_internal_name = null;
|
||||
}
|
||||
if (is_null($this->application->settings->custom_internal_name)) {
|
||||
$this->application->settings->save();
|
||||
$this->dispatch('success', 'Custom name saved.');
|
||||
|
||||
return;
|
||||
}
|
||||
$customInternalName = $this->application->settings->custom_internal_name;
|
||||
$server = $this->application->destination->server;
|
||||
$allApplications = $server->applications();
|
||||
|
||||
@@ -69,6 +69,20 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function getLogLinesProperty()
|
||||
{
|
||||
return decode_remote_command_output($this->application_deployment_queue)->map(function ($logLine) {
|
||||
$logLine['line'] = e($logLine['line']);
|
||||
$logLine['line'] = preg_replace(
|
||||
'/(https?:\/\/[^\s]+)/',
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer" class="underline text-neutral-400">$1</a>',
|
||||
$logLine['line'],
|
||||
);
|
||||
|
||||
return $logLine;
|
||||
});
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.deployment.show');
|
||||
|
||||
@@ -55,9 +55,14 @@ class DeploymentNavbar extends Component
|
||||
public function cancel()
|
||||
{
|
||||
$kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
|
||||
$build_server_id = $this->application_deployment_queue->build_server_id;
|
||||
$server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
|
||||
try {
|
||||
$server = Server::find($server_id);
|
||||
if ($this->application->settings->is_build_server_enabled) {
|
||||
$server = Server::find($build_server_id);
|
||||
} else {
|
||||
$server = Server::find($server_id);
|
||||
}
|
||||
if ($this->application_deployment_queue->logs) {
|
||||
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\LocalFileVolume;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
@@ -30,6 +29,8 @@ class General extends Component
|
||||
|
||||
public ?string $ports_exposes = null;
|
||||
|
||||
public bool $is_preserve_repository_enabled = false;
|
||||
|
||||
public bool $is_container_label_escape_enabled = true;
|
||||
|
||||
public $customLabels;
|
||||
@@ -130,7 +131,7 @@ class General extends Component
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$this->parsedServices = $this->application->parseCompose();
|
||||
$this->parsedServices = $this->application->parse();
|
||||
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
|
||||
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||
|
||||
@@ -145,6 +146,7 @@ class General extends Component
|
||||
}
|
||||
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
|
||||
$this->ports_exposes = $this->application->ports_exposes;
|
||||
$this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled;
|
||||
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$this->customLabels = $this->application->parseContainerLabels();
|
||||
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
|
||||
@@ -168,9 +170,21 @@ class General extends Component
|
||||
$this->application->settings->save();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
$this->application->refresh();
|
||||
|
||||
// If port_exposes changed, reset default labels
|
||||
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
if ($this->is_preserve_repository_enabled !== $this->application->settings->is_preserve_repository_enabled) {
|
||||
if ($this->application->settings->is_preserve_repository_enabled === false) {
|
||||
$this->application->fileStorages->each(function ($storage) {
|
||||
$storage->is_based_on_git = $this->application->settings->is_preserve_repository_enabled;
|
||||
$storage->save();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function loadComposeFile($isInit = false)
|
||||
@@ -179,39 +193,18 @@ class General extends Component
|
||||
if ($isInit && $this->application->docker_compose_raw) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must reload the application to get the latest database changes
|
||||
// Why? Not sure, but it works.
|
||||
// $this->application->refresh();
|
||||
|
||||
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit);
|
||||
if (is_null($this->parsedServices)) {
|
||||
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||
|
||||
return;
|
||||
}
|
||||
$compose = $this->application->parseCompose();
|
||||
$services = data_get($compose, 'services');
|
||||
if ($services) {
|
||||
$volumes = collect($services)->map(function ($service) {
|
||||
return data_get($service, 'volumes');
|
||||
})->flatten()->filter(function ($volume) {
|
||||
return str($volume)->startsWith('/data/coolify');
|
||||
})->unique()->values();
|
||||
foreach ($volumes as $volume) {
|
||||
$source = str($volume)->before(':');
|
||||
$target = str($volume)->after(':')->beforeLast(':');
|
||||
|
||||
LocalFileVolume::updateOrCreate(
|
||||
[
|
||||
'mount_path' => $target,
|
||||
'resource_id' => $this->application->id,
|
||||
'resource_type' => get_class($this->application),
|
||||
],
|
||||
[
|
||||
'fs_path' => $source,
|
||||
'mount_path' => $target,
|
||||
'resource_id' => $this->application->id,
|
||||
'resource_type' => get_class($this->application),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->application->parse();
|
||||
$this->dispatch('success', 'Docker compose file loaded.');
|
||||
$this->dispatch('compose_loaded');
|
||||
$this->dispatch('refreshStorages');
|
||||
|
||||
@@ -21,6 +21,8 @@ class Heading extends Component
|
||||
|
||||
protected string $deploymentUuid;
|
||||
|
||||
public bool $docker_cleanup = true;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
@@ -102,7 +104,7 @@ class Heading extends Component
|
||||
|
||||
public function stop()
|
||||
{
|
||||
StopApplication::run($this->application);
|
||||
StopApplication::run($this->application, false, $this->docker_cleanup);
|
||||
$this->application->status = 'exited';
|
||||
$this->application->save();
|
||||
if ($this->application->additional_servers->count() > 0) {
|
||||
@@ -135,4 +137,13 @@ class Heading extends Component
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.heading', [
|
||||
'checkboxes' => [
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ namespace App\Livewire\Project\Application;
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
@@ -79,8 +81,15 @@ class Previews extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
$fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$preview->generate_preview_fqdn_compose();
|
||||
$this->application->refresh();
|
||||
$this->dispatch('success', 'Domain generated.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
|
||||
$url = Url::fromString($fqdn);
|
||||
$template = $this->application->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
@@ -177,17 +186,20 @@ class Previews extends Component
|
||||
public function stop(int $pull_request_id)
|
||||
{
|
||||
try {
|
||||
$server = $this->application->destination->server;
|
||||
$timeout = 300;
|
||||
|
||||
if ($this->application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
|
||||
foreach ($containers as $container) {
|
||||
$name = str_replace('/', '', $container['Names']);
|
||||
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
|
||||
}
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
|
||||
$this->stopContainers($containers, $server, $timeout);
|
||||
}
|
||||
GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high');
|
||||
$this->dispatch('reloadWindow');
|
||||
|
||||
GetContainersStatus::run($server);
|
||||
$this->application->refresh();
|
||||
$this->dispatch('containerStatusUpdated');
|
||||
$this->dispatch('success', 'Preview Deployment stopped.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -196,16 +208,21 @@ class Previews extends Component
|
||||
public function delete(int $pull_request_id)
|
||||
{
|
||||
try {
|
||||
$server = $this->application->destination->server;
|
||||
$timeout = 300;
|
||||
|
||||
if ($this->application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
|
||||
foreach ($containers as $container) {
|
||||
$name = str_replace('/', '', $container['Names']);
|
||||
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
|
||||
}
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
|
||||
$this->stopContainers($containers, $server, $timeout);
|
||||
}
|
||||
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
|
||||
|
||||
ApplicationPreview::where('application_id', $this->application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->first()
|
||||
->delete();
|
||||
|
||||
$this->application->refresh();
|
||||
$this->dispatch('update_links');
|
||||
$this->dispatch('success', 'Preview deleted.');
|
||||
@@ -213,4 +230,49 @@ class Previews extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainers(array $containers, $server, int $timeout)
|
||||
{
|
||||
$processes = [];
|
||||
foreach ($containers as $container) {
|
||||
$containerName = str_replace('/', '', $container['Names']);
|
||||
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
|
||||
}
|
||||
|
||||
$startTime = time();
|
||||
while (count($processes) > 0) {
|
||||
$finishedProcesses = array_filter($processes, function ($process) {
|
||||
return ! $process->running();
|
||||
});
|
||||
foreach (array_keys($finishedProcesses) as $containerName) {
|
||||
unset($processes[$containerName]);
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
|
||||
if (time() - $startTime >= $timeout) {
|
||||
$this->forceStopRemainingContainers(array_keys($processes), $server);
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(100000);
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||
{
|
||||
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
}
|
||||
|
||||
private function removeContainer(string $containerName, $server)
|
||||
{
|
||||
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
|
||||
}
|
||||
|
||||
private function forceStopRemainingContainers(array $containerNames, $server)
|
||||
{
|
||||
foreach ($containerNames as $containerName) {
|
||||
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
@@ -12,6 +14,12 @@ class BackupEdit extends Component
|
||||
|
||||
public $s3s;
|
||||
|
||||
public bool $delete_associated_backups_locally = false;
|
||||
|
||||
public bool $delete_associated_backups_s3 = false;
|
||||
|
||||
public bool $delete_associated_backups_sftp = false;
|
||||
|
||||
public ?string $status = null;
|
||||
|
||||
public array $parameters;
|
||||
@@ -46,10 +54,24 @@ class BackupEdit extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->delete_associated_backups_locally) {
|
||||
$this->deleteAssociatedBackupsLocally();
|
||||
}
|
||||
if ($this->delete_associated_backups_s3) {
|
||||
$this->deleteAssociatedBackupsS3();
|
||||
}
|
||||
|
||||
$this->backup->delete();
|
||||
|
||||
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$previousUrl = url()->previous();
|
||||
$url = Url::fromString($previousUrl);
|
||||
@@ -104,4 +126,66 @@ class BackupEdit extends Component
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteAssociatedBackupsLocally()
|
||||
{
|
||||
$executions = $this->backup->executions;
|
||||
$backupFolder = null;
|
||||
|
||||
foreach ($executions as $execution) {
|
||||
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$server = $this->backup->database->service->destination->server;
|
||||
} else {
|
||||
$server = $this->backup->database->destination->server;
|
||||
}
|
||||
|
||||
if (! $backupFolder) {
|
||||
$backupFolder = dirname($execution->filename);
|
||||
}
|
||||
|
||||
delete_backup_locally($execution->filename, $server);
|
||||
$execution->delete();
|
||||
}
|
||||
|
||||
if ($backupFolder) {
|
||||
$this->deleteEmptyBackupFolder($backupFolder, $server);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteAssociatedBackupsS3()
|
||||
{
|
||||
//Add function to delete backups from S3
|
||||
}
|
||||
|
||||
public function deleteAssociatedBackupsSftp()
|
||||
{
|
||||
//Add function to delete backups from SFTP
|
||||
}
|
||||
|
||||
private function deleteEmptyBackupFolder($folderPath, $server)
|
||||
{
|
||||
$checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
|
||||
|
||||
if (trim($checkEmpty) === 'empty') {
|
||||
instant_remote_process(["rmdir '$folderPath'"], $server);
|
||||
|
||||
$parentFolder = dirname($folderPath);
|
||||
$checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
|
||||
|
||||
if (trim($checkParentEmpty) === 'empty') {
|
||||
instant_remote_process(["rmdir '$parentFolder'"], $server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.backup-edit', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
|
||||
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
|
||||
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,28 @@
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class BackupExecutions extends Component
|
||||
{
|
||||
public ?ScheduledDatabaseBackup $backup = null;
|
||||
|
||||
public $database;
|
||||
|
||||
public $executions = [];
|
||||
|
||||
public $setDeletableBackup;
|
||||
|
||||
public $delete_backup_s3 = true;
|
||||
|
||||
public $delete_backup_sftp = true;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
$userId = Auth::id();
|
||||
|
||||
return [
|
||||
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
|
||||
@@ -32,19 +41,36 @@ class BackupExecutions extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteBackup($exeuctionId)
|
||||
#[On('deleteBackup')]
|
||||
public function deleteBackup($executionId, $password)
|
||||
{
|
||||
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$execution = $this->backup->executions()->where('id', $executionId)->first();
|
||||
if (is_null($execution)) {
|
||||
$this->dispatch('error', 'Backup execution not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
|
||||
} else {
|
||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
|
||||
}
|
||||
|
||||
if ($this->delete_backup_s3) {
|
||||
// Add logic to delete from S3
|
||||
}
|
||||
|
||||
if ($this->delete_backup_sftp) {
|
||||
// Add logic to delete from SFTP
|
||||
}
|
||||
|
||||
$execution->delete();
|
||||
$this->dispatch('success', 'Backup deleted.');
|
||||
$this->refreshBackupExecutions();
|
||||
@@ -58,7 +84,66 @@ class BackupExecutions extends Component
|
||||
public function refreshBackupExecutions(): void
|
||||
{
|
||||
if ($this->backup) {
|
||||
$this->executions = $this->backup->executions()->get()->sortBy('created_at');
|
||||
$this->executions = $this->backup->executions()->get();
|
||||
}
|
||||
}
|
||||
|
||||
public function mount(ScheduledDatabaseBackup $backup)
|
||||
{
|
||||
$this->backup = $backup;
|
||||
$this->database = $backup->database;
|
||||
$this->refreshBackupExecutions();
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
if ($this->database) {
|
||||
$server = null;
|
||||
|
||||
if ($this->database instanceof \App\Models\ServiceDatabase) {
|
||||
$server = $this->database->service->destination->server;
|
||||
} elseif ($this->database->destination && $this->database->destination->server) {
|
||||
$server = $this->database->destination->server;
|
||||
}
|
||||
if ($server) {
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getServerTimezone()
|
||||
{
|
||||
$server = $this->server();
|
||||
if (! $server) {
|
||||
return 'UTC';
|
||||
}
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
|
||||
return $serverTimezone;
|
||||
}
|
||||
|
||||
public function formatDateInServerTimezone($date)
|
||||
{
|
||||
$serverTimezone = $this->getServerTimezone();
|
||||
$dateObj = new \DateTime($date);
|
||||
try {
|
||||
$dateObj->setTimezone(new \DateTimeZone($serverTimezone));
|
||||
} catch (\Exception $e) {
|
||||
$dateObj->setTimezone(new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
return $dateObj->format('Y-m-d H:i:s T');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.backup-executions', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
|
||||
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -42,6 +43,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -30,6 +30,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -40,6 +41,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -14,6 +14,8 @@ class Heading extends Component
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public $docker_cleanup = true;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
@@ -54,7 +56,7 @@ class Heading extends Component
|
||||
|
||||
public function stop()
|
||||
{
|
||||
StopDatabase::run($this->database);
|
||||
StopDatabase::run($this->database, false, $this->docker_cleanup);
|
||||
$this->database->status = 'exited';
|
||||
$this->database->save();
|
||||
$this->check_status();
|
||||
@@ -71,4 +73,13 @@ class Heading extends Component
|
||||
$activity = StartDatabase::run($this->database);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.heading', [
|
||||
'checkboxes' => [
|
||||
['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -42,6 +43,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -34,6 +34,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -48,6 +49,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -33,6 +33,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -46,6 +47,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -34,6 +34,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -48,6 +49,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -49,6 +49,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -65,6 +66,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -32,6 +32,7 @@ class General extends Component
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -44,6 +45,7 @@ class General extends Component
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
||||
@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
|
||||
|
||||
public bool $disabled = false;
|
||||
|
||||
public string $environmentName = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
|
||||
}
|
||||
|
||||
public function delete()
|
||||
|
||||
@@ -13,9 +13,12 @@ class DeleteProject extends Component
|
||||
|
||||
public bool $disabled = false;
|
||||
|
||||
public string $projectName = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->projectName = Project::findOrFail($this->project_id)->name;
|
||||
}
|
||||
|
||||
public function delete()
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Livewire\Project\New;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -58,12 +60,26 @@ class DockerCompose extends Component
|
||||
|
||||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
|
||||
|
||||
$destination_uuid = $this->query['destination'];
|
||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
|
||||
if (! $destination) {
|
||||
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
|
||||
}
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found. What?!');
|
||||
}
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
$service = Service::create([
|
||||
'name' => 'service'.Str::random(10),
|
||||
'docker_compose_raw' => $this->dockerComposeRaw,
|
||||
'environment_id' => $environment->id,
|
||||
'server_id' => (int) $server_id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination_class,
|
||||
]);
|
||||
|
||||
$variables = parseEnvFormatToArray($this->envFile);
|
||||
foreach ($variables as $key => $variable) {
|
||||
EnvironmentVariable::create([
|
||||
|
||||
@@ -99,6 +99,16 @@ class PublicGitRepository extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedDockerComposeLocation()
|
||||
{
|
||||
if ($this->docker_compose_location) {
|
||||
$this->docker_compose_location = rtrim($this->docker_compose_location, '/');
|
||||
if (! str($this->docker_compose_location)->startsWith('/')) {
|
||||
$this->docker_compose_location = '/'.$this->docker_compose_location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedBuildPack()
|
||||
{
|
||||
if ($this->build_pack === 'nixpacks') {
|
||||
|
||||
@@ -45,6 +45,8 @@ class Select extends Component
|
||||
|
||||
public ?string $selectedEnvironment = null;
|
||||
|
||||
public string $postgresql_type = 'postgres:16-alpine';
|
||||
|
||||
public ?string $existingPostgresqlUrl = null;
|
||||
|
||||
public ?string $search = null;
|
||||
@@ -202,6 +204,8 @@ class Select extends Component
|
||||
$docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first();
|
||||
if ($docker) {
|
||||
$this->setDestination($docker->uuid);
|
||||
|
||||
return $this->whatToDoNext();
|
||||
}
|
||||
}
|
||||
$this->current_step = 'destinations';
|
||||
@@ -211,15 +215,38 @@ class Select extends Component
|
||||
{
|
||||
$this->destination_uuid = $destination_uuid;
|
||||
|
||||
return $this->whatToDoNext();
|
||||
}
|
||||
|
||||
public function setPostgresqlType(string $type)
|
||||
{
|
||||
$this->postgresql_type = $type;
|
||||
|
||||
return redirect()->route('project.resource.create', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
'type' => $this->type,
|
||||
'destination' => $this->destination_uuid,
|
||||
'server_id' => $this->server_id,
|
||||
'database_image' => $this->postgresql_type,
|
||||
]);
|
||||
}
|
||||
|
||||
public function whatToDoNext()
|
||||
{
|
||||
if ($this->type === 'postgresql') {
|
||||
$this->current_step = 'select-postgresql-type';
|
||||
} else {
|
||||
return redirect()->route('project.resource.create', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
'type' => $this->type,
|
||||
'destination' => $this->destination_uuid,
|
||||
'server_id' => $this->server_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadServers()
|
||||
{
|
||||
$this->servers = Server::isUsable()->get();
|
||||
|
||||
@@ -18,6 +18,7 @@ class Create extends Component
|
||||
$type = str(request()->query('type'));
|
||||
$destination_uuid = request()->query('destination');
|
||||
$server_id = request()->query('server_id');
|
||||
$database_image = request()->query('database_image');
|
||||
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (! $project) {
|
||||
@@ -33,7 +34,11 @@ class Create extends Component
|
||||
|
||||
if (in_array($type, DATABASE_TYPES)) {
|
||||
if ($type->value() === 'postgresql') {
|
||||
$database = create_standalone_postgresql($environment->id, $destination_uuid);
|
||||
$database = create_standalone_postgresql(
|
||||
environmentId: $environment->id,
|
||||
destinationUuid: $destination_uuid,
|
||||
databaseImage: $database_image
|
||||
);
|
||||
} elseif ($type->value() === 'redis') {
|
||||
$database = create_standalone_redis($environment->id, $destination_uuid);
|
||||
} elseif ($type->value() === 'mongodb') {
|
||||
@@ -86,18 +91,16 @@ class Create extends Component
|
||||
$oneClickDotEnvs->each(function ($value) use ($service) {
|
||||
$key = str()->before($value, '=');
|
||||
$value = str(str()->after($value, '='));
|
||||
$generatedValue = $value;
|
||||
if ($value->contains('SERVICE_')) {
|
||||
$command = $value->after('SERVICE_')->beforeLast('_');
|
||||
$generatedValue = generateEnvValue($command->value(), $service);
|
||||
if ($value) {
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'service_id' => $service->id,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $generatedValue,
|
||||
'service_id' => $service->id,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
});
|
||||
}
|
||||
$service->parse(isNew: true);
|
||||
|
||||
@@ -25,6 +25,7 @@ class Configuration extends Component
|
||||
return [
|
||||
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
|
||||
'check_status',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ class Configuration extends Component
|
||||
$application = $this->service->applications->find($id);
|
||||
if ($application) {
|
||||
$application->restart();
|
||||
$this->dispatch('success', 'Application restarted successfully.');
|
||||
$this->dispatch('success', 'Service application restarted successfully.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -64,7 +65,7 @@ class Configuration extends Component
|
||||
$database = $this->service->databases->find($id);
|
||||
if ($database) {
|
||||
$database->restart();
|
||||
$this->dispatch('success', 'Database restarted successfully.');
|
||||
$this->dispatch('success', 'Service database restarted successfully.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -75,6 +76,12 @@ class Configuration extends Component
|
||||
{
|
||||
try {
|
||||
GetContainersStatus::run($this->service->server);
|
||||
$this->service->applications->each(function ($application) {
|
||||
$application->refresh();
|
||||
});
|
||||
$this->service->databases->each(function ($database) {
|
||||
$database->refresh();
|
||||
});
|
||||
$this->dispatch('$refresh');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -11,7 +11,11 @@ class EditCompose extends Component
|
||||
|
||||
public $serviceId;
|
||||
|
||||
protected $listeners = ['refreshEnvs', 'envsUpdated'];
|
||||
protected $listeners = [
|
||||
'refreshEnvs',
|
||||
'envsUpdated',
|
||||
'refresh' => 'envsUpdated',
|
||||
];
|
||||
|
||||
protected $rules = [
|
||||
'service.docker_compose_raw' => 'required',
|
||||
@@ -39,6 +43,7 @@ class EditCompose extends Component
|
||||
{
|
||||
$this->dispatch('info', 'Saving new docker compose...');
|
||||
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
|
||||
$this->dispatch('refreshStorages');
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
|
||||
@@ -14,6 +14,8 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class FileStorage extends Component
|
||||
@@ -33,6 +35,7 @@ class FileStorage extends Component
|
||||
'fileStorage.fs_path' => 'required',
|
||||
'fileStorage.mount_path' => 'required',
|
||||
'fileStorage.content' => 'nullable',
|
||||
'fileStorage.is_based_on_git' => 'required|boolean',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -45,6 +48,7 @@ class FileStorage extends Component
|
||||
$this->workdir = null;
|
||||
$this->fs_path = $this->fileStorage->fs_path;
|
||||
}
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
}
|
||||
|
||||
public function convertToDirectory()
|
||||
@@ -53,6 +57,7 @@ class FileStorage extends Component
|
||||
$this->fileStorage->deleteStorageOnServer();
|
||||
$this->fileStorage->is_directory = true;
|
||||
$this->fileStorage->content = null;
|
||||
$this->fileStorage->is_based_on_git = false;
|
||||
$this->fileStorage->save();
|
||||
$this->fileStorage->saveStorageOnServer();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -68,6 +73,9 @@ class FileStorage extends Component
|
||||
$this->fileStorage->deleteStorageOnServer();
|
||||
$this->fileStorage->is_directory = false;
|
||||
$this->fileStorage->content = null;
|
||||
if (data_get($this->resource, 'settings.is_preserve_repository_enabled')) {
|
||||
$this->fileStorage->is_based_on_git = true;
|
||||
}
|
||||
$this->fileStorage->save();
|
||||
$this->fileStorage->saveStorageOnServer();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -77,8 +85,14 @@ class FileStorage extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$message = 'File deleted.';
|
||||
if ($this->fileStorage->is_directory) {
|
||||
@@ -123,6 +137,13 @@ class FileStorage extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.file-storage');
|
||||
return view('livewire.project.service.file-storage', [
|
||||
'directoryDeletionCheckboxes' => [
|
||||
['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
|
||||
],
|
||||
'fileDeletionCheckboxes' => [
|
||||
['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ class Navbar extends Component
|
||||
|
||||
public $isDeploymentProgress = false;
|
||||
|
||||
public $docker_cleanup = true;
|
||||
|
||||
public $title = 'Configuration';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
|
||||
@@ -40,7 +44,7 @@ class Navbar extends Component
|
||||
|
||||
public function serviceStarted()
|
||||
{
|
||||
$this->dispatch('success', 'Service status changed.');
|
||||
// $this->dispatch('success', 'Service status changed.');
|
||||
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
|
||||
$this->service->isConfigurationChanged(true);
|
||||
$this->dispatch('configurationChanged');
|
||||
@@ -60,11 +64,6 @@ class Navbar extends Component
|
||||
$this->dispatch('success', 'Service status updated.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.navbar');
|
||||
}
|
||||
|
||||
public function checkDeployments()
|
||||
{
|
||||
try {
|
||||
@@ -95,14 +94,9 @@ class Navbar extends Component
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function stop(bool $forceCleanup = false)
|
||||
public function stop()
|
||||
{
|
||||
StopService::run($this->service);
|
||||
if ($forceCleanup) {
|
||||
$this->dispatch('success', 'Containers cleaned up.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Service stopped.');
|
||||
}
|
||||
StopService::run($this->service, false, $this->docker_cleanup);
|
||||
ServiceStatusChanged::dispatch();
|
||||
}
|
||||
|
||||
@@ -121,4 +115,13 @@ class Navbar extends Component
|
||||
$activity = StartService::run($this->service);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.navbar', [
|
||||
'checkboxes' => [
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class ServiceApplicationView extends Component
|
||||
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
|
||||
|
||||
public $parameters;
|
||||
|
||||
public $docker_cleanup = true;
|
||||
|
||||
public $delete_volumes = true;
|
||||
|
||||
protected $rules = [
|
||||
'application.human_name' => 'nullable',
|
||||
'application.description' => 'nullable',
|
||||
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
|
||||
'application.is_stripprefix_enabled' => 'nullable|boolean',
|
||||
];
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.service-application-view');
|
||||
}
|
||||
|
||||
public function updatedApplicationFqdn()
|
||||
{
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->application->delete();
|
||||
$this->dispatch('success', 'Application deleted.');
|
||||
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
|
||||
$this->dispatch('generateDockerCompose');
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.service-application-view', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
|
||||
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
|
||||
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class StackForm extends Component
|
||||
$key = data_get($field, 'key');
|
||||
$value = data_get($field, 'value');
|
||||
$rules = data_get($field, 'rules', 'nullable');
|
||||
$isPassword = data_get($field, 'isPassword');
|
||||
$isPassword = data_get($field, 'isPassword', false);
|
||||
$this->fields->put($key, [
|
||||
'serviceName' => $serviceName,
|
||||
'key' => $key,
|
||||
@@ -47,13 +47,21 @@ class StackForm extends Component
|
||||
$this->validationAttributes["fields.$key.value"] = $fieldKey;
|
||||
}
|
||||
}
|
||||
$this->fields = $this->fields->sortBy('name');
|
||||
$this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
|
||||
return $group->sortBy(function ($field) {
|
||||
return data_get($field, 'isPassword') ? 1 : 0;
|
||||
})->mapWithKeys(function ($field) {
|
||||
return [$field['key'] => $field];
|
||||
});
|
||||
})->flatMap(function ($group) {
|
||||
return $group;
|
||||
});
|
||||
}
|
||||
|
||||
public function saveCompose($raw)
|
||||
{
|
||||
$this->service->docker_compose_raw = $raw;
|
||||
$this->submit();
|
||||
$this->submit(notify: false);
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
@@ -62,7 +70,7 @@ class StackForm extends Component
|
||||
$this->dispatch('success', 'Service settings saved.');
|
||||
}
|
||||
|
||||
public function submit()
|
||||
public function submit($notify = true)
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
@@ -76,7 +84,7 @@ class StackForm extends Component
|
||||
$this->service->refresh();
|
||||
$this->service->saveComposeConfigs();
|
||||
$this->dispatch('refreshEnvs');
|
||||
$this->dispatch('success', 'Service saved.');
|
||||
$notify && $this->dispatch('success', 'Service saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -10,6 +15,8 @@ class Danger extends Component
|
||||
{
|
||||
public $resource;
|
||||
|
||||
public $resourceName;
|
||||
|
||||
public $projectUuid;
|
||||
|
||||
public $environmentName;
|
||||
@@ -18,22 +25,93 @@ class Danger extends Component
|
||||
|
||||
public bool $delete_volumes = true;
|
||||
|
||||
public bool $docker_cleanup = true;
|
||||
|
||||
public bool $delete_connected_networks = true;
|
||||
|
||||
public ?string $modalId = null;
|
||||
|
||||
public string $resourceDomain = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->modalId = new Cuid2;
|
||||
$parameters = get_route_parameters();
|
||||
$this->modalId = new Cuid2;
|
||||
$this->projectUuid = data_get($parameters, 'project_uuid');
|
||||
$this->environmentName = data_get($parameters, 'environment_name');
|
||||
|
||||
if ($this->resource === null) {
|
||||
if (isset($parameters['service_uuid'])) {
|
||||
$this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
|
||||
} elseif (isset($parameters['stack_service_uuid'])) {
|
||||
$this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
|
||||
?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->resource === null) {
|
||||
$this->resourceName = 'Unknown Resource';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! method_exists($this->resource, 'type')) {
|
||||
$this->resourceName = 'Unknown Resource';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
$this->resourceName = $this->resource->name ?? 'Application';
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
case 'standalone-redis':
|
||||
case 'standalone-mongodb':
|
||||
case 'standalone-mysql':
|
||||
case 'standalone-mariadb':
|
||||
case 'standalone-keydb':
|
||||
case 'standalone-dragonfly':
|
||||
case 'standalone-clickhouse':
|
||||
$this->resourceName = $this->resource->name ?? 'Database';
|
||||
break;
|
||||
case 'service':
|
||||
$this->resourceName = $this->resource->name ?? 'Service';
|
||||
break;
|
||||
case 'service-application':
|
||||
$this->resourceName = $this->resource->name ?? 'Service Application';
|
||||
break;
|
||||
case 'service-database':
|
||||
$this->resourceName = $this->resource->name ?? 'Service Database';
|
||||
break;
|
||||
default:
|
||||
$this->resourceName = 'Unknown Resource';
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
public function delete($password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->resource) {
|
||||
$this->addError('resource', 'Resource not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// $this->authorize('delete', $this->resource);
|
||||
$this->resource->delete();
|
||||
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
|
||||
DeleteResourceJob::dispatch(
|
||||
$this->resource,
|
||||
$this->delete_configurations,
|
||||
$this->delete_volumes,
|
||||
$this->docker_cleanup,
|
||||
$this->delete_connected_networks
|
||||
);
|
||||
|
||||
return redirect()->route('project.resource.index', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
@@ -43,4 +121,19 @@ class Danger extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.danger', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
|
||||
['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
|
||||
['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
|
||||
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
|
||||
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
|
||||
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ use App\Events\ApplicationStatusChanged;
|
||||
use App\Jobs\ContainerStatusJob;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -115,8 +117,14 @@ class Destination extends Component
|
||||
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
|
||||
}
|
||||
|
||||
public function removeServer(int $network_id, int $server_id)
|
||||
public function removeServer(int $network_id, int $server_id, $password)
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
|
||||
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ class All extends Component
|
||||
public string $view = 'normal';
|
||||
|
||||
protected $listeners = [
|
||||
'refreshEnvs',
|
||||
'saveKey' => 'submit',
|
||||
'refreshEnvs',
|
||||
'environmentVariableDeleted' => 'refreshEnvs',
|
||||
];
|
||||
|
||||
protected $rules = [
|
||||
@@ -40,220 +41,240 @@ class All extends Component
|
||||
$this->showPreview = true;
|
||||
}
|
||||
$this->modalId = new Cuid2;
|
||||
$this->sortMe();
|
||||
$this->getDevView();
|
||||
}
|
||||
|
||||
public function sortMe()
|
||||
{
|
||||
if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
|
||||
if ($this->resource->settings->is_env_sorting_enabled) {
|
||||
$this->resource->environment_variables = $this->resource->environment_variables->sortBy('key');
|
||||
$this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('key');
|
||||
} else {
|
||||
$this->resource->environment_variables = $this->resource->environment_variables->sortBy('id');
|
||||
$this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('id');
|
||||
}
|
||||
}
|
||||
$this->getDevView();
|
||||
$this->sortEnvironmentVariables();
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
|
||||
$this->resource->settings->save();
|
||||
$this->dispatch('success', 'Environment variable settings updated.');
|
||||
$this->sortMe();
|
||||
$this->resource->settings->save();
|
||||
$this->sortEnvironmentVariables();
|
||||
$this->dispatch('success', 'Environment variable settings updated.');
|
||||
}
|
||||
|
||||
public function sortEnvironmentVariables()
|
||||
{
|
||||
if ($this->resource->type() === 'application') {
|
||||
$this->resource->load(['environment_variables', 'environment_variables_preview']);
|
||||
} else {
|
||||
$this->resource->load(['environment_variables']);
|
||||
}
|
||||
|
||||
$sortBy = data_get($this->resource, 'settings.is_env_sorting_enabled') ? 'key' : 'order';
|
||||
|
||||
$sortFunction = function ($variables) use ($sortBy) {
|
||||
if (! $variables) {
|
||||
return $variables;
|
||||
}
|
||||
if ($sortBy === 'key') {
|
||||
return $variables->sortBy(function ($item) {
|
||||
return strtolower($item->key);
|
||||
}, SORT_NATURAL | SORT_FLAG_CASE)->values();
|
||||
} else {
|
||||
return $variables->sortBy('order')->values();
|
||||
}
|
||||
};
|
||||
|
||||
$this->resource->environment_variables = $sortFunction($this->resource->environment_variables);
|
||||
$this->resource->environment_variables_preview = $sortFunction($this->resource->environment_variables_preview);
|
||||
|
||||
$this->getDevView();
|
||||
}
|
||||
|
||||
public function getDevView()
|
||||
{
|
||||
$this->variables = $this->resource->environment_variables->map(function ($item) {
|
||||
$this->variables = $this->formatEnvironmentVariables($this->resource->environment_variables);
|
||||
if ($this->showPreview) {
|
||||
$this->variablesPreview = $this->formatEnvironmentVariables($this->resource->environment_variables_preview);
|
||||
}
|
||||
}
|
||||
|
||||
private function formatEnvironmentVariables($variables)
|
||||
{
|
||||
return $variables->map(function ($item) {
|
||||
if ($item->is_shown_once) {
|
||||
return "$item->key=(locked secret)";
|
||||
return "$item->key=(Locked Secret, delete and add again to change)";
|
||||
}
|
||||
if ($item->is_multiline) {
|
||||
return "$item->key=(multiline, edit in normal view)";
|
||||
return "$item->key=(Multiline environment variable, edit in normal view)";
|
||||
}
|
||||
|
||||
return "$item->key=$item->value";
|
||||
})->join('
|
||||
');
|
||||
if ($this->showPreview) {
|
||||
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
|
||||
if ($item->is_shown_once) {
|
||||
return "$item->key=(locked secret)";
|
||||
}
|
||||
if ($item->is_multiline) {
|
||||
return "$item->key=(multiline, edit in normal view)";
|
||||
}
|
||||
|
||||
return "$item->key=$item->value";
|
||||
})->join('
|
||||
');
|
||||
}
|
||||
})->join("\n");
|
||||
}
|
||||
|
||||
public function switch()
|
||||
{
|
||||
if ($this->view === 'normal') {
|
||||
$this->view = 'dev';
|
||||
} else {
|
||||
$this->view = 'normal';
|
||||
}
|
||||
$this->sortMe();
|
||||
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
|
||||
$this->sortEnvironmentVariables();
|
||||
}
|
||||
|
||||
public function saveVariables($isPreview)
|
||||
public function submit($data = null)
|
||||
{
|
||||
if ($isPreview) {
|
||||
$variables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
|
||||
} else {
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
|
||||
}
|
||||
foreach ($variables as $key => $variable) {
|
||||
if ($isPreview) {
|
||||
$found = $this->resource->environment_variables_preview()->where('key', $key)->first();
|
||||
try {
|
||||
if ($data === null) {
|
||||
$this->handleBulkSubmit();
|
||||
} else {
|
||||
$found = $this->resource->environment_variables()->where('key', $key)->first();
|
||||
$this->handleSingleSubmit($data);
|
||||
}
|
||||
if ($found) {
|
||||
if ($found->is_shown_once || $found->is_multiline) {
|
||||
continue;
|
||||
|
||||
$this->updateOrder();
|
||||
$this->sortEnvironmentVariables();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateOrder()
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$order = 1;
|
||||
foreach ($variables as $key => $value) {
|
||||
$env = $this->resource->environment_variables()->where('key', $key)->first();
|
||||
if ($env) {
|
||||
$env->order = $order;
|
||||
$env->save();
|
||||
}
|
||||
$order++;
|
||||
}
|
||||
|
||||
if ($this->showPreview) {
|
||||
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$order = 1;
|
||||
foreach ($previewVariables as $key => $value) {
|
||||
$env = $this->resource->environment_variables_preview()->where('key', $key)->first();
|
||||
if ($env) {
|
||||
$env->order = $order;
|
||||
$env->save();
|
||||
}
|
||||
$found->value = $variable;
|
||||
// if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) {
|
||||
// $type = str($found->value)->after('{{')->before('.')->value;
|
||||
// if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
// $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
|
||||
$order++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
$found->save();
|
||||
private function handleBulkSubmit()
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$this->deleteRemovedVariables(false, $variables);
|
||||
$this->updateOrCreateVariables(false, $variables);
|
||||
|
||||
continue;
|
||||
if ($this->showPreview) {
|
||||
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$this->deleteRemovedVariables(true, $previewVariables);
|
||||
$this->updateOrCreateVariables(true, $previewVariables);
|
||||
}
|
||||
|
||||
$this->dispatch('success', 'Environment variables updated.');
|
||||
}
|
||||
|
||||
private function handleSingleSubmit($data)
|
||||
{
|
||||
$found = $this->resource->environment_variables()->where('key', $data['key'])->first();
|
||||
if ($found) {
|
||||
$this->dispatch('error', 'Environment variable already exists.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$maxOrder = $this->resource->environment_variables()->max('order') ?? 0;
|
||||
$environment = $this->createEnvironmentVariable($data);
|
||||
$environment->order = $maxOrder + 1;
|
||||
$environment->save();
|
||||
}
|
||||
|
||||
private function createEnvironmentVariable($data)
|
||||
{
|
||||
$environment = new EnvironmentVariable;
|
||||
$environment->key = $data['key'];
|
||||
$environment->value = $data['value'];
|
||||
$environment->is_build_time = $data['is_build_time'] ?? false;
|
||||
$environment->is_multiline = $data['is_multiline'] ?? false;
|
||||
$environment->is_literal = $data['is_literal'] ?? false;
|
||||
$environment->is_preview = $data['is_preview'] ?? false;
|
||||
|
||||
$resourceType = $this->resource->type();
|
||||
$resourceIdField = $this->getResourceIdField($resourceType);
|
||||
|
||||
if ($resourceIdField) {
|
||||
$environment->$resourceIdField = $this->resource->id;
|
||||
}
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
private function getResourceIdField($resourceType)
|
||||
{
|
||||
$resourceTypes = [
|
||||
'application' => 'application_id',
|
||||
'standalone-postgresql' => 'standalone_postgresql_id',
|
||||
'standalone-redis' => 'standalone_redis_id',
|
||||
'standalone-mongodb' => 'standalone_mongodb_id',
|
||||
'standalone-mysql' => 'standalone_mysql_id',
|
||||
'standalone-mariadb' => 'standalone_mariadb_id',
|
||||
'standalone-keydb' => 'standalone_keydb_id',
|
||||
'standalone-dragonfly' => 'standalone_dragonfly_id',
|
||||
'standalone-clickhouse' => 'standalone_clickhouse_id',
|
||||
'service' => 'service_id',
|
||||
];
|
||||
|
||||
return $resourceTypes[$resourceType] ?? null;
|
||||
}
|
||||
|
||||
private function deleteRemovedVariables($isPreview, $variables)
|
||||
{
|
||||
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
|
||||
$this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
|
||||
}
|
||||
|
||||
private function updateOrCreateVariables($isPreview, $variables)
|
||||
{
|
||||
foreach ($variables as $key => $value) {
|
||||
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
|
||||
$found = $this->resource->$method()->where('key', $key)->first();
|
||||
|
||||
if ($found) {
|
||||
if (! $found->is_shown_once && ! $found->is_multiline) {
|
||||
$found->value = $value;
|
||||
$found->save();
|
||||
}
|
||||
} else {
|
||||
$environment = new EnvironmentVariable;
|
||||
$environment->key = $key;
|
||||
$environment->value = $variable;
|
||||
// if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) {
|
||||
// $type = str($environment->value)->after('{{')->before('.')->value;
|
||||
// if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
// $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
|
||||
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
$environment->value = $value;
|
||||
$environment->is_build_time = false;
|
||||
$environment->is_multiline = false;
|
||||
$environment->is_preview = $isPreview ? true : false;
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
$environment->application_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
$environment->standalone_postgresql_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
$environment->standalone_redis_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
$environment->standalone_mongodb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
$environment->standalone_mysql_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mariadb':
|
||||
$environment->standalone_mariadb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-keydb':
|
||||
$environment->standalone_keydb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-dragonfly':
|
||||
$environment->standalone_dragonfly_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-clickhouse':
|
||||
$environment->standalone_clickhouse_id = $this->resource->id;
|
||||
break;
|
||||
case 'service':
|
||||
$environment->service_id = $this->resource->id;
|
||||
break;
|
||||
}
|
||||
$environment->is_preview = $isPreview;
|
||||
|
||||
$this->setEnvironmentResourceId($environment);
|
||||
$environment->save();
|
||||
}
|
||||
}
|
||||
if ($isPreview) {
|
||||
$this->dispatch('success', 'Preview environment variables updated.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Environment variables updated.');
|
||||
}
|
||||
|
||||
private function setEnvironmentResourceId($environment)
|
||||
{
|
||||
$resourceTypes = [
|
||||
'application' => 'application_id',
|
||||
'standalone-postgresql' => 'standalone_postgresql_id',
|
||||
'standalone-redis' => 'standalone_redis_id',
|
||||
'standalone-mongodb' => 'standalone_mongodb_id',
|
||||
'standalone-mysql' => 'standalone_mysql_id',
|
||||
'standalone-mariadb' => 'standalone_mariadb_id',
|
||||
'standalone-keydb' => 'standalone_keydb_id',
|
||||
'standalone-dragonfly' => 'standalone_dragonfly_id',
|
||||
'standalone-clickhouse' => 'standalone_clickhouse_id',
|
||||
'service' => 'service_id',
|
||||
];
|
||||
|
||||
$resourceType = $this->resource->type();
|
||||
if (isset($resourceTypes[$resourceType])) {
|
||||
$environment->{$resourceTypes[$resourceType]} = $this->resource->id;
|
||||
}
|
||||
$this->refreshEnvs();
|
||||
}
|
||||
|
||||
public function refreshEnvs()
|
||||
{
|
||||
$this->resource->refresh();
|
||||
$this->sortEnvironmentVariables();
|
||||
$this->getDevView();
|
||||
}
|
||||
|
||||
public function submit($data)
|
||||
{
|
||||
try {
|
||||
$found = $this->resource->environment_variables()->where('key', $data['key'])->first();
|
||||
if ($found) {
|
||||
$this->dispatch('error', 'Environment variable already exists.');
|
||||
|
||||
return;
|
||||
}
|
||||
$environment = new EnvironmentVariable;
|
||||
$environment->key = $data['key'];
|
||||
$environment->value = $data['value'];
|
||||
$environment->is_build_time = $data['is_build_time'];
|
||||
$environment->is_multiline = $data['is_multiline'];
|
||||
$environment->is_literal = $data['is_literal'];
|
||||
$environment->is_preview = $data['is_preview'];
|
||||
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
$environment->application_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
$environment->standalone_postgresql_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
$environment->standalone_redis_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
$environment->standalone_mongodb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
$environment->standalone_mysql_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-mariadb':
|
||||
$environment->standalone_mariadb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-keydb':
|
||||
$environment->standalone_keydb_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-dragonfly':
|
||||
$environment->standalone_dragonfly_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-clickhouse':
|
||||
$environment->standalone_clickhouse_id = $this->resource->id;
|
||||
break;
|
||||
case 'service':
|
||||
$environment->service_id = $this->resource->id;
|
||||
break;
|
||||
}
|
||||
$environment->save();
|
||||
$this->refreshEnvs();
|
||||
$this->dispatch('success', 'Environment variable added.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ class Show extends Component
|
||||
public string $type;
|
||||
|
||||
protected $listeners = [
|
||||
'refresh' => 'refresh',
|
||||
'refreshEnvs' => 'refresh',
|
||||
'refresh',
|
||||
'compose_loaded' => '$refresh',
|
||||
];
|
||||
|
||||
@@ -129,7 +130,8 @@ class Show extends Component
|
||||
{
|
||||
try {
|
||||
$this->env->delete();
|
||||
$this->dispatch('refreshEnvs');
|
||||
$this->dispatch('environmentVariableDeleted');
|
||||
$this->dispatch('success', 'Environment variable deleted successfully.');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Actions\Server\RunCommand;
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class ExecuteContainerCommand extends Component
|
||||
{
|
||||
public string $command;
|
||||
|
||||
public string $container;
|
||||
public $container;
|
||||
|
||||
public Collection $containers;
|
||||
|
||||
@@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component
|
||||
|
||||
public string $type;
|
||||
|
||||
public string $workDir = '';
|
||||
|
||||
public Server $server;
|
||||
|
||||
public Collection $servers;
|
||||
@@ -33,11 +29,13 @@ class ExecuteContainerCommand extends Component
|
||||
'server' => 'required',
|
||||
'container' => 'required',
|
||||
'command' => 'required',
|
||||
'workDir' => 'nullable',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (! auth()->user()->isAdmin()) {
|
||||
abort(403);
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->containers = collect();
|
||||
$this->servers = collect();
|
||||
@@ -62,24 +60,13 @@ class ExecuteContainerCommand extends Component
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||
}
|
||||
$this->container = $this->resource->uuid;
|
||||
$this->containers->push($this->container);
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
$this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
|
||||
});
|
||||
$this->resource->databases()->get()->each(function ($database) {
|
||||
$this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid'));
|
||||
});
|
||||
if ($this->resource->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->server);
|
||||
}
|
||||
}
|
||||
if ($this->containers->count() > 0) {
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
}
|
||||
|
||||
public function loadContainers()
|
||||
@@ -102,44 +89,65 @@ class ExecuteContainerCommand extends Component
|
||||
];
|
||||
$this->containers = $this->containers->push($payload);
|
||||
}
|
||||
} elseif (data_get($this->parameters, 'database_uuid')) {
|
||||
if ($this->resource->isRunning()) {
|
||||
$this->containers = $this->containers->push([
|
||||
'server' => $server,
|
||||
'container' => [
|
||||
'Names' => $this->resource->uuid,
|
||||
],
|
||||
]);
|
||||
}
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
ray($application);
|
||||
if ($application->isRunning()) {
|
||||
$this->containers->push([
|
||||
'server' => $this->resource->server,
|
||||
'container' => [
|
||||
'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
});
|
||||
$this->resource->databases()->get()->each(function ($database) {
|
||||
if ($database->isRunning()) {
|
||||
$this->containers->push([
|
||||
'server' => $this->resource->server,
|
||||
'container' => [
|
||||
'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
if ($this->containers->count() > 0) {
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->container = data_get($this->containers->first(), 'container.Names');
|
||||
} elseif (data_get($this->parameters, 'database_uuid')) {
|
||||
$this->container = $this->containers->first();
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
}
|
||||
|
||||
public function runCommand()
|
||||
#[On('connectToContainer')]
|
||||
public function connectToContainer()
|
||||
{
|
||||
try {
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$container = $this->containers->where('container.Names', $this->container)->first();
|
||||
$container_name = data_get($container, 'container.Names');
|
||||
if (is_null($container)) {
|
||||
throw new \RuntimeException('Container not found.');
|
||||
}
|
||||
$server = data_get($container, 'server');
|
||||
} else {
|
||||
$container_name = $this->container;
|
||||
$server = $this->servers->first();
|
||||
$container_name = data_get($this->container, 'container.Names');
|
||||
if (is_null($container_name)) {
|
||||
throw new \RuntimeException('Container not found.');
|
||||
}
|
||||
$server = data_get($this->container, 'server');
|
||||
|
||||
if ($server->isForceDisabled()) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
$cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'";
|
||||
if (! empty($this->workDir)) {
|
||||
$exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
|
||||
} else {
|
||||
$exec = "docker exec {$container_name} {$cmd}";
|
||||
}
|
||||
$activity = RunCommand::run(server: $server, command: $exec);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
|
||||
$this->dispatch('send-terminal-command',
|
||||
true,
|
||||
$container_name,
|
||||
$server->uuid,
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
@@ -97,7 +98,7 @@ class GetLogs extends Component
|
||||
if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) {
|
||||
return;
|
||||
}
|
||||
if (! $this->numberOfLines) {
|
||||
if ($this->numberOfLines <= 0) {
|
||||
$this->numberOfLines = 1000;
|
||||
}
|
||||
if ($this->container) {
|
||||
@@ -108,14 +109,14 @@ class GetLogs extends Component
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
} else {
|
||||
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
}
|
||||
} else {
|
||||
if ($this->server->isSwarm()) {
|
||||
@@ -124,14 +125,14 @@ class GetLogs extends Component
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
} else {
|
||||
$command = "docker logs -n {$this->numberOfLines} {$this->container}";
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
$sshCommand = generateSshCommand($this->server, $command);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
}
|
||||
}
|
||||
if ($refresh) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user