Merge branch 'next' into feat/disable-default-redirect
This commit is contained in:
15
.env.dusk.ci
Normal file
15
.env.dusk.ci
Normal file
@@ -0,0 +1,15 @@
|
||||
APP_ENV=production
|
||||
APP_NAME="Coolify Staging"
|
||||
APP_ID=development
|
||||
APP_KEY=
|
||||
APP_URL=http://localhost
|
||||
APP_PORT=8000
|
||||
SSH_MUX_ENABLED=true
|
||||
|
||||
# PostgreSQL Database Configuration
|
||||
DB_DATABASE=coolify
|
||||
DB_USERNAME=coolify
|
||||
DB_PASSWORD=password
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
|
@@ -4,6 +4,7 @@ APP_ID=coolify-windows-docker-desktop
|
||||
APP_NAME=Coolify
|
||||
APP_KEY=base64:ssTlCmrIE/q7whnKMvT6DwURikg69COzGsAwFVROm80=
|
||||
|
||||
DB_USERNAME=coolify
|
||||
DB_PASSWORD=coolify
|
||||
REDIS_PASSWORD=coolify
|
||||
|
||||
|
65
.github/workflows/browser-tests.yml
vendored
Normal file
65
.github/workflows/browser-tests.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: Dusk
|
||||
on:
|
||||
push:
|
||||
branches: [ "not-existing" ]
|
||||
jobs:
|
||||
dusk:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
env:
|
||||
REDIS_HOST: localhost
|
||||
REDIS_PORT: 6379
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up PostgreSQL
|
||||
run: |
|
||||
sudo systemctl start postgresql
|
||||
sudo -u postgres psql -c "CREATE DATABASE coolify;"
|
||||
sudo -u postgres psql -c "CREATE USER coolify WITH PASSWORD 'password';"
|
||||
sudo -u postgres psql -c "ALTER ROLE coolify SET client_encoding TO 'utf8';"
|
||||
sudo -u postgres psql -c "ALTER ROLE coolify SET default_transaction_isolation TO 'read committed';"
|
||||
sudo -u postgres psql -c "ALTER ROLE coolify SET timezone TO 'UTC';"
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE coolify TO coolify;"
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.2'
|
||||
- name: Copy .env
|
||||
run: cp .env.dusk.ci .env
|
||||
- name: Install Dependencies
|
||||
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||
- name: Generate key
|
||||
run: php artisan key:generate
|
||||
- name: Install Chrome binaries
|
||||
run: php artisan dusk:chrome-driver --detect
|
||||
- name: Start Chrome Driver
|
||||
run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=4444 &
|
||||
- name: Build assets
|
||||
run: npm install && npm run build
|
||||
- name: Run Laravel Server
|
||||
run: php artisan serve --no-reload &
|
||||
- name: Execute tests
|
||||
run: php artisan dusk
|
||||
- name: Upload Screenshots
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: screenshots
|
||||
path: tests/Browser/screenshots
|
||||
- name: Upload Console Logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: console
|
||||
path: tests/Browser/console
|
@@ -12,6 +12,7 @@ class GenerateConfig
|
||||
public function handle(Application $application, bool $is_json = false)
|
||||
{
|
||||
ray()->clearAll();
|
||||
|
||||
return $application->generateConfig(is_json: $is_json);
|
||||
}
|
||||
}
|
||||
|
@@ -21,8 +21,6 @@ class StartRedis
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
@@ -37,6 +35,8 @@ class StartRedis
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->add_custom_redis();
|
||||
|
||||
$startCommand = $this->buildStartCommand();
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
$container_name => [
|
||||
@@ -105,7 +105,6 @@ class StartRedis
|
||||
'target' => '/usr/local/etc/redis/redis.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
@@ -160,12 +159,26 @@ class StartRedis
|
||||
private function generate_environment_variables()
|
||||
{
|
||||
$environment_variables = collect();
|
||||
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
if ($env->is_shared) {
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
|
||||
if ($env->key === 'REDIS_PASSWORD') {
|
||||
$this->database->update(['redis_password' => $env->real_value]);
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
|
||||
if ($env->key === 'REDIS_USERNAME') {
|
||||
$this->database->update(['redis_username' => $env->real_value]);
|
||||
}
|
||||
} else {
|
||||
if ($env->key === 'REDIS_PASSWORD') {
|
||||
$env->update(['value' => $this->database->redis_password]);
|
||||
} elseif ($env->key === 'REDIS_USERNAME') {
|
||||
$env->update(['value' => $this->database->redis_username]);
|
||||
}
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
||||
@@ -173,6 +186,27 @@ class StartRedis
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
private function buildStartCommand(): string
|
||||
{
|
||||
$hasRedisConf = ! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf);
|
||||
$redisConfPath = '/usr/local/etc/redis/redis.conf';
|
||||
|
||||
if ($hasRedisConf) {
|
||||
$confContent = $this->database->redis_conf;
|
||||
$hasRequirePass = str_contains($confContent, 'requirepass');
|
||||
|
||||
if ($hasRequirePass) {
|
||||
$command = "redis-server $redisConfPath";
|
||||
} else {
|
||||
$command = "redis-server $redisConfPath --requirepass {$this->database->redis_password}";
|
||||
}
|
||||
} else {
|
||||
$command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
}
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
private function add_custom_redis()
|
||||
{
|
||||
if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) {
|
||||
|
@@ -651,31 +651,5 @@ class GetContainersStatus
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
|
||||
if (! $this->server->proxySet() || $this->server->proxy->force_stop) {
|
||||
return;
|
||||
}
|
||||
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
}
|
||||
} else {
|
||||
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
||||
$this->server->save();
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
app/Actions/Server/DeleteServer.php
Normal file
17
app/Actions/Server/DeleteServer.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class DeleteServer
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
StopSentinel::run($server);
|
||||
$server->forceDelete();
|
||||
}
|
||||
}
|
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -10,32 +9,48 @@ class StartSentinel
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, $version = 'latest', bool $restart = false)
|
||||
public function handle(Server $server, $version = 'next', bool $restart = false)
|
||||
{
|
||||
if ($restart) {
|
||||
StopSentinel::run($server);
|
||||
}
|
||||
$metrics_history = $server->settings->metrics_history_days;
|
||||
$refresh_rate = $server->settings->metrics_refresh_rate_seconds;
|
||||
$token = $server->settings->sentinel_token;
|
||||
$fqdn = InstanceSettings::get()->fqdn;
|
||||
if (str($fqdn)->startsWith('http')) {
|
||||
throw new \Exception('You should use https to run Sentinel.');
|
||||
$metrics_history = data_get($server, 'settings.sentinel_metrics_history_days');
|
||||
$refresh_rate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
|
||||
$push_interval = data_get($server, 'settings.sentinel_push_interval_seconds');
|
||||
$token = data_get($server, 'settings.sentinel_token');
|
||||
$endpoint = data_get($server, 'settings.sentinel_custom_url');
|
||||
$mount_dir = '/data/coolify/sentinel';
|
||||
$image = "ghcr.io/coollabsio/sentinel:$version";
|
||||
if (! $endpoint) {
|
||||
throw new \Exception('You should set FQDN in Instance Settings.');
|
||||
}
|
||||
$environments = [
|
||||
'TOKEN' => $token,
|
||||
'ENDPOINT' => InstanceSettings::get()->fqdn,
|
||||
'COLLECTOR_ENABLED' => 'true',
|
||||
'PUSH_ENDPOINT' => $endpoint,
|
||||
'PUSH_INTERVAL_SECONDS' => $push_interval,
|
||||
'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false',
|
||||
'COLLECTOR_REFRESH_RATE_SECONDS' => $refresh_rate,
|
||||
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history
|
||||
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history,
|
||||
];
|
||||
$docker_environments = "-e \"" . implode("\" -e \"", array_map(fn($key, $value) => "$key=$value", array_keys($environments), $environments)) . "\"";
|
||||
ray($docker_environments);
|
||||
return true;
|
||||
// instant_remote_process([
|
||||
// "docker run --rm --pull always -d $docker_environments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/sentinel:/app/sentinel --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version",
|
||||
// 'chown -R 9999:root /data/coolify/sentinel',
|
||||
// 'chmod -R 700 /data/coolify/sentinel',
|
||||
// ], $server, true);
|
||||
if (isDev()) {
|
||||
// data_set($environments, 'DEBUG', 'true');
|
||||
$mount_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel';
|
||||
// $image = 'sentinel';
|
||||
}
|
||||
$docker_environments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"';
|
||||
|
||||
$docker_command = "docker run -d $docker_environments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mount_dir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway $image";
|
||||
|
||||
instant_remote_process([
|
||||
'docker rm -f coolify-sentinel || true',
|
||||
"mkdir -p $mount_dir",
|
||||
$docker_command,
|
||||
"chown -R 9999:root $mount_dir",
|
||||
"chmod -R 700 $mount_dir",
|
||||
], $server);
|
||||
|
||||
$server->settings->is_sentinel_enabled = true;
|
||||
$server->settings->save();
|
||||
$server->sentinelHeartbeat();
|
||||
}
|
||||
}
|
||||
|
@@ -12,5 +12,6 @@ class StopSentinel
|
||||
public function handle(Server $server)
|
||||
{
|
||||
instant_remote_process(['docker rm -f coolify-sentinel'], $server, false);
|
||||
$server->sentinelHeartbeat(isReset: true);
|
||||
}
|
||||
}
|
||||
|
@@ -3,16 +3,15 @@
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\CleanupStaleMultiplexedConnections;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\ServerCheckJob;
|
||||
use App\Jobs\ServerStorageCheckJob;
|
||||
use App\Jobs\UpdateCoolifyJob;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
@@ -20,6 +19,7 @@ use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
@@ -38,13 +38,13 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
|
||||
// Server Jobs
|
||||
$this->check_scheduled_backups($schedule);
|
||||
// $this->check_resources($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();
|
||||
$schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->onOneServer();
|
||||
} else {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
@@ -80,7 +80,7 @@ class Kernel extends ConsoleKernel
|
||||
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
$schedule->job(new PullHelperImageJob)
|
||||
$schedule->job(new CheckHelperImageJob)
|
||||
->cron($settings->update_check_frequency)
|
||||
->timezone($settings->instance_timezone)
|
||||
->onOneServer();
|
||||
@@ -115,7 +115,10 @@ class Kernel extends ConsoleKernel
|
||||
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
$last_sentinel_update = $server->sentinel_updated_at;
|
||||
if (Carbon::parse($last_sentinel_update)->isBefore(now()->subMinutes(4))) {
|
||||
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
|
||||
}
|
||||
// $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer();
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
if ($server->settings->force_docker_cleanup) {
|
||||
|
@@ -1579,11 +1579,16 @@ class ApplicationsController extends Controller
|
||||
$request->offsetUnset('docker_compose_domains');
|
||||
}
|
||||
$instantDeploy = $request->instant_deploy;
|
||||
$isStatic = $request->is_static;
|
||||
$useBuildServer = $request->use_build_server;
|
||||
|
||||
$use_build_server = $request->use_build_server;
|
||||
if (isset($useBuildServer)) {
|
||||
$application->settings->is_build_server_enabled = $useBuildServer;
|
||||
$application->settings->save();
|
||||
}
|
||||
|
||||
if (isset($use_build_server)) {
|
||||
$application->settings->is_build_server_enabled = $use_build_server;
|
||||
if (isset($isStatic)) {
|
||||
$application->settings->is_static = $isStatic;
|
||||
$application->settings->save();
|
||||
}
|
||||
|
||||
|
@@ -160,7 +160,7 @@ class OtherController extends Controller
|
||||
#[OA\Get(
|
||||
summary: 'Healthcheck',
|
||||
description: 'Healthcheck endpoint.',
|
||||
path: '/healthcheck',
|
||||
path: '/health',
|
||||
operationId: 'healthcheck',
|
||||
responses: [
|
||||
new OA\Response(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use App\Actions\Server\ValidateServer;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
@@ -726,6 +727,7 @@ class ServersController extends Controller
|
||||
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
|
||||
}
|
||||
$server->delete();
|
||||
DeleteServer::dispatch($server);
|
||||
|
||||
return response()->json(['message' => 'Server deleted.']);
|
||||
}
|
||||
|
40
app/Jobs/CheckHelperImageJob.php
Normal file
40
app/Jobs/CheckHelperImageJob.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CheckHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
$settings = instanceSettings();
|
||||
$latest_version = data_get($versions, 'coolify.helper.version');
|
||||
$current_version = $settings->helper_version;
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
$settings->update(['helper_version' => $latest_version]);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('CheckHelperImageJob failed with: '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
@@ -504,8 +504,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$network = $this->database->destination->network;
|
||||
}
|
||||
|
||||
$this->ensureHelperImageAvailable();
|
||||
|
||||
$fullImageName = $this->getFullImageName();
|
||||
|
||||
if (isDev()) {
|
||||
@@ -538,35 +536,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
@@ -17,28 +16,15 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
public function __construct() {}
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
$settings = instanceSettings();
|
||||
$latest_version = data_get($versions, 'coolify.helper.version');
|
||||
$current_version = $settings->helper_version;
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$latest_version = instanceSettings()->helper_version;
|
||||
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage());
|
||||
ray($e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
@@ -2,17 +2,23 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Server\InstallLogDrain;
|
||||
use App\Actions\Shared\ComplexStatusCheck;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
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\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PushServerUpdateJob implements ShouldQueue
|
||||
{
|
||||
@@ -20,7 +26,45 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 60;
|
||||
public $timeout = 30;
|
||||
|
||||
public Collection $containers;
|
||||
|
||||
public Collection $applications;
|
||||
|
||||
public Collection $previews;
|
||||
|
||||
public Collection $databases;
|
||||
|
||||
public Collection $services;
|
||||
|
||||
public Collection $allApplicationIds;
|
||||
|
||||
public Collection $allDatabaseUuids;
|
||||
|
||||
public Collection $allTcpProxyUuids;
|
||||
|
||||
public Collection $allServiceApplicationIds;
|
||||
|
||||
public Collection $allApplicationPreviewsIds;
|
||||
|
||||
public Collection $allServiceDatabaseIds;
|
||||
|
||||
public Collection $allApplicationsWithAdditionalServers;
|
||||
|
||||
public Collection $foundApplicationIds;
|
||||
|
||||
public Collection $foundDatabaseUuids;
|
||||
|
||||
public Collection $foundServiceApplicationIds;
|
||||
|
||||
public Collection $foundServiceDatabaseIds;
|
||||
|
||||
public Collection $foundApplicationPreviewsIds;
|
||||
|
||||
public bool $foundProxy = false;
|
||||
|
||||
public bool $foundLogDrainContainer = false;
|
||||
|
||||
public function backoff(): int
|
||||
{
|
||||
@@ -29,108 +73,335 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
|
||||
public function __construct(public Server $server, public $data)
|
||||
{
|
||||
// TODO: Handle multiple servers
|
||||
// TODO: Handle Preview deployments
|
||||
// TODO: Handle DB TCP proxies
|
||||
// TODO: Handle DBs
|
||||
// TODO: Handle services
|
||||
// TODO: Handle proxies
|
||||
$this->containers = collect();
|
||||
$this->foundApplicationIds = collect();
|
||||
$this->foundDatabaseUuids = collect();
|
||||
$this->foundServiceApplicationIds = collect();
|
||||
$this->foundApplicationPreviewsIds = collect();
|
||||
$this->foundServiceDatabaseIds = collect();
|
||||
$this->allApplicationIds = collect();
|
||||
$this->allDatabaseUuids = collect();
|
||||
$this->allTcpProxyUuids = collect();
|
||||
$this->allServiceApplicationIds = collect();
|
||||
$this->allServiceDatabaseIds = collect();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
if (! $this->data) {
|
||||
throw new \Exception('No data provided');
|
||||
}
|
||||
$data = collect($this->data);
|
||||
$containers = collect(data_get($data, 'containers'));
|
||||
if ($containers->isEmpty()) {
|
||||
|
||||
$this->serverStatus();
|
||||
|
||||
$this->server->sentinelHeartbeat();
|
||||
|
||||
$this->containers = collect(data_get($data, 'containers'));
|
||||
if ($this->containers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
$foundApplicationIds = collect();
|
||||
$foundServiceIds = collect();
|
||||
$foundProxy = false;
|
||||
foreach ($containers as $container) {
|
||||
$this->applications = $this->server->applications();
|
||||
$this->databases = $this->server->databases();
|
||||
$this->previews = $this->server->previews();
|
||||
$this->services = $this->server->services()->get();
|
||||
$this->allApplicationIds = $this->applications->filter(function ($application) {
|
||||
return $application->additional_servers->count() === 0;
|
||||
})->pluck('id');
|
||||
$this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) {
|
||||
return $application->additional_servers->count() > 0;
|
||||
});
|
||||
$this->allApplicationPreviewsIds = $this->previews->pluck('id');
|
||||
$this->allDatabaseUuids = $this->databases->pluck('uuid');
|
||||
$this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid');
|
||||
$this->services->each(function ($service) {
|
||||
$service->applications()->pluck('id')->each(function ($applicationId) {
|
||||
$this->allServiceApplicationIds->push($applicationId);
|
||||
});
|
||||
$service->databases()->pluck('id')->each(function ($databaseId) {
|
||||
$this->allServiceDatabaseIds->push($databaseId);
|
||||
});
|
||||
});
|
||||
|
||||
ray('allServiceApplicationIds', ['allServiceApplicationIds' => $this->allServiceApplicationIds]);
|
||||
|
||||
foreach ($this->containers as $container) {
|
||||
$containerStatus = data_get($container, 'state', 'exited');
|
||||
$containerHealth = data_get($container, 'health_status', 'unhealthy');
|
||||
$containerStatus = "$containerStatus ($containerHealth)";
|
||||
$labels = collect(data_get($container, 'labels'));
|
||||
$coolify_managed = $labels->has('coolify.managed');
|
||||
if ($coolify_managed) {
|
||||
$name = data_get($container, 'name');
|
||||
if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) {
|
||||
$this->foundLogDrainContainer = true;
|
||||
}
|
||||
if ($labels->has('coolify.applicationId')) {
|
||||
$applicationId = $labels->get('coolify.applicationId');
|
||||
$pullRequestId = data_get($labels, 'coolify.pullRequestId', '0');
|
||||
$foundApplicationIds->push($applicationId);
|
||||
try {
|
||||
$this->updateApplicationStatus($applicationId, $pullRequestId, $containerStatus);
|
||||
if ($pullRequestId === '0') {
|
||||
if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) {
|
||||
$this->foundApplicationIds->push($applicationId);
|
||||
}
|
||||
$this->updateApplicationStatus($applicationId, $containerStatus);
|
||||
} else {
|
||||
if ($this->allApplicationPreviewsIds->contains($applicationId) && $this->isRunning($containerStatus)) {
|
||||
$this->foundApplicationPreviewsIds->push($applicationId);
|
||||
}
|
||||
$this->updateApplicationPreviewStatus($applicationId, $containerStatus);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
ray()->error($e);
|
||||
}
|
||||
} elseif ($labels->has('coolify.serviceId')) {
|
||||
$serviceId = $labels->get('coolify.serviceId');
|
||||
$foundServiceIds->push($serviceId);
|
||||
Log::info("Service: $serviceId, $containerStatus");
|
||||
$subType = $labels->get('coolify.service.subType');
|
||||
$subId = $labels->get('coolify.service.subId');
|
||||
if ($subType === 'application' && $this->isRunning($containerStatus)) {
|
||||
$this->foundServiceApplicationIds->push($subId);
|
||||
$this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
|
||||
} elseif ($subType === 'database' && $this->isRunning($containerStatus)) {
|
||||
$this->foundServiceDatabaseIds->push($subId);
|
||||
$this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
|
||||
}
|
||||
|
||||
} else {
|
||||
$name = data_get($container, 'name');
|
||||
$uuid = $labels->get('com.docker.compose.service');
|
||||
$type = $labels->get('coolify.type');
|
||||
if ($name === 'coolify-proxy') {
|
||||
$foundProxy = true;
|
||||
Log::info("Proxy: $uuid, $containerStatus");
|
||||
} elseif ($type === 'service') {
|
||||
Log::info("Service: $uuid, $containerStatus");
|
||||
if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) {
|
||||
$this->foundProxy = true;
|
||||
} elseif ($type === 'service' && $this->isRunning($containerStatus)) {
|
||||
ray("Service: $uuid, $containerStatus");
|
||||
} else {
|
||||
Log::info("Database: $uuid, $containerStatus");
|
||||
if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) {
|
||||
$this->foundDatabaseUuids->push($uuid);
|
||||
if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
|
||||
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
|
||||
} else {
|
||||
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If proxy is not found, start it
|
||||
if (! $foundProxy && $this->server->isProxyShouldRun()) {
|
||||
Log::info('Proxy not found, starting it');
|
||||
StartProxy::dispatch($this->server);
|
||||
$this->updateProxyStatus();
|
||||
|
||||
$this->updateNotFoundApplicationStatus();
|
||||
$this->updateNotFoundApplicationPreviewStatus();
|
||||
$this->updateNotFoundDatabaseStatus();
|
||||
$this->updateNotFoundServiceStatus();
|
||||
|
||||
$this->updateAdditionalServersStatus();
|
||||
|
||||
$this->checkLogDrainContainer();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Update not found applications
|
||||
$allApplicationIds = $this->server->applications()->pluck('id');
|
||||
$notFoundApplicationIds = $allApplicationIds->diff($foundApplicationIds);
|
||||
}
|
||||
|
||||
private function serverStatus()
|
||||
{
|
||||
if ($this->server->isFunctional() === false) {
|
||||
throw new \Exception('Server is not ready.');
|
||||
}
|
||||
if ($this->server->status() === false) {
|
||||
throw new \Exception('Server is not reachable.');
|
||||
}
|
||||
}
|
||||
|
||||
private function updateApplicationStatus(string $applicationId, string $containerStatus)
|
||||
{
|
||||
$application = $this->applications->where('id', $applicationId)->first();
|
||||
if (! $application) {
|
||||
return;
|
||||
}
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
ray('Application updated', ['application_id' => $applicationId, 'status' => $containerStatus]);
|
||||
}
|
||||
|
||||
private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus)
|
||||
{
|
||||
$application = $this->previews->where('id', $applicationId)->first();
|
||||
if (! $application) {
|
||||
return;
|
||||
}
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
ray('Application preview updated', ['application_id' => $applicationId, 'status' => $containerStatus]);
|
||||
}
|
||||
|
||||
private function updateNotFoundApplicationStatus()
|
||||
{
|
||||
$notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds);
|
||||
if ($notFoundApplicationIds->isNotEmpty()) {
|
||||
Log::info('Not found application ids', ['application_ids' => $notFoundApplicationIds]);
|
||||
$this->updateNotFoundApplications($notFoundApplicationIds);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateApplicationStatus(string $applicationId, string $pullRequestId, string $containerStatus)
|
||||
{
|
||||
if ($pullRequestId === '0') {
|
||||
$application = Application::find($applicationId);
|
||||
if (! $application) {
|
||||
return;
|
||||
}
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
Log::info('Application updated', ['application_id' => $applicationId, 'status' => $containerStatus]);
|
||||
} else {
|
||||
$application = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
|
||||
if (! $application) {
|
||||
return;
|
||||
}
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function updateNotFoundApplications(Collection $applicationIds)
|
||||
{
|
||||
$applicationIds->each(function ($applicationId) {
|
||||
Log::info('Updating application status', ['application_id' => $applicationId, 'status' => 'exited']);
|
||||
ray('Not found application ids', ['application_ids' => $notFoundApplicationIds]);
|
||||
$notFoundApplicationIds->each(function ($applicationId) {
|
||||
ray('Updating application status', ['application_id' => $applicationId, 'status' => 'exited']);
|
||||
$application = Application::find($applicationId);
|
||||
if ($application) {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
Log::info('Application status updated', ['application_id' => $applicationId, 'status' => 'exited']);
|
||||
ray('Application status updated', ['application_id' => $applicationId, 'status' => 'exited']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function updateNotFoundApplicationPreviewStatus()
|
||||
{
|
||||
$notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds);
|
||||
if ($notFoundApplicationPreviewsIds->isNotEmpty()) {
|
||||
ray('Not found application previews ids', ['application_previews_ids' => $notFoundApplicationPreviewsIds]);
|
||||
$notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) {
|
||||
ray('Updating application preview status', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']);
|
||||
$applicationPreview = ApplicationPreview::find($applicationPreviewId);
|
||||
if ($applicationPreview) {
|
||||
$applicationPreview->status = 'exited';
|
||||
$applicationPreview->save();
|
||||
ray('Application preview status updated', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function updateProxyStatus()
|
||||
{
|
||||
// If proxy is not found, start it
|
||||
if ($this->server->isProxyShouldRun()) {
|
||||
if ($this->foundProxy === false) {
|
||||
try {
|
||||
if (CheckProxy::run($this->server)) {
|
||||
StartProxy::run($this->server, false);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
} else {
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, bool $tcpProxy = false)
|
||||
{
|
||||
$database = $this->databases->where('uuid', $databaseUuid)->first();
|
||||
if (! $database) {
|
||||
return;
|
||||
}
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => $containerStatus]);
|
||||
if ($this->isRunning($containerStatus) && $tcpProxy) {
|
||||
$tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) {
|
||||
return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running';
|
||||
})->first();
|
||||
if (! $tcpProxyContainerFound) {
|
||||
ray('Starting TCP proxy for database', ['database_uuid' => $databaseUuid]);
|
||||
StartDatabaseProxy::dispatch($database);
|
||||
} else {
|
||||
ray('TCP proxy for database found in containers', ['database_uuid' => $databaseUuid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function updateNotFoundDatabaseStatus()
|
||||
{
|
||||
$notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids);
|
||||
if ($notFoundDatabaseUuids->isNotEmpty()) {
|
||||
ray('Not found database uuids', ['database_uuids' => $notFoundDatabaseUuids]);
|
||||
$notFoundDatabaseUuids->each(function ($databaseUuid) {
|
||||
ray('Updating database status', ['database_uuid' => $databaseUuid, 'status' => 'exited']);
|
||||
$database = $this->databases->where('uuid', $databaseUuid)->first();
|
||||
if ($database) {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => 'exited']);
|
||||
ray('Database is public', ['database_uuid' => $databaseUuid, 'is_public' => $database->is_public]);
|
||||
if ($database->is_public) {
|
||||
ray('Stopping TCP proxy for database', ['database_uuid' => $databaseUuid]);
|
||||
StopDatabaseProxy::dispatch($database);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus)
|
||||
{
|
||||
$service = $this->services->where('id', $serviceId)->first();
|
||||
if (! $service) {
|
||||
return;
|
||||
}
|
||||
if ($subType === 'application') {
|
||||
$application = $service->applications()->where('id', $subId)->first();
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
ray('Service application updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
|
||||
} elseif ($subType === 'database') {
|
||||
$database = $service->databases()->where('id', $subId)->first();
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
ray('Service database updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
|
||||
} else {
|
||||
ray()->warning('Unknown sub type', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateNotFoundServiceStatus()
|
||||
{
|
||||
$notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds);
|
||||
$notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds);
|
||||
if ($notFoundServiceApplicationIds->isNotEmpty()) {
|
||||
ray('Not found service application ids', ['service_application_ids' => $notFoundServiceApplicationIds]);
|
||||
$notFoundServiceApplicationIds->each(function ($serviceApplicationId) {
|
||||
ray('Updating service application status', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']);
|
||||
$application = ServiceApplication::find($serviceApplicationId);
|
||||
if ($application) {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
ray('Service application status updated', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']);
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($notFoundServiceDatabaseIds->isNotEmpty()) {
|
||||
ray('Not found service database ids', ['service_database_ids' => $notFoundServiceDatabaseIds]);
|
||||
$notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) {
|
||||
ray('Updating service database status', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']);
|
||||
$database = ServiceDatabase::find($serviceDatabaseId);
|
||||
if ($database) {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
ray('Service database status updated', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function updateAdditionalServersStatus()
|
||||
{
|
||||
$this->allApplicationsWithAdditionalServers->each(function ($application) {
|
||||
ray('Updating additional servers status for application', ['application_id' => $application->id]);
|
||||
ComplexStatusCheck::run($application);
|
||||
});
|
||||
}
|
||||
|
||||
private function isRunning(string $containerStatus)
|
||||
{
|
||||
return str($containerStatus)->contains('running');
|
||||
}
|
||||
|
||||
private function checkLogDrainContainer()
|
||||
{
|
||||
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
|
||||
InstallLogDrain::dispatch($this->server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -72,6 +72,32 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->server->isLogDrainEnabled()) {
|
||||
$this->checkLogDrainContainer();
|
||||
}
|
||||
if ($this->server->proxySet() && ! $this->server->proxy->force_stop) {
|
||||
$this->server->proxyType();
|
||||
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
}
|
||||
} else {
|
||||
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
||||
$this->server->save();
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
@@ -387,31 +413,5 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
|
||||
// Check if proxy is running
|
||||
$this->server->proxyType();
|
||||
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
}
|
||||
} else {
|
||||
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
||||
$this->server->save();
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -66,7 +66,7 @@ class Show extends Component
|
||||
return ! $alreadyAddedNetworks->contains('network', $network['Name']);
|
||||
});
|
||||
if ($this->networks->count() === 0) {
|
||||
$this->dispatch('success', 'No new networks found.');
|
||||
$this->dispatch('success', 'No new destinations found on this server.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
@@ -241,7 +241,6 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function updatedApplicationBuildPack()
|
||||
{
|
||||
if ($this->application->build_pack !== 'nixpacks') {
|
||||
@@ -335,9 +334,15 @@ class General extends Component
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
$this->resetDefaultLabels();
|
||||
|
||||
if ($this->application->isDirty('redirect')) {
|
||||
@@ -403,17 +408,19 @@ class General extends Component
|
||||
}
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
$showToaster && $this->dispatch('success', 'Application settings updated!');
|
||||
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
|
||||
} catch (\Throwable $e) {
|
||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->dispatch('configurationChanged');
|
||||
}
|
||||
}
|
||||
|
||||
public function downloadConfig()
|
||||
{
|
||||
$config = GenerateConfig::run($this->application, true);
|
||||
|
@@ -11,12 +11,21 @@ use Livewire\Component;
|
||||
|
||||
class General extends Component
|
||||
{
|
||||
protected $listeners = ['refresh'];
|
||||
protected $listeners = [
|
||||
'envsUpdated' => 'refresh',
|
||||
'refresh',
|
||||
];
|
||||
|
||||
public Server $server;
|
||||
|
||||
public StandaloneRedis $database;
|
||||
|
||||
public string $redis_username;
|
||||
|
||||
public string $redis_password;
|
||||
|
||||
public string $redis_version;
|
||||
|
||||
public ?string $db_url = null;
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
@@ -25,33 +34,33 @@ class General extends Component
|
||||
'database.name' => 'required',
|
||||
'database.description' => 'nullable',
|
||||
'database.redis_conf' => 'nullable',
|
||||
'database.redis_password' => 'required',
|
||||
'database.image' => 'required',
|
||||
'database.ports_mappings' => 'nullable',
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'redis_username' => 'required',
|
||||
'redis_password' => 'required',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'database.name' => 'Name',
|
||||
'database.description' => 'Description',
|
||||
'database.redis_conf' => 'Redis Configuration',
|
||||
'database.redis_password' => 'Redis Password',
|
||||
'database.image' => 'Image',
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||
'redis_username' => 'Redis Username',
|
||||
'redis_password' => 'Redis Password',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
|
||||
$this->refreshView();
|
||||
}
|
||||
|
||||
public function instantSaveAdvanced()
|
||||
@@ -75,13 +84,24 @@ class General extends Component
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
if ($this->database->redis_conf === '') {
|
||||
$this->database->redis_conf = null;
|
||||
|
||||
if (version_compare($this->redis_version, '6.0', '>=')) {
|
||||
$this->database->runtime_environment_variables()->updateOrCreate(
|
||||
['key' => 'REDIS_USERNAME'],
|
||||
['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id]
|
||||
);
|
||||
}
|
||||
$this->database->runtime_environment_variables()->updateOrCreate(
|
||||
['key' => 'REDIS_PASSWORD'],
|
||||
['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id]
|
||||
);
|
||||
|
||||
$this->database->save();
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->dispatch('refreshEnvs');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,10 +139,25 @@ class General extends Component
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->refreshView();
|
||||
}
|
||||
|
||||
private function refreshView()
|
||||
{
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->redis_version = $this->database->getRedisVersion();
|
||||
$this->redis_username = $this->database->redis_username;
|
||||
$this->redis_password = $this->database->redis_password;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.redis.general');
|
||||
}
|
||||
|
||||
public function isSharedVariable($name)
|
||||
{
|
||||
return $this->database->runtime_environment_variables()->where('key', $name)->where('is_shared', true)->exists();
|
||||
}
|
||||
}
|
||||
|
@@ -7,18 +7,22 @@ use Livewire\Component;
|
||||
|
||||
class DeleteEnvironment extends Component
|
||||
{
|
||||
public array $parameters;
|
||||
|
||||
public int $environment_id;
|
||||
|
||||
public bool $disabled = false;
|
||||
|
||||
public string $environmentName = '';
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
|
||||
$this->parameters = get_route_parameters();
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
@@ -30,7 +34,7 @@ class DeleteEnvironment extends Component
|
||||
if ($environment->isEmpty()) {
|
||||
$environment->delete();
|
||||
|
||||
return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]);
|
||||
return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]);
|
||||
}
|
||||
|
||||
return $this->dispatch('error', 'Environment has defined resources, please delete them first.');
|
||||
|
@@ -18,7 +18,11 @@ class Index extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeam()->get()->map(function ($project) {
|
||||
$project->settingsRoute = route('project.edit', ['project_uuid' => $project->uuid]);
|
||||
|
||||
return $project;
|
||||
});
|
||||
$this->servers = Server::ownedByCurrentTeam()->count();
|
||||
}
|
||||
|
||||
|
@@ -317,6 +317,7 @@ class PublicGitRepository extends Component
|
||||
// $application->setConfig($config);
|
||||
// }
|
||||
}
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'environment_name' => $environment->name,
|
||||
|
@@ -32,8 +32,11 @@ class Index extends Component
|
||||
|
||||
public $services = [];
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (! $project) {
|
||||
return redirect()->route('dashboard');
|
||||
@@ -44,7 +47,6 @@ class Index extends Component
|
||||
}
|
||||
$this->project = $project;
|
||||
$this->environment = $environment;
|
||||
|
||||
$this->applications = $this->environment->applications->load(['tags']);
|
||||
$this->applications = $this->applications->map(function ($application) {
|
||||
if (data_get($application, 'environment.project.uuid')) {
|
||||
|
@@ -21,6 +21,7 @@ class EditDomain extends Component
|
||||
{
|
||||
$this->application = ServiceApplication::find($this->applicationId);
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
@@ -28,9 +29,14 @@ class EditDomain extends Component
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
check_domain_usage(resource: $this->application);
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
@@ -38,7 +44,7 @@ class EditDomain extends Component
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Service saved.');
|
||||
! $warning && $this->dispatch('success', 'Service saved.');
|
||||
}
|
||||
$this->application->service->parse();
|
||||
$this->dispatch('refresh');
|
||||
@@ -48,6 +54,7 @@ class EditDomain extends Component
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ class Navbar extends Component
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted',
|
||||
"envsUpdated" => '$refresh',
|
||||
'envsUpdated' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -30,11 +30,6 @@ class ServiceApplicationView extends Component
|
||||
'application.is_stripprefix_enabled' => 'nullable|boolean',
|
||||
];
|
||||
|
||||
public function updatedApplicationFqdn()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
$this->submit();
|
||||
@@ -82,10 +77,14 @@ class ServiceApplicationView extends Component
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
check_domain_usage(resource: $this->application);
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
@@ -93,7 +92,7 @@ class ServiceApplicationView extends Component
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Service saved.');
|
||||
! $warning && $this->dispatch('success', 'Service saved.');
|
||||
}
|
||||
$this->dispatch('generateDockerCompose');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -101,6 +100,7 @@ class ServiceApplicationView extends Component
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
@@ -31,13 +31,8 @@ class Metrics extends Component
|
||||
public function loadData()
|
||||
{
|
||||
try {
|
||||
$metrics = $this->resource->getMetrics($this->interval);
|
||||
$cpuMetrics = collect($metrics)->map(function ($metric) {
|
||||
return [$metric[0], $metric[1]];
|
||||
});
|
||||
$memoryMetrics = collect($metrics)->map(function ($metric) {
|
||||
return [$metric[0], $metric[2]];
|
||||
});
|
||||
$cpuMetrics = $this->resource->getCpuMetrics($this->interval);
|
||||
$memoryMetrics = $this->resource->getMemoryMetrics($this->interval);
|
||||
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [
|
||||
'seriesData' => $cpuMetrics,
|
||||
]);
|
||||
|
@@ -8,8 +8,11 @@ use Livewire\Component;
|
||||
class UploadConfig extends Component
|
||||
{
|
||||
public $config;
|
||||
|
||||
public $applicationId;
|
||||
public function mount() {
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (isDev()) {
|
||||
$this->config = '{
|
||||
"build_pack": "nixpacks",
|
||||
@@ -22,6 +25,7 @@ class UploadConfig extends Component
|
||||
}';
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadConfig()
|
||||
{
|
||||
try {
|
||||
@@ -30,10 +34,12 @@ class UploadConfig extends Component
|
||||
$this->dispatch('success', 'Application settings updated');
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.upload-config');
|
||||
|
77
app/Livewire/Server/Advanced.php
Normal file
77
app/Livewire/Server/Advanced.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Advanced extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
protected $rules = [
|
||||
'server.settings.concurrent_builds' => 'required|integer|min:1',
|
||||
'server.settings.dynamic_timeout' => 'required|integer|min:1',
|
||||
'server.settings.force_docker_cleanup' => 'required|boolean',
|
||||
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
|
||||
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
|
||||
'server.settings.delete_unused_volumes' => 'boolean',
|
||||
'server.settings.delete_unused_networks' => 'boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
|
||||
'server.settings.concurrent_builds' => 'Concurrent Builds',
|
||||
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
|
||||
'server.settings.force_docker_cleanup' => 'Force Docker Cleanup',
|
||||
'server.settings.docker_cleanup_frequency' => 'Docker Cleanup Frequency',
|
||||
'server.settings.docker_cleanup_threshold' => 'Docker Cleanup Threshold',
|
||||
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
|
||||
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
|
||||
];
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Throwable $e) {
|
||||
$this->server->settings->refresh();
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCleanup()
|
||||
{
|
||||
try {
|
||||
DockerCleanupJob::dispatch($this->server, true);
|
||||
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$frequency = $this->server->settings->docker_cleanup_frequency;
|
||||
if (empty($frequency) || ! validate_cron_expression($frequency)) {
|
||||
$this->server->settings->docker_cleanup_frequency = '*/10 * * * *';
|
||||
throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.');
|
||||
}
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.advanced');
|
||||
}
|
||||
}
|
@@ -34,12 +34,12 @@ class Charts extends Component
|
||||
try {
|
||||
$cpuMetrics = $this->server->getCpuMetrics($this->interval);
|
||||
$memoryMetrics = $this->server->getMemoryMetrics($this->interval);
|
||||
$cpuMetrics = collect($cpuMetrics)->map(function ($metric) {
|
||||
return [$metric[0], $metric[1]];
|
||||
});
|
||||
$memoryMetrics = collect($memoryMetrics)->map(function ($metric) {
|
||||
return [$metric[0], $metric[1]];
|
||||
});
|
||||
// $cpuMetrics = collect($cpuMetrics)->map(function ($metric) {
|
||||
// return [$metric[0], $metric[1]];
|
||||
// });
|
||||
// $memoryMetrics = collect($memoryMetrics)->map(function ($metric) {
|
||||
// return [$metric[0], $metric[1]];
|
||||
// });
|
||||
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [
|
||||
'seriesData' => $cpuMetrics,
|
||||
]);
|
||||
|
44
app/Livewire/Server/CloudflareTunnels.php
Normal file
44
app/Livewire/Server/CloudflareTunnels.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class CloudflareTunnels extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
protected $rules = [
|
||||
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
|
||||
];
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCloudflareConfig()
|
||||
{
|
||||
$this->server->settings->is_cloudflare_tunnel = true;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.cloudflare-tunnels');
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@@ -28,6 +29,7 @@ class Delete extends Component
|
||||
return;
|
||||
}
|
||||
$this->server->delete();
|
||||
DeleteServer::dispatch($this->server);
|
||||
|
||||
return redirect()->route('server.index');
|
||||
} catch (\Throwable $e) {
|
||||
|
@@ -4,10 +4,7 @@ namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\StartSentinel;
|
||||
use App\Actions\Server\StopSentinel;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Component;
|
||||
|
||||
class Form extends Component
|
||||
@@ -47,25 +44,19 @@ class Form extends Component
|
||||
'server.ip' => 'required',
|
||||
'server.user' => 'required',
|
||||
'server.port' => 'required',
|
||||
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
|
||||
'wildcard_domain' => 'nullable|url',
|
||||
'server.settings.is_reachable' => 'required',
|
||||
'server.settings.is_swarm_manager' => 'required|boolean',
|
||||
'server.settings.is_swarm_worker' => 'required|boolean',
|
||||
'server.settings.is_build_server' => 'required|boolean',
|
||||
'server.settings.concurrent_builds' => 'required|integer|min:1',
|
||||
'server.settings.dynamic_timeout' => 'required|integer|min:1',
|
||||
'server.settings.is_metrics_enabled' => 'required|boolean',
|
||||
'server.settings.sentinel_token' => 'required',
|
||||
'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1',
|
||||
'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1',
|
||||
'wildcard_domain' => 'nullable|url',
|
||||
'server.settings.is_server_api_enabled' => 'required|boolean',
|
||||
'server.settings.sentinel_push_interval_seconds' => 'required|integer|min:10',
|
||||
'server.settings.sentinel_custom_url' => 'nullable|url',
|
||||
'server.settings.is_sentinel_enabled' => 'required|boolean',
|
||||
'server.settings.server_timezone' => 'required|string|timezone',
|
||||
'server.settings.force_docker_cleanup' => 'required|boolean',
|
||||
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
|
||||
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
|
||||
'server.settings.delete_unused_volumes' => 'boolean',
|
||||
'server.settings.delete_unused_networks' => 'boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -74,21 +65,18 @@ class Form extends Component
|
||||
'server.ip' => 'IP address/Domain',
|
||||
'server.user' => 'User',
|
||||
'server.port' => 'Port',
|
||||
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
|
||||
'server.settings.is_reachable' => 'Is reachable',
|
||||
'server.settings.is_swarm_manager' => 'Swarm Manager',
|
||||
'server.settings.is_swarm_worker' => 'Swarm Worker',
|
||||
'server.settings.is_build_server' => 'Build Server',
|
||||
'server.settings.concurrent_builds' => 'Concurrent Builds',
|
||||
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
|
||||
'server.settings.is_metrics_enabled' => 'Metrics',
|
||||
'server.settings.sentinel_token' => 'Metrics Token',
|
||||
'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval',
|
||||
'server.settings.sentinel_metrics_history_days' => 'Metrics History',
|
||||
'server.settings.is_server_api_enabled' => 'Server API',
|
||||
'server.settings.sentinel_push_interval_seconds' => 'Push Interval',
|
||||
'server.settings.is_sentinel_enabled' => 'Server API',
|
||||
'server.settings.sentinel_custom_url' => 'Coolify URL',
|
||||
'server.settings.server_timezone' => 'Server Timezone',
|
||||
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
|
||||
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
|
||||
];
|
||||
|
||||
public function mount(Server $server)
|
||||
@@ -96,20 +84,26 @@ class Form extends Component
|
||||
$this->server = $server;
|
||||
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
|
||||
$this->wildcard_domain = $this->server->settings->wildcard_domain;
|
||||
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
|
||||
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
|
||||
$this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes;
|
||||
$this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
|
||||
}
|
||||
public function regenerateSentinelToken() {
|
||||
try {
|
||||
$this->server->generateSentinelToken();
|
||||
|
||||
public function checkSyncStatus()
|
||||
{
|
||||
$this->server->refresh();
|
||||
$this->server->settings->refresh();
|
||||
$this->dispatch('success', 'Metrics token regenerated.');
|
||||
}
|
||||
|
||||
public function regenerateSentinelToken()
|
||||
{
|
||||
try {
|
||||
$this->server->settings->generateSentinelToken();
|
||||
$this->server->settings->refresh();
|
||||
$this->restartSentinel(notification: false);
|
||||
$this->dispatch('success', 'Token regenerated & Sentinel restarted.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function updated($field)
|
||||
{
|
||||
if ($field === 'server.settings.docker_cleanup_frequency') {
|
||||
@@ -140,21 +134,35 @@ class Form extends Component
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
|
||||
public function checkPortForServerApi()
|
||||
public function updatedServerSettingsIsSentinelEnabled($value)
|
||||
{
|
||||
$this->validate();
|
||||
$this->validate([
|
||||
'server.settings.sentinel_custom_url' => 'required|url',
|
||||
]);
|
||||
if ($value === false) {
|
||||
StopSentinel::dispatch($this->server);
|
||||
$this->server->settings->is_metrics_enabled = false;
|
||||
$this->server->settings->save();
|
||||
$this->server->sentinelHeartbeat(isReset: true);
|
||||
} else {
|
||||
try {
|
||||
if ($this->server->settings->is_server_api_enabled === true) {
|
||||
$this->server->checkServerApi();
|
||||
$this->dispatch('success', 'Server API is reachable.');
|
||||
}
|
||||
StartSentinel::run($this->server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedServerSettingsIsMetricsEnabled()
|
||||
{
|
||||
$this->restartSentinel();
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
refresh_server_connection($this->server->privateKey);
|
||||
$this->validateServer(false);
|
||||
|
||||
@@ -162,55 +170,27 @@ class Form extends Component
|
||||
$this->server->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
if ($this->server->isSentinelEnabled()) {
|
||||
PullSentinelImageJob::dispatchSync($this->server);
|
||||
ray('Sentinel is enabled');
|
||||
if ($this->server->settings->isDirty('is_metrics_enabled')) {
|
||||
$this->dispatch('reloadWindow');
|
||||
}
|
||||
if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) {
|
||||
ray('Starting sentinel');
|
||||
}
|
||||
} else {
|
||||
ray('Sentinel is not enabled');
|
||||
StopSentinel::dispatch($this->server);
|
||||
}
|
||||
$this->server->settings->save();
|
||||
// $this->checkPortForServerApi();
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->server->settings->refresh();
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPushData()
|
||||
public function restartSentinel($notification = true)
|
||||
{
|
||||
try {
|
||||
if (!isDev()) {
|
||||
throw new \Exception('This feature is only available in dev mode.');
|
||||
}
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->server->settings->sentinel_token,
|
||||
])->post('http://host.docker.internal:8888/api/push', [
|
||||
'data' => 'test',
|
||||
$this->validate();
|
||||
$this->validate([
|
||||
'server.settings.sentinel_custom_url' => 'required|url',
|
||||
]);
|
||||
if ($response->successful()) {
|
||||
$this->dispatch('success', 'Push data sent.');
|
||||
return;
|
||||
}
|
||||
$error = data_get($response->json(), 'error');
|
||||
throw new \Exception($error);
|
||||
|
||||
} catch(\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function restartSentinel()
|
||||
{
|
||||
try {
|
||||
$version = get_latest_sentinel_version();
|
||||
StartSentinel::run($this->server, $version, true);
|
||||
$this->dispatch('success', 'Sentinel restarted.');
|
||||
if ($notification) {
|
||||
$this->dispatch('success', 'Sentinel started.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -267,11 +247,11 @@ class Form extends Component
|
||||
}
|
||||
refresh_server_connection($this->server->privateKey);
|
||||
$this->server->settings->wildcard_domain = $this->wildcard_domain;
|
||||
if ($this->server->settings->force_docker_cleanup) {
|
||||
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
|
||||
} else {
|
||||
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
|
||||
}
|
||||
// if ($this->server->settings->force_docker_cleanup) {
|
||||
// $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
|
||||
// } else {
|
||||
// $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
|
||||
// }
|
||||
$currentTimezone = $this->server->settings->getOriginal('server_timezone');
|
||||
$newTimezone = $this->server->settings->server_timezone;
|
||||
if ($currentTimezone !== $newTimezone || $currentTimezone === '') {
|
||||
@@ -285,21 +265,4 @@ class Form extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function manualCleanup()
|
||||
{
|
||||
try {
|
||||
DockerCleanupJob::dispatch($this->server, true);
|
||||
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCloudflareConfig()
|
||||
{
|
||||
$this->server->settings->is_cloudflare_tunnel = true;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server\Proxy;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Modal extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
}
|
@@ -22,10 +22,7 @@ class Show extends Component
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
|
||||
if (is_null($this->server)) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
@@ -15,7 +15,9 @@ class Resources extends Component
|
||||
|
||||
public $parameters = [];
|
||||
|
||||
public Collection $unmanagedContainers;
|
||||
public Collection $containers;
|
||||
|
||||
public $activeTab = 'managed';
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
@@ -50,14 +52,29 @@ class Resources extends Component
|
||||
public function refreshStatus()
|
||||
{
|
||||
$this->server->refresh();
|
||||
if ($this->activeTab === 'managed') {
|
||||
$this->loadManagedContainers();
|
||||
} else {
|
||||
$this->loadUnmanagedContainers();
|
||||
}
|
||||
$this->dispatch('success', 'Resource statuses refreshed.');
|
||||
}
|
||||
|
||||
public function loadManagedContainers()
|
||||
{
|
||||
try {
|
||||
$this->activeTab = 'managed';
|
||||
$this->containers = $this->server->refresh()->definedResources();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadUnmanagedContainers()
|
||||
{
|
||||
$this->activeTab = 'unmanaged';
|
||||
try {
|
||||
$this->unmanagedContainers = $this->server->loadUnmanagedContainers();
|
||||
$this->containers = $this->server->loadUnmanagedContainers();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -65,13 +82,14 @@ class Resources extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->unmanagedContainers = collect();
|
||||
$this->containers = collect();
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
|
||||
if (is_null($this->server)) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
$this->loadManagedContainers();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
@@ -10,20 +10,17 @@ class Show extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public ?Server $server = null;
|
||||
public Server $server;
|
||||
|
||||
public $parameters = [];
|
||||
public array $parameters;
|
||||
|
||||
protected $listeners = ['refreshServerShow'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
|
||||
if (is_null($this->server)) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
|
||||
$this->parameters = get_route_parameters();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -14,15 +13,29 @@ class ShowPrivateKey extends Component
|
||||
|
||||
public $parameters;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
}
|
||||
|
||||
public function setPrivateKey($privateKeyId)
|
||||
{
|
||||
$originalPrivateKeyId = $this->server->getOriginal('private_key_id');
|
||||
try {
|
||||
$privateKey = PrivateKey::findOrFail($privateKeyId);
|
||||
$this->server->update(['private_key_id' => $privateKey->id]);
|
||||
$this->server->refresh();
|
||||
$this->server->update(['private_key_id' => $privateKeyId]);
|
||||
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
|
||||
if ($uptime) {
|
||||
$this->dispatch('success', 'Private key updated successfully.');
|
||||
} else {
|
||||
throw new \Exception('Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->server->update(['private_key_id' => $originalPrivateKeyId]);
|
||||
$this->server->validateConnection();
|
||||
$this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
|
||||
} finally {
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->server->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,18 +46,15 @@ class ShowPrivateKey extends Component
|
||||
if ($uptime) {
|
||||
$this->dispatch('success', 'Server is reachable.');
|
||||
} else {
|
||||
ray($error);
|
||||
$this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->server->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
}
|
||||
}
|
||||
|
@@ -25,10 +25,13 @@ class Index extends Component
|
||||
|
||||
public string $update_check_frequency;
|
||||
|
||||
public $timezones;
|
||||
|
||||
public bool $disable_two_step_confirmation;
|
||||
|
||||
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
|
||||
|
||||
protected Server $server;
|
||||
public $timezones;
|
||||
|
||||
protected $rules = [
|
||||
'settings.fqdn' => 'nullable',
|
||||
@@ -39,6 +42,8 @@ class Index extends Component
|
||||
'settings.instance_name' => 'nullable',
|
||||
'settings.allowed_ips' => 'nullable',
|
||||
'settings.is_auto_update_enabled' => 'boolean',
|
||||
'settings.public_ipv4' => 'nullable',
|
||||
'settings.public_ipv6' => 'nullable',
|
||||
'auto_update_frequency' => 'string',
|
||||
'update_check_frequency' => 'string',
|
||||
'settings.instance_timezone' => 'required|string|timezone',
|
||||
@@ -52,16 +57,18 @@ class Index extends Component
|
||||
'settings.custom_dns_servers' => 'Custom DNS servers',
|
||||
'settings.allowed_ips' => 'Allowed IPs',
|
||||
'settings.is_auto_update_enabled' => 'Auto Update Enabled',
|
||||
'settings.public_ipv4' => 'IPv4',
|
||||
'settings.public_ipv6' => 'IPv6',
|
||||
'auto_update_frequency' => 'Auto Update Frequency',
|
||||
'update_check_frequency' => 'Update Check Frequency',
|
||||
'settings.instance_timezone' => 'Instance Timezone',
|
||||
];
|
||||
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (isInstanceAdmin()) {
|
||||
$this->settings = instanceSettings();
|
||||
loggy($this->settings);
|
||||
$this->do_not_track = $this->settings->do_not_track;
|
||||
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
|
||||
$this->is_registration_enabled = $this->settings->is_registration_enabled;
|
||||
@@ -70,6 +77,7 @@ class Index extends Component
|
||||
$this->auto_update_frequency = $this->settings->auto_update_frequency;
|
||||
$this->update_check_frequency = $this->settings->update_check_frequency;
|
||||
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
|
||||
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
|
||||
} else {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
@@ -84,6 +92,7 @@ class Index extends Component
|
||||
$this->settings->is_api_enabled = $this->is_api_enabled;
|
||||
$this->settings->auto_update_frequency = $this->auto_update_frequency;
|
||||
$this->settings->update_check_frequency = $this->update_check_frequency;
|
||||
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
|
||||
$this->settings->save();
|
||||
$this->dispatch('success', 'Settings updated!');
|
||||
}
|
||||
@@ -171,9 +180,16 @@ class Index extends Component
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.index');
|
||||
}
|
||||
|
||||
public function toggleTwoStepConfirmation()
|
||||
{
|
||||
$this->settings->disable_two_step_confirmation = true;
|
||||
$this->settings->save();
|
||||
$this->disable_two_step_confirmation = true;
|
||||
$this->dispatch('success', 'Two step confirmation has been disabled.');
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ class Create extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->name = generate_random_name();
|
||||
$this->name = substr(generate_random_name(), 0, 34); // GitHub Apps names can only be 34 characters long
|
||||
}
|
||||
|
||||
public function createGitHubApp()
|
||||
|
@@ -1400,13 +1400,21 @@ class Application extends BaseModel
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getMetrics(int $mins = 5)
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
if ($server->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
|
||||
if (isDev() && $server->id === 0) {
|
||||
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/cpu/history?from=$from");
|
||||
if ($process->failed()) {
|
||||
throw new \Exception($process->errorOutput());
|
||||
}
|
||||
$metrics = $process->output();
|
||||
} else {
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
}
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
@@ -1415,14 +1423,41 @@ class Application extends BaseModel
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = str($metrics)->explode("\n")->skip(1)->all();
|
||||
$parsedCollection = collect($metrics)->flatMap(function ($item) {
|
||||
return collect(explode("\n", trim($item)))->map(function ($line) {
|
||||
[$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
|
||||
$cpu_usage_percent = number_format($cpu_usage_percent, 2);
|
||||
|
||||
return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
if ($server->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
if (isDev() && $server->id === 0) {
|
||||
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/memory/history?from=$from");
|
||||
if ($process->failed()) {
|
||||
throw new \Exception($process->errorOutput());
|
||||
}
|
||||
$metrics = $process->output();
|
||||
} else {
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
}
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error == 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
@@ -1459,7 +1494,9 @@ class Application extends BaseModel
|
||||
|
||||
return $config;
|
||||
}
|
||||
public function setConfig($config) {
|
||||
|
||||
public function setConfig($config)
|
||||
{
|
||||
|
||||
$config = $config;
|
||||
$validator = Validator::make(['config' => $config], [
|
||||
|
@@ -74,6 +74,9 @@ class EnvironmentVariable extends Model
|
||||
'version' => config('version'),
|
||||
]);
|
||||
});
|
||||
static::saving(function (EnvironmentVariable $environmentVariable) {
|
||||
$environmentVariable->updateIsShared();
|
||||
});
|
||||
}
|
||||
|
||||
public function service()
|
||||
@@ -217,4 +220,11 @@ class EnvironmentVariable extends Model
|
||||
set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value,
|
||||
);
|
||||
}
|
||||
|
||||
protected function updateIsShared(): void
|
||||
{
|
||||
$type = str($this->value)->after('{{')->before('.')->value;
|
||||
$isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}');
|
||||
$this->is_shared = $isShared;
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -24,6 +25,20 @@ class InstanceSettings extends Model implements SendsEmail
|
||||
'sentinel_token' => 'encrypted',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::updated(function ($settings) {
|
||||
if ($settings->isDirty('helper_version')) {
|
||||
Server::chunkById(100, function ($servers) {
|
||||
foreach ($servers as $server) {
|
||||
PullHelperImageJob::dispatch($server);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public function fqdn(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -86,17 +101,4 @@ class InstanceSettings extends Model implements SendsEmail
|
||||
|
||||
return "[{$instanceName}]";
|
||||
}
|
||||
|
||||
public function helperVersion(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function ($value) {
|
||||
if (isDev()) {
|
||||
return 'latest';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -51,7 +51,6 @@ class ScheduledDatabaseBackup extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,8 @@ use App\Enums\ProxyTypes;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
@@ -43,7 +45,7 @@ use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Server extends BaseModel
|
||||
{
|
||||
use SchemalessAttributesTrait;
|
||||
use SchemalessAttributesTrait,SoftDeletes;
|
||||
|
||||
public static $batch_counter = 0;
|
||||
|
||||
@@ -103,7 +105,8 @@ class Server extends BaseModel
|
||||
$server->proxy->redirect_enabled = true;
|
||||
}
|
||||
});
|
||||
static::deleting(function ($server) {
|
||||
|
||||
static::forceDeleting(function ($server) {
|
||||
$server->destinations()->each(function ($destination) {
|
||||
$destination->delete();
|
||||
});
|
||||
@@ -520,22 +523,20 @@ $schema://$host {
|
||||
Storage::disk('ssh-mux')->delete($this->muxFilename());
|
||||
}
|
||||
|
||||
public function generateSentinelToken()
|
||||
public function sentinelHeartbeat(bool $isReset = false)
|
||||
{
|
||||
$data = [
|
||||
'server_uuid' => $this->uuid,
|
||||
];
|
||||
$token = json_encode($data);
|
||||
$encrypted = encrypt($token);
|
||||
$this->settings->sentinel_token = $encrypted;
|
||||
$this->settings->save();
|
||||
$this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $encrypted;
|
||||
public function isSentinelLive()
|
||||
{
|
||||
return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subMinutes(4));
|
||||
}
|
||||
|
||||
public function isSentinelEnabled()
|
||||
{
|
||||
return $this->isMetricsEnabled() || $this->isServerApiEnabled();
|
||||
return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
|
||||
}
|
||||
|
||||
public function isMetricsEnabled()
|
||||
@@ -545,7 +546,7 @@ $schema://$host {
|
||||
|
||||
public function isServerApiEnabled()
|
||||
{
|
||||
return $this->settings->is_server_api_enabled;
|
||||
return $this->settings->is_sentinel_enabled;
|
||||
}
|
||||
|
||||
public function checkServerApi()
|
||||
@@ -586,7 +587,15 @@ $schema://$host {
|
||||
{
|
||||
if ($this->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
if (isDev() && $this->id === 0) {
|
||||
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/cpu/history?from=$from");
|
||||
if ($process->failed()) {
|
||||
throw new \Exception($process->errorOutput());
|
||||
}
|
||||
$cpu = $process->output();
|
||||
} else {
|
||||
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
|
||||
}
|
||||
if (str($cpu)->contains('error')) {
|
||||
$error = json_decode($cpu, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
@@ -595,17 +604,13 @@ $schema://$host {
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$cpu = str($cpu)->explode("\n")->skip(1)->all();
|
||||
$parsedCollection = collect($cpu)->flatMap(function ($item) {
|
||||
return collect(explode("\n", trim($item)))->map(function ($line) {
|
||||
[$time, $cpu_usage_percent] = explode(',', trim($line));
|
||||
$cpu_usage_percent = number_format($cpu_usage_percent, 0);
|
||||
|
||||
return [(int) $time, (float) $cpu_usage_percent];
|
||||
});
|
||||
$cpu = json_decode($cpu, true);
|
||||
$parsedCollection = collect($cpu)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
return $parsedCollection;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,7 +618,15 @@ $schema://$host {
|
||||
{
|
||||
if ($this->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
if (isDev() && $this->id === 0) {
|
||||
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/memory/history?from=$from");
|
||||
if ($process->failed()) {
|
||||
throw new \Exception($process->errorOutput());
|
||||
}
|
||||
$memory = $process->output();
|
||||
} else {
|
||||
$memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
|
||||
}
|
||||
if (str($memory)->contains('error')) {
|
||||
$error = json_decode($memory, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
@@ -622,14 +635,9 @@ $schema://$host {
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$memory = str($memory)->explode("\n")->skip(1)->all();
|
||||
$parsedCollection = collect($memory)->flatMap(function ($item) {
|
||||
return collect(explode("\n", trim($item)))->map(function ($line) {
|
||||
[$time, $used, $free, $usedPercent] = explode(',', trim($line));
|
||||
$usedPercent = number_format($usedPercent, 0);
|
||||
|
||||
return [(int) $time, (float) $usedPercent];
|
||||
});
|
||||
$memory = json_decode($memory, true);
|
||||
$parsedCollection = collect($memory)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['usedPercent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
@@ -1049,6 +1057,38 @@ $schema://$host {
|
||||
return data_get($this, 'settings.is_swarm_worker');
|
||||
}
|
||||
|
||||
public function status(): bool
|
||||
{
|
||||
['uptime' => $uptime] = $this->validateConnection(false);
|
||||
if ($uptime) {
|
||||
if ($this->unreachable_notification_sent === true) {
|
||||
$this->update(['unreachable_notification_sent' => false]);
|
||||
}
|
||||
} else {
|
||||
// $this->server->team?->notify(new Unreachable($this->server));
|
||||
foreach ($this->applications as $application) {
|
||||
$application->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($this->databases as $database) {
|
||||
$database->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($this->services as $service) {
|
||||
$apps = $service->applications()->get();
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($apps as $app) {
|
||||
$app->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($dbs as $db) {
|
||||
$db->update(['status' => 'exited']);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validateConnection($isManualCheck = true)
|
||||
{
|
||||
config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
|
||||
|
@@ -24,7 +24,7 @@ use OpenApi\Attributes as OA;
|
||||
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
|
||||
'is_metrics_enabled' => ['type' => 'boolean'],
|
||||
'is_reachable' => ['type' => 'boolean'],
|
||||
'is_server_api_enabled' => ['type' => 'boolean'],
|
||||
'is_sentinel_enabled' => ['type' => 'boolean'],
|
||||
'is_swarm_manager' => ['type' => 'boolean'],
|
||||
'is_swarm_worker' => ['type' => 'boolean'],
|
||||
'is_usable' => ['type' => 'boolean'],
|
||||
@@ -56,6 +56,63 @@ class ServerSetting extends Model
|
||||
'sentinel_token' => 'encrypted',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($setting) {
|
||||
try {
|
||||
if (str($setting->sentinel_token)->isEmpty()) {
|
||||
$setting->generateSentinelToken(save: false);
|
||||
}
|
||||
if (str($setting->sentinel_custom_url)->isEmpty()) {
|
||||
$url = $setting->generateSentinelUrl(save: false);
|
||||
if (str($url)->isEmpty()) {
|
||||
$setting->is_sentinel_enabled = false;
|
||||
} else {
|
||||
$setting->is_sentinel_enabled = true;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
loggy('Error creating server setting: '.$e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function generateSentinelToken(bool $save = true)
|
||||
{
|
||||
$data = [
|
||||
'server_uuid' => $this->server->uuid,
|
||||
];
|
||||
$token = json_encode($data);
|
||||
$encrypted = encrypt($token);
|
||||
$this->sentinel_token = $encrypted;
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $encrypted;
|
||||
}
|
||||
|
||||
public function generateSentinelUrl(bool $save = true)
|
||||
{
|
||||
$domain = null;
|
||||
$settings = InstanceSettings::get();
|
||||
if ($this->server->isLocalhost()) {
|
||||
$domain = 'http://host.docker.internal:8000';
|
||||
} elseif ($settings->fqdn) {
|
||||
$domain = $settings->fqdn;
|
||||
} elseif ($settings->ipv4) {
|
||||
$domain = $settings->ipv4.':8000';
|
||||
} elseif ($settings->ipv6) {
|
||||
$domain = $settings->ipv6.':8000';
|
||||
}
|
||||
$this->sentinel_custom_url = $domain;
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
|
@@ -297,7 +297,7 @@ class Service extends BaseModel
|
||||
'key' => 'CP_DISABLE_HTTPS',
|
||||
'value' => data_get($disable_https, 'value'),
|
||||
'rules' => 'required',
|
||||
'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS",
|
||||
'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS',
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -1326,9 +1326,9 @@ class Service extends BaseModel
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -210,7 +210,12 @@ class StandaloneRedis extends BaseModel
|
||||
protected function internalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0",
|
||||
get: function () {
|
||||
$redis_version = $this->getRedisVersion();
|
||||
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
|
||||
|
||||
return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -219,7 +224,10 @@ class StandaloneRedis extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
$redis_version = $this->getRedisVersion();
|
||||
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
|
||||
|
||||
return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -227,6 +235,13 @@ class StandaloneRedis extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function getRedisVersion()
|
||||
{
|
||||
$image_parts = explode(':', $this->image);
|
||||
|
||||
return $image_parts[1] ?? '0.0';
|
||||
}
|
||||
|
||||
public function environment()
|
||||
{
|
||||
return $this->belongsTo(Environment::class);
|
||||
@@ -295,4 +310,33 @@ class StandaloneRedis extends BaseModel
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function redisPassword(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
$password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first();
|
||||
if (! $password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $password->value;
|
||||
},
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
public function redisUsername(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
$username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
|
||||
if (! $username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $username->value;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDocker;
|
||||
@@ -48,7 +49,7 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
|
||||
}
|
||||
$database = new StandaloneRedis;
|
||||
$database->name = generate_database_name('redis');
|
||||
$database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||
$redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||
$database->environment_id = $environment_id;
|
||||
$database->destination_id = $destination->id;
|
||||
$database->destination_type = $destination->getMorphClass();
|
||||
@@ -57,6 +58,20 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
|
||||
}
|
||||
$database->save();
|
||||
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'REDIS_PASSWORD',
|
||||
'value' => $redis_password,
|
||||
'standalone_redis_id' => $database->id,
|
||||
'is_shared' => false,
|
||||
]);
|
||||
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'REDIS_USERNAME',
|
||||
'value' => 'default',
|
||||
'standalone_redis_id' => $database->id,
|
||||
'is_shared' => false,
|
||||
]);
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
|
@@ -335,6 +335,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
||||
if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) {
|
||||
return explode(',', $matches[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
})->flatten()
|
||||
->filter()
|
||||
|
@@ -241,9 +241,11 @@ function generate_default_proxy_configuration(Server $server)
|
||||
'ports' => [
|
||||
'80:80',
|
||||
'443:443',
|
||||
'443:443/udp',
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed=true',
|
||||
'coolify.proxy=true',
|
||||
],
|
||||
'volumes' => [
|
||||
'/var/run/docker.sock:/var/run/docker.sock:ro',
|
||||
|
@@ -126,7 +126,7 @@ function refreshSession(?Team $team = null): void
|
||||
}
|
||||
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
|
||||
{
|
||||
ray($error);
|
||||
loggy($error);
|
||||
if ($error instanceof TooManyRequestsException) {
|
||||
if (isset($livewire)) {
|
||||
return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
|
||||
@@ -142,6 +142,10 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
|
||||
return 'Duplicate entry found. Please use a different name.';
|
||||
}
|
||||
|
||||
if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if ($error instanceof Throwable) {
|
||||
$message = $error->getMessage();
|
||||
} else {
|
||||
@@ -164,10 +168,10 @@ function get_route_parameters(): array
|
||||
function get_latest_sentinel_version(): string
|
||||
{
|
||||
try {
|
||||
$response = Http::get('https://cdn.coollabs.io/sentinel/versions.json');
|
||||
$response = Http::get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
$versions = $response->json();
|
||||
|
||||
return data_get($versions, 'sentinel.version');
|
||||
return data_get($versions, 'coolify.sentinel.version');
|
||||
} catch (\Throwable $e) {
|
||||
//throw $e;
|
||||
ray($e->getMessage());
|
||||
@@ -3785,7 +3789,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
||||
service_name: $serviceName,
|
||||
image: $image,
|
||||
predefinedPort: $predefinedPort
|
||||
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -3983,13 +3986,14 @@ function instanceSettings()
|
||||
return InstanceSettings::get();
|
||||
}
|
||||
|
||||
function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) {
|
||||
function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
|
||||
{
|
||||
|
||||
$server = Server::find($server_id)->where('team_id', $team_id)->first();
|
||||
if (! $server) {
|
||||
return;
|
||||
}
|
||||
$uuid = new Cuid2();
|
||||
$uuid = new Cuid2;
|
||||
$cloneCommand = "git clone --no-checkout -b $branch $repository .";
|
||||
$workdir = rtrim($base_directory, '/');
|
||||
$fileList = collect([".$workdir/coolify.json"]);
|
||||
@@ -4010,3 +4014,30 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire
|
||||
// continue
|
||||
}
|
||||
}
|
||||
|
||||
function loggy($message = null, array $context = [])
|
||||
{
|
||||
if (! isDev()) {
|
||||
return;
|
||||
}
|
||||
if (function_exists('ray') && config('app.debug')) {
|
||||
ray($message, $context);
|
||||
}
|
||||
if (is_null($message)) {
|
||||
return app('log');
|
||||
}
|
||||
|
||||
return app('log')->debug($message, $context);
|
||||
}
|
||||
function sslipDomainWarning(string $domains)
|
||||
{
|
||||
$domains = str($domains)->trim()->explode(',');
|
||||
$showSslipHttpsWarning = false;
|
||||
$domains->each(function ($domain) use (&$showSslipHttpsWarning) {
|
||||
if (str($domain)->contains('https') && str($domain)->contains('sslip')) {
|
||||
$showSslipHttpsWarning = true;
|
||||
}
|
||||
});
|
||||
|
||||
return $showSslipHttpsWarning;
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@
|
||||
"laravel/fortify": "^v1.16.0",
|
||||
"laravel/framework": "^v11",
|
||||
"laravel/horizon": "^5.29.1",
|
||||
"laravel/pail": "^1.1",
|
||||
"laravel/prompts": "^0.1.6",
|
||||
"laravel/sanctum": "^v4.0",
|
||||
"laravel/socialite": "^v5.14.0",
|
||||
|
79
composer.lock
generated
79
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c47adf3684eb727e22503937435c0914",
|
||||
"content-hash": "943975ec232403b96a40d215253492d8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "amphp/amp",
|
||||
@@ -3144,6 +3144,83 @@
|
||||
},
|
||||
"time": "2024-10-08T18:23:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pail",
|
||||
"version": "v1.1.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/pail.git",
|
||||
"reference": "b33ad8321416fe86efed7bf398f3306c47b4871b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/pail/zipball/b33ad8321416fe86efed7bf398f3306c47b4871b",
|
||||
"reference": "b33ad8321416fe86efed7bf398f3306c47b4871b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"illuminate/console": "^10.24|^11.0",
|
||||
"illuminate/contracts": "^10.24|^11.0",
|
||||
"illuminate/log": "^10.24|^11.0",
|
||||
"illuminate/process": "^10.24|^11.0",
|
||||
"illuminate/support": "^10.24|^11.0",
|
||||
"nunomaduro/termwind": "^1.15|^2.0",
|
||||
"php": "^8.2",
|
||||
"symfony/console": "^6.0|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.13",
|
||||
"orchestra/testbench": "^8.12|^9.0",
|
||||
"pestphp/pest": "^2.20",
|
||||
"pestphp/pest-plugin-type-coverage": "^2.3",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"symfony/var-dumper": "^6.3|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.x-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Pail\\PailServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Pail\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
},
|
||||
{
|
||||
"name": "Nuno Maduro",
|
||||
"email": "enunomaduro@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Easily delve into your Laravel application's log files directly from the command line.",
|
||||
"homepage": "https://github.com/laravel/pail",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"logs",
|
||||
"php",
|
||||
"tail"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/pail/issues",
|
||||
"source": "https://github.com/laravel/pail"
|
||||
},
|
||||
"time": "2024-10-15T20:06:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
"version": "v0.1.25",
|
||||
|
6
config/testing.php
Normal file
6
config/testing.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'dusk_test_email' => env('DUSK_TEST_EMAIL', 'test@example.com'),
|
||||
'dusk_test_password' => env('DUSK_TEST_PASSWORD', 'password'),
|
||||
];
|
@@ -4,6 +4,7 @@ use App\Models\EnvironmentVariable;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -14,6 +15,7 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
try {
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->dropColumn('docker_compose_pr_location');
|
||||
$table->dropColumn('docker_compose_pr');
|
||||
@@ -47,11 +49,11 @@ return new class extends Migration
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->integer('metrics_history_days')->default(7)->change();
|
||||
});
|
||||
Server::all()->each(function (Server $server) {
|
||||
$server->settings->update([
|
||||
'metrics_history_days' => 7,
|
||||
]);
|
||||
});
|
||||
|
||||
DB::table('server_settings')->update(['metrics_history_days' => 7]);
|
||||
} catch (\Exception $e) {
|
||||
loggy($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -12,7 +12,7 @@ return new class extends Migration
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_force_cleanup_enabled')->default(false)->after('is_sentinel_enabled');
|
||||
$table->boolean('is_force_cleanup_enabled')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -15,12 +15,17 @@ return new class extends Migration
|
||||
$table->dropColumn('metrics_token');
|
||||
$table->dropColumn('metrics_refresh_rate_seconds');
|
||||
$table->dropColumn('metrics_history_days');
|
||||
$table->dropColumn('is_server_api_enabled');
|
||||
|
||||
$table->boolean('is_sentinel_enabled')->default(false);
|
||||
$table->text('sentinel_token')->nullable();
|
||||
$table->integer('sentinel_metrics_refresh_rate_seconds')->default(5);
|
||||
$table->integer('sentinel_metrics_history_days')->default(30);
|
||||
$table->integer('sentinel_metrics_refresh_rate_seconds')->default(10);
|
||||
$table->integer('sentinel_metrics_history_days')->default(7);
|
||||
$table->integer('sentinel_push_interval_seconds')->default(60);
|
||||
$table->string('sentinel_custom_url')->nullable();
|
||||
});
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dateTime('sentinel_update_at')->default(now());
|
||||
$table->dateTime('sentinel_updated_at')->default(now());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,12 +38,17 @@ return new class extends Migration
|
||||
$table->string('metrics_token')->nullable();
|
||||
$table->integer('metrics_refresh_rate_seconds')->default(5);
|
||||
$table->integer('metrics_history_days')->default(30);
|
||||
$table->boolean('is_server_api_enabled')->default(false);
|
||||
|
||||
$table->dropColumn('is_sentinel_enabled');
|
||||
$table->dropColumn('sentinel_token');
|
||||
$table->dropColumn('sentinel_metrics_refresh_rate_seconds');
|
||||
$table->dropColumn('sentinel_metrics_history_days');
|
||||
$table->dropColumn('sentinel_push_interval_seconds');
|
||||
$table->dropColumn('sentinel_custom_url');
|
||||
});
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('sentinel_update_at');
|
||||
$table->dropColumn('sentinel_updated_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddIsSharedToEnvironmentVariables extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('environment_variables', function (Blueprint $table) {
|
||||
$table->boolean('is_shared')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('environment_variables', function (Blueprint $table) {
|
||||
$table->dropColumn('is_shared');
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class MoveRedisPasswordToEnvs extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
try {
|
||||
StandaloneRedis::chunkById(100, function ($redisInstances) {
|
||||
foreach ($redisInstances as $redis) {
|
||||
$redis_password = DB::table('standalone_redis')->where('id', $redis->id)->value('redis_password');
|
||||
EnvironmentVariable::create([
|
||||
'standalone_redis_id' => $redis->id,
|
||||
'key' => 'REDIS_PASSWORD',
|
||||
'value' => $redis_password,
|
||||
]);
|
||||
EnvironmentVariable::create([
|
||||
'standalone_redis_id' => $redis->id,
|
||||
'key' => 'REDIS_USERNAME',
|
||||
'value' => 'default',
|
||||
]);
|
||||
}
|
||||
});
|
||||
Schema::table('standalone_redis', function (Blueprint $table) {
|
||||
$table->dropColumn('redis_password');
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
echo 'Moving Redis passwords to envs failed.';
|
||||
echo $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->boolean('disable_two_step_confirmation')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('disable_two_step_confirmation');
|
||||
});
|
||||
}
|
||||
};
|
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropSoftDeletes();
|
||||
});
|
||||
}
|
||||
};
|
@@ -26,6 +26,8 @@ class DatabaseSeeder extends Seeder
|
||||
S3StorageSeeder::class,
|
||||
StandalonePostgresqlSeeder::class,
|
||||
OauthSettingSeeder::class,
|
||||
DisableTwoStepConfirmationSeeder::class,
|
||||
SentinelSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
20
database/seeders/DisableTwoStepConfirmationSeeder.php
Normal file
20
database/seeders/DisableTwoStepConfirmationSeeder.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DisableTwoStepConfirmationSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
DB::table('instance_settings')->updateOrInsert(
|
||||
[],
|
||||
['disable_two_step_confirmation' => true]
|
||||
);
|
||||
}
|
||||
}
|
@@ -186,6 +186,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
|
||||
$this->call(OauthSettingSeeder::class);
|
||||
$this->call(PopulateSshKeysDirectorySeeder::class);
|
||||
$this->call(SentinelSeeder::class);
|
||||
|
||||
}
|
||||
}
|
||||
|
31
database/seeders/SentinelSeeder.php
Normal file
31
database/seeders/SentinelSeeder.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class SentinelSeeder extends Seeder
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
Server::chunk(100, function ($servers) {
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
if (str($server->settings->sentinel_token)->isEmpty()) {
|
||||
$server->settings->generateSentinelToken();
|
||||
}
|
||||
if (str($server->settings->sentinel_custom_url)->isEmpty()) {
|
||||
$url = $server->settings->generateSentinelUrl();
|
||||
if (str($url)->isEmpty()) {
|
||||
$server->settings->is_sentinel_enabled = false;
|
||||
$server->settings->save();
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
loggy("Error: {$e->getMessage()}\n");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -5,34 +5,38 @@ ARG TARGETPLATFORM
|
||||
ARG CLOUDFLARED_VERSION=2024.4.1
|
||||
|
||||
ARG POSTGRES_VERSION=15
|
||||
RUN apt-get update
|
||||
# Postgres version requirements
|
||||
RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y
|
||||
RUN curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null
|
||||
|
||||
RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list
|
||||
# Use build arguments for caching
|
||||
ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl"
|
||||
ARG RUNTIME_DEPS="postgresql-client-$POSTGRES_VERSION php8.2-pgsql openssh-client git git-lfs jq lsof"
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install postgresql-client-$POSTGRES_VERSION -y
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/apt \
|
||||
apt-get update && \
|
||||
apt-get install -y $BUILDTIME_DEPS && \
|
||||
curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null && \
|
||||
echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y $RUNTIME_DEPS && \
|
||||
apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
|
||||
|
||||
# Coolify requirements
|
||||
RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof
|
||||
RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
|
||||
COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/
|
||||
|
||||
COPY docker/dev/nginx.conf /etc/nginx/conf.d/custom.conf
|
||||
|
||||
RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc
|
||||
RUN echo "alias a='php artisan'" >>/etc/bash.bashrc
|
||||
RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc && \
|
||||
echo "alias a='php artisan'" >>/etc/bash.bashrc
|
||||
|
||||
RUN mkdir -p /usr/local/bin
|
||||
|
||||
RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
/bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
|
||||
echo 'amd64' && \
|
||||
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
|
||||
;fi"
|
||||
|
||||
RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
/bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
|
||||
echo 'arm64' && \
|
||||
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
|
||||
;fi"
|
||||
|
@@ -33,5 +33,6 @@
|
||||
"resource.delete_volumes": "Permanently delete all volumes associated with this resource.",
|
||||
"resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.",
|
||||
"resource.delete_configurations": "Permanently delete all configuration files from the server.",
|
||||
"database.delete_backups_locally": "All backups will be permanently deleted from local storage."
|
||||
"database.delete_backups_locally": "All backups will be permanently deleted from local storage.",
|
||||
"warning.sslipdomain": "Your configuration is saved, but sslip domain with https is <span class='dark:text-red-500 text-red-500 font-bold'>NOT</span> recommended, because Let's Encrypt servers with this public domain are rate limited (SSL certificate validation will fail). <br><br>Use your own domain instead."
|
||||
}
|
||||
|
20
openapi.yaml
20
openapi.yaml
@@ -98,6 +98,10 @@ paths:
|
||||
is_static:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the application is static.'
|
||||
static_image:
|
||||
type: string
|
||||
enum: ['nginx:alpine']
|
||||
description: 'The static image.'
|
||||
install_command:
|
||||
type: string
|
||||
description: 'The install command.'
|
||||
@@ -323,6 +327,10 @@ paths:
|
||||
is_static:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the application is static.'
|
||||
static_image:
|
||||
type: string
|
||||
enum: ['nginx:alpine']
|
||||
description: 'The static image.'
|
||||
install_command:
|
||||
type: string
|
||||
description: 'The install command.'
|
||||
@@ -548,6 +556,10 @@ paths:
|
||||
is_static:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the application is static.'
|
||||
static_image:
|
||||
type: string
|
||||
enum: ['nginx:alpine']
|
||||
description: 'The static image.'
|
||||
install_command:
|
||||
type: string
|
||||
description: 'The install command.'
|
||||
@@ -3093,7 +3105,7 @@ paths:
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/healthcheck:
|
||||
/health:
|
||||
get:
|
||||
summary: Healthcheck
|
||||
description: 'Healthcheck endpoint.'
|
||||
@@ -4959,7 +4971,7 @@ components:
|
||||
type: boolean
|
||||
is_reachable:
|
||||
type: boolean
|
||||
is_server_api_enabled:
|
||||
is_sentinel_enabled:
|
||||
type: boolean
|
||||
is_swarm_manager:
|
||||
type: boolean
|
||||
@@ -4981,10 +4993,10 @@ components:
|
||||
type: string
|
||||
logdrain_newrelic_license_key:
|
||||
type: string
|
||||
sentinel_metrics_refresh_rate_seconds:
|
||||
type: integer
|
||||
sentinel_metrics_history_days:
|
||||
type: integer
|
||||
sentinel_metrics_refresh_rate_seconds:
|
||||
type: integer
|
||||
sentinel_token:
|
||||
type: string
|
||||
docker_cleanup_frequency:
|
||||
|
3
public/svgs/edgedb.svg
Normal file
3
public/svgs/edgedb.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="176" height="80" viewBox="0 0 176 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M149.073 39.613C149.073 51.7589 144.203 53.9359 138.76 53.9359H127.187V25.2902H138.76C144.203 25.2902 149.073 27.4672 149.073 39.613V39.613ZM143.172 39.6143C143.172 31.1352 140.594 30.7342 136.87 30.7342H133.261V48.4945H136.87C140.594 48.4945 143.172 48.0935 143.172 39.6143V39.6143ZM81.8689 53.9359V25.2902H100.088V30.7329H87.9418V36.5766H97.1084V41.962H87.9418V48.4932H100.088V53.9359H81.8689ZM110 79.9998H116V0H110V79.9998ZM161.217 41.2193V48.4953H166.259C169.41 48.4953 170.212 46.4328 170.212 44.8859C170.212 43.6828 169.639 41.2193 165.342 41.2193H161.217ZM161.217 30.7342V36.1768H165.342C167.691 36.1768 169.066 35.1456 169.066 33.4269C169.066 31.7081 167.691 30.7342 165.342 30.7342H161.217ZM155.146 25.2902H166.833C172.964 25.2902 174.797 29.587 174.797 32.6808C174.797 35.5454 172.964 37.6078 171.703 38.1808C175.37 39.9568 176 43.5662 176 45.3995C176 47.8057 174.797 53.9359 166.833 53.9359H155.146V25.2902ZM46.9799 39.613C46.9799 51.7589 42.1101 53.9359 36.6674 53.9359H25.0945V25.2902H36.6674C42.1101 25.2902 46.9799 27.4672 46.9799 39.613V39.613ZM64.8538 48.725C67.9475 48.725 69.5517 47.6937 70.1246 47.0062V43.8552H65.1975V38.9281H74.9944V50.3291C74.135 51.6468 69.4371 54.2249 65.1402 54.2249C58.0934 54.2249 52.1351 51.4749 52.1351 39.3291C52.1351 27.1833 58.1507 25.0063 63.5934 25.0063C72.1298 25.0063 74.2496 29.475 74.9371 33.4281L69.8954 34.5739C69.609 32.7406 68.0048 30.449 64.3382 30.449C60.6142 30.449 58.0361 30.85 58.0361 39.3291C58.0361 47.8083 60.7288 48.725 64.8538 48.725V48.725ZM41.0781 39.6143C41.0781 31.1352 38.5 30.7342 34.7761 30.7342H31.1667V48.4945H34.7761C38.5 48.4945 41.0781 48.0935 41.0781 39.6143V39.6143ZM0 53.9359V25.2902H18.2187V30.7329H6.0729V36.5766H15.2395V41.962H6.0729V48.4932H18.2187V53.9359H0Z" fill="#0CCB93"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
12
public/svgs/mindsdb.svg
Normal file
12
public/svgs/mindsdb.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="264" height="151" viewBox="0 0 264 151" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_13_35)">
|
||||
<path d="M186.86 75.8418C189.798 98.8976 192.525 120.544 195.356 142.168C195.691 144.759 195.155 145.673 192.254 145.818C174.783 146.686 174.793 146.755 168.478 130.847C165.848 124.227 163.286 117.581 160.486 111.028C159.869 109.951 159.642 108.7 159.84 107.479C160.039 106.259 160.652 105.14 161.579 104.306C169.843 95.2778 177.892 86.0052 186.86 75.8418Z" fill="#00A587"/>
|
||||
<path d="M84.1747 95.1655C86.6767 107.857 89.145 119.785 91.3184 131.767C91.9891 135.476 95.0077 140.188 92.4252 142.696C90.1982 144.851 85.2546 141.802 81.4547 141.94C71.3595 142.317 66.6607 137.651 65.0207 127.854C64.0581 122.102 63.6321 118.119 68.4516 113.664C74.0774 107.825 79.3286 101.647 84.1747 95.1655V95.1655Z" fill="#00A587"/>
|
||||
<path d="M112.68 118.314C112.68 111.186 112.569 104.059 112.734 96.9342C112.797 94.136 112.006 93.1922 108.984 93.0932C88.369 92.4223 67.7583 91.6083 47.1521 90.6514C43.9156 90.5029 42.6377 91.407 41.9502 94.555C38.7707 109.134 35.1619 123.626 32.0562 138.222C31.1842 142.32 29.5945 143.831 25.2378 143.366C20.4038 142.873 15.5373 142.763 10.6853 143.036C7.02954 143.218 6.32523 141.716 6.3789 138.585C6.69416 116.694 7.16037 94.8026 6.94572 72.9181C6.90613 66.1838 9.19198 59.6359 13.4288 54.3467C24.832 38.7551 35.8159 22.8829 46.8636 7.0504C47.4595 5.96628 48.3973 5.10199 49.5357 4.58777C50.6742 4.07355 51.9516 3.93721 53.1756 4.19931C94.3679 8.53528 135.576 12.736 176.8 16.8013C179.378 17.1054 181.84 18.0315 183.967 19.4973C208.497 35.1813 232.984 50.9215 257.638 66.4142C261.327 68.724 261.428 70.044 257.883 72.1328C257.12 72.5383 256.438 73.0773 255.871 73.7233C248.77 82.7483 240.443 82.3424 230.381 78.343C218.22 73.522 205.395 70.3245 192.925 66.2261C191.738 65.7528 190.451 65.5735 189.178 65.7041C187.904 65.8347 186.682 66.2712 185.62 66.9751C174.15 73.9839 162.627 80.907 151.052 87.7442C150.009 88.2936 149.119 89.0852 148.458 90.0502C147.797 91.0151 147.385 92.1243 147.259 93.2813C144.72 108.328 141.815 123.31 139.481 138.396C138.81 142.719 137.244 144.089 132.877 143.788C127.332 143.466 121.772 143.457 116.225 143.762C112.935 143.907 112.093 142.772 112.2 139.687C112.415 132.562 112.268 125.428 112.268 118.301L112.68 118.314Z" fill="#00A587"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_13_35">
|
||||
<rect width="264" height="151" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/svgs/mosquitto.png
Normal file
BIN
public/svgs/mosquitto.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@@ -14,7 +14,10 @@
|
||||
'w-full' => $fullWidth,
|
||||
])>
|
||||
@if (!$hideLabel)
|
||||
<label class="flex gap-4 px-0 min-w-fit label">
|
||||
<label @class([
|
||||
"flex gap-4 px-0 min-w-fit label",
|
||||
'opacity-40' => $disabled,
|
||||
])>
|
||||
<span class="flex gap-2">
|
||||
@if ($label)
|
||||
{!! $label !!}
|
||||
|
@@ -22,18 +22,23 @@
|
||||
'dispatchEventMessage' => '',
|
||||
])
|
||||
|
||||
@php
|
||||
$settings = instanceSettings();
|
||||
$disableTwoStepConfirmation = $settings->disable_two_step_confirmation ?? false;
|
||||
@endphp
|
||||
|
||||
<div x-data="{
|
||||
modalOpen: false,
|
||||
step: {{ empty($checkboxes) ? 2 : 1 }},
|
||||
initialStep: {{ empty($checkboxes) ? 2 : 1 }},
|
||||
finalStep: {{ $confirmWithPassword ? 3 : 2 }},
|
||||
finalStep: {{ $confirmWithPassword && !$disableTwoStepConfirmation ? 3 : 2 }},
|
||||
deleteText: '',
|
||||
password: '',
|
||||
actions: @js($actions),
|
||||
confirmationText: @js($confirmationText),
|
||||
userConfirmationText: '',
|
||||
confirmWithText: @js($confirmWithText),
|
||||
confirmWithPassword: @js($confirmWithPassword),
|
||||
confirmWithText: @js($confirmWithText && !$disableTwoStepConfirmation),
|
||||
confirmWithPassword: @js($confirmWithPassword && !$disableTwoStepConfirmation),
|
||||
copied: false,
|
||||
submitAction: @js($submitAction),
|
||||
passwordError: '',
|
||||
@@ -41,6 +46,7 @@
|
||||
dispatchEvent: @js($dispatchEvent),
|
||||
dispatchEventType: @js($dispatchEventType),
|
||||
dispatchEventMessage: @js($dispatchEventMessage),
|
||||
disableTwoStepConfirmation: @js($disableTwoStepConfirmation),
|
||||
resetModal() {
|
||||
this.step = this.initialStep;
|
||||
this.deleteText = '';
|
||||
@@ -153,8 +159,8 @@
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen"
|
||||
class="absolute inset-0 w-full h-full bg-black bg-opacity-20 backdrop-blur-sm"></div>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black bg-opacity-20 backdrop-blur-sm">
|
||||
</div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
@@ -222,6 +228,7 @@
|
||||
</template>
|
||||
@endforeach
|
||||
</ul>
|
||||
@if (!$disableTwoStepConfirmation)
|
||||
@if ($confirmWithText)
|
||||
<div class="mb-4">
|
||||
<h4 class="mb-2 text-lg font-semibold">Confirm Actions</h4>
|
||||
@@ -233,8 +240,8 @@
|
||||
class="absolute right-2 top-1/2 text-gray-500 transform -translate-y-1/2 hover:text-gray-700"
|
||||
title="Copy confirmation text" x-ref="copyButton">
|
||||
<template x-if="!copied">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
||||
<path
|
||||
d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
||||
@@ -259,9 +266,11 @@
|
||||
class="p-2 mt-1 w-full text-black rounded input">
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Password confirmation -->
|
||||
@if (!$disableTwoStepConfirmation)
|
||||
<div x-show="step === 3 && confirmWithPassword">
|
||||
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert">
|
||||
<p class="font-bold">Final Confirmation</p>
|
||||
@@ -273,15 +282,17 @@
|
||||
Your Password
|
||||
</label>
|
||||
<form @submit.prevent @keydown.enter.prevent>
|
||||
<input type="password" id="password-confirm" x-model="password" class="w-full input"
|
||||
placeholder="Enter your password">
|
||||
<input type="password" id="password-confirm" x-model="password"
|
||||
class="w-full input" placeholder="Enter your password">
|
||||
</form>
|
||||
<p x-show="passwordError" x-text="passwordError" class="mt-1 text-sm text-red-500"></p>
|
||||
<p x-show="passwordError" x-text="passwordError" class="mt-1 text-sm text-red-500">
|
||||
</p>
|
||||
@error('password')
|
||||
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<!-- Navigation buttons -->
|
||||
<div class="flex flex-wrap gap-2 justify-between mt-4">
|
||||
@@ -304,7 +315,8 @@
|
||||
</template>
|
||||
|
||||
<template x-if="step === 2">
|
||||
<x-forms.button x-bind:disabled="confirmWithText && userConfirmationText !== confirmationText"
|
||||
<x-forms.button
|
||||
x-bind:disabled="!disableTwoStepConfirmation && confirmWithText && userConfirmationText !== confirmationText"
|
||||
class="w-auto" isError
|
||||
@click="
|
||||
if (dispatchEvent) {
|
||||
@@ -316,11 +328,13 @@
|
||||
modalOpen = false;
|
||||
resetModal();
|
||||
submitForm();
|
||||
}">
|
||||
}
|
||||
">
|
||||
<span x-text="step2ButtonText"></span>
|
||||
</x-forms.button>
|
||||
</template>
|
||||
|
||||
@if (!$disableTwoStepConfirmation)
|
||||
<template x-if="step === 3 && confirmWithPassword">
|
||||
<x-forms.button x-bind:disabled="!password" class="w-auto" isError
|
||||
@click="
|
||||
@@ -339,6 +353,7 @@
|
||||
<span x-text="step3ButtonText"></span>
|
||||
</x-forms.button>
|
||||
</template>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,5 +1,14 @@
|
||||
<div class="pb-6">
|
||||
<livewire:server.proxy.modal :server="$server" />
|
||||
<x-modal modalId="startProxy">
|
||||
<x-slot:modalBody>
|
||||
<livewire:activity-monitor header="Proxy Startup Logs" />
|
||||
</x-slot:modalBody>
|
||||
<x-slot:modalSubmit>
|
||||
<x-forms.button onclick="startProxy.close()" type="submit">
|
||||
Close
|
||||
</x-forms.button>
|
||||
</x-slot:modalSubmit>
|
||||
</x-modal>
|
||||
<div class="flex items-center gap-2">
|
||||
<h1>Server</h1>
|
||||
@if ($server->proxySet())
|
||||
@@ -13,20 +22,9 @@
|
||||
href="{{ route('server.show', [
|
||||
'server_uuid' => data_get($parameters, 'server_uuid'),
|
||||
]) }}">
|
||||
<button>General</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.private-key') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('server.private-key', [
|
||||
'server_uuid' => data_get($parameters, 'server_uuid'),
|
||||
]) }}">
|
||||
<button>Private Key</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('server.resources', [
|
||||
'server_uuid' => data_get($parameters, 'server_uuid'),
|
||||
]) }}">
|
||||
<button>Resources</button>
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
|
||||
@if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('server.proxy', [
|
||||
@@ -34,18 +32,6 @@
|
||||
]) }}">
|
||||
<button>Proxy</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.destinations') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('server.destinations', [
|
||||
'server_uuid' => data_get($parameters, 'server_uuid'),
|
||||
]) }}">
|
||||
<button>Destinations</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.log-drains') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('server.log-drains', [
|
||||
'server_uuid' => data_get($parameters, 'server_uuid'),
|
||||
]) }}">
|
||||
<button>Log Drains</button>
|
||||
</a>
|
||||
@endif
|
||||
</nav>
|
||||
<div class="order-first sm:order-last">
|
||||
|
@@ -1,18 +1,16 @@
|
||||
@if ($server->proxySet())
|
||||
<div class="flex h-full pr-4">
|
||||
<div class="flex flex-col w-48 gap-4 min-w-fit">
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}"
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
href="{{ route('server.proxy', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
|
||||
<button>Dynamic Configurations</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.proxy.logs') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('server.proxy.logs') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
href="{{ route('server.proxy.logs', $parameters) }}">
|
||||
<button>Logs</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
@@ -5,10 +5,10 @@
|
||||
<x-modal-input buttonTitle="+ Add" title="New Destination">
|
||||
<livewire:destination.new.docker :server_id="$server->id" />
|
||||
</x-modal-input>
|
||||
<x-forms.button wire:click='scan'>Scan Destinations</x-forms.button>
|
||||
<x-forms.button wire:click='scan'>Scan for Destinations</x-forms.button>
|
||||
</div>
|
||||
<div class="pt-2 pb-6 ">Destinations are used to segregate resources by network.</div>
|
||||
<div class="flex gap-2 ">
|
||||
<div>Destinations are used to segregate resources by network.</div>
|
||||
<div class="flex gap-2 pt-6">
|
||||
Available for using:
|
||||
@forelse ($server->standaloneDockers as $docker)
|
||||
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
|
||||
|
@@ -9,8 +9,25 @@
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Name" id="database.name" />
|
||||
<x-forms.input label="Description" id="database.description" />
|
||||
<x-forms.input label="Image" id="database.image" required
|
||||
helper="For all available images, check here:<br><br><a target='_blank' href='https://hub.docker.com/_/redis'>https://hub.docker.com/_/redis</a>" />
|
||||
<x-forms.input label="Image" id="database.image" required helper="For all available images, check here:<br><br><a target='_blank' href='https://hub.docker.com/_/redis'>https://hub.docker.com/_/redis</a>" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@if (version_compare($redis_version, '6.0', '>='))
|
||||
<x-forms.input label="Username" id="redis_username" required
|
||||
helper="You can change the Redis Username in the input field below or by editing the value of the REDIS_USERNAME environment variable.
|
||||
<br><br>
|
||||
If you change the Redis Username in the database, please sync it here, otherwise automations (like backups) won't work.
|
||||
<br><br>
|
||||
Note: If the environment variable REDIS_USERNAME is set as a shared variable (environment, project, or team-based), this input field will become read-only."
|
||||
:disabled="$this->isSharedVariable('REDIS_USERNAME')" />
|
||||
@endif
|
||||
<x-forms.input label="Password" id="redis_password" type="password" required
|
||||
helper="You can change the Redis Password in the input field below or by editing the value of the REDIS_PASSWORD environment variable.
|
||||
<br><br>
|
||||
If you change the Redis Password in the database, please sync it here, otherwise automations (like backups) won't work.
|
||||
<br><br>
|
||||
Note: If the environment variable REDIS_PASSWORD is set as a shared variable (environment, project, or team-based), this input field will become read-only."
|
||||
:disabled="$this->isSharedVariable('REDIS_PASSWORD')" />
|
||||
</div>
|
||||
<x-forms.input
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
@@ -19,42 +36,32 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="py-2">Network</h3>
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
|
||||
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433" />
|
||||
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings" helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433" />
|
||||
</div>
|
||||
<x-forms.input label="Redis URL (internal)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="db_url" />
|
||||
<x-forms.input label="Redis URL (internal)" helper="If you change the user/password/port, this could be different. This is with the default values." type="password" readonly wire:model="db_url" />
|
||||
@if ($db_url_public)
|
||||
<x-forms.input label="Redis URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="db_url_public" />
|
||||
<x-forms.input label="Redis URL (public)" helper="If you change the user/password/port, this could be different. This is with the default values." type="password" readonly wire:model="db_url_public" />
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="py-2">Proxy</h3>
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.input placeholder="5432" disabled="{{ data_get($database, 'is_public') }}"
|
||||
id="database.public_port" label="Public Port" />
|
||||
<x-forms.input placeholder="5432" disabled="{{ data_get($database, 'is_public') }}" id="database.public_port" label="Public Port" />
|
||||
<x-slide-over fullScreen>
|
||||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database" container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}" @click="slideOverOpen=true"
|
||||
class="w-28">Proxy Logs</x-forms.button>
|
||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}" @click="slideOverOpen=true" class="w-28">Proxy Logs</x-forms.button>
|
||||
</x-slide-over>
|
||||
<x-forms.checkbox instantSave id="database.is_public" label="Make it publicly available" />
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.textarea
|
||||
helper="<a target='_blank' class='underline dark:text-white' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>Redis Default Configuration</a>"
|
||||
label="Custom Redis Configuration" rows="10" id="database.redis_conf" />
|
||||
<x-forms.textarea helper="<a target='_blank' class='underline dark:text-white' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>Redis Default Configuration</a>" label="Custom Redis Configuration" rows="10" id="database.redis_conf" />
|
||||
<h3 class="pt-4">Advanced</h3>
|
||||
<div class="flex flex-col">
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveAdvanced" id="database.is_log_drain_enabled" label="Drain Logs" />
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings." instantSave="instantSaveAdvanced" id="database.is_log_drain_enabled" label="Drain Logs" />
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -24,6 +24,12 @@
|
||||
<div x-text="item.description"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-2 pt-4 pb-2 mr-4 text-xs lg:py-0 lg:justify-normal">
|
||||
<a class="mx-4 font-bold hover:underline"
|
||||
:href="item.settingsRoute">
|
||||
Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
@@ -7,15 +7,15 @@
|
||||
<h1>Resources</h1>
|
||||
@if ($environment->isEmpty())
|
||||
<a class="button"
|
||||
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => request()->route('environment_name')]) }}">
|
||||
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($parameters, 'environment_name')]) }}">
|
||||
Clone
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('project.resource.create', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
|
||||
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_name' => data_get($parameters, 'environment_name')]) }} "
|
||||
class="button">+
|
||||
New</a>
|
||||
<a class="button"
|
||||
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => request()->route('environment_name')]) }}">
|
||||
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($parameters, 'environment_name')]) }}">
|
||||
Clone
|
||||
</a>
|
||||
@endif
|
||||
@@ -25,7 +25,7 @@
|
||||
<ol class="flex items-center">
|
||||
<li class="inline-flex items-center">
|
||||
<a class="text-xs truncate lg:text-sm"
|
||||
href="{{ route('project.show', ['project_uuid' => request()->route('project_uuid')]) }}">
|
||||
href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}">
|
||||
{{ $project->name }}</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -44,7 +44,7 @@
|
||||
</nav>
|
||||
</div>
|
||||
@if ($environment->isEmpty())
|
||||
<a href="{{ route('project.resource.create', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
|
||||
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_name' => data_get($parameters, 'environment_name')]) }} "
|
||||
class="items-center justify-center box">+ Add New Resource</a>
|
||||
@else
|
||||
<div x-data="searchComponent()">
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" x-init="$wire.check_status">
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }">
|
||||
<x-slot:title>
|
||||
{{ data_get_str($service, 'name')->limit(10) }} > Configuration | Coolify
|
||||
</x-slot>
|
||||
|
@@ -17,7 +17,6 @@
|
||||
label="Image Tag" id="database.image"></x-forms.input>
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
|
||||
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
|
||||
label="Public Port" />
|
||||
<x-forms.checkbox instantSave id="database.is_public" label="Make it publicly available" />
|
||||
|
@@ -3,8 +3,9 @@
|
||||
<div>Private Keys are used to connect to your servers without passwords.</div>
|
||||
<div class="font-bold">You should not use passphrase protected keys.</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<x-forms.button wire:click="generateNewEDKey">Generate new ED25519 SSH Key (Recommended, fastest and most secure)</x-forms.button>
|
||||
<div class="flex gap-2 mb-4 w-full">
|
||||
<x-forms.button wire:click="generateNewEDKey" isHighlighted class="w-full">Generate new ED25519 SSH
|
||||
Key</x-forms.button>
|
||||
<x-forms.button wire:click="generateNewRSAKey">Generate new RSA SSH Key</x-forms.button>
|
||||
</div>
|
||||
<form class="flex flex-col gap-2" wire:submit='createPrivateKey'>
|
||||
|
84
resources/views/livewire/server/advanced.blade.php
Normal file
84
resources/views/livewire/server/advanced.blade.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<form wire:submit='submit'>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Advanced</h2>
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Manual Cleanup"
|
||||
submitAction="manualCleanup" :actions="[
|
||||
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
|
||||
'Permanently deletes all unused images',
|
||||
'Clears build cache',
|
||||
'Removes old versions of the Coolify helper image',
|
||||
'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
|
||||
'Optionally permanently deletes all unused networks (if enabled in advanced options).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Trigger Docker Cleanup" />
|
||||
</div>
|
||||
<div>Advanced configuration for your server.</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 pt-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>Docker Cleanup</h3>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
@if ($server->settings->force_docker_cleanup)
|
||||
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
|
||||
label="Docker cleanup frequency" required
|
||||
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
|
||||
@else
|
||||
<x-forms.input id="server.settings.docker_cleanup_threshold" label="Docker cleanup threshold (%)"
|
||||
required
|
||||
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
|
||||
@endif
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox
|
||||
helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Removes stopped containers manged by Coolify (as containers are none persistent, no data will be lost).</li>
|
||||
<li>Deletes unused images.</li>
|
||||
<li>Clears build cache.</li>
|
||||
<li>Removes old versions of the Coolify helper image.</li>
|
||||
<li>Optionally delete unused volumes (if enabled in advanced options).</li>
|
||||
<li>Optionally remove unused networks (if enabled in advanced options).</li>
|
||||
</ul>"
|
||||
instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
<span class="dark:text-warning font-bold">Warning: Enable these
|
||||
options only if you fully understand their implications and
|
||||
consequences!</span><br>Improper use will result in data loss and could cause
|
||||
functional issues.
|
||||
</p>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox instantSave id="server.settings.delete_unused_volumes" label="Delete Unused Volumes"
|
||||
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br>
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li>
|
||||
<li>Data from stopped containers volumes will be permanently lost.</li>
|
||||
<li>No way to recover deleted volume data.</li>
|
||||
</ul>" />
|
||||
<x-forms.checkbox instantSave id="server.settings.delete_unused_networks" label="Delete Unused Networks"
|
||||
helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br>
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li>
|
||||
<li>Custom networks for stopped containers will be permanently deleted.</li>
|
||||
<li>Functionality may be lost and containers may not be able to communicate with each other.</li>
|
||||
</ul>" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<h3>Builds</h3>
|
||||
<div>Customize the build process.</div>
|
||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap pt-4">
|
||||
<x-forms.input id="server.settings.concurrent_builds" label="Number of concurrent builds" required
|
||||
helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." />
|
||||
<x-forms.input id="server.settings.dynamic_timeout" label="Deployment timeout (seconds)" required
|
||||
helper="You can define the maximum duration for a deployment to run before timing it out." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
42
resources/views/livewire/server/cloudflare-tunnels.blade.php
Normal file
42
resources/views/livewire/server/cloudflare-tunnels.blade.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<div>
|
||||
<div class="flex gap-1 items-center">
|
||||
<h2>Cloudflare Tunnels</h2>
|
||||
<x-helper class="inline-flex"
|
||||
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 pt-6">
|
||||
@if ($server->settings->is_cloudflare_tunnel)
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel"
|
||||
label="Enabled" />
|
||||
</div>
|
||||
@elseif (!$server->isFunctional())
|
||||
<div
|
||||
class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded dark:bg-yellow-900 dark:text-yellow-300">
|
||||
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please
|
||||
validate your server first.</span> Then you will need a Cloudflare token and an SSH
|
||||
domain configured.
|
||||
<br />
|
||||
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please
|
||||
click <span wire:click="manualCloudflareConfig"
|
||||
class="underline cursor-pointer">here</span>, then you should validate the server.
|
||||
<br /><br />
|
||||
For more information, please read our <a
|
||||
href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank"
|
||||
class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">documentation</a>.
|
||||
</div>
|
||||
@endif
|
||||
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional())
|
||||
<x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels"
|
||||
class="w-full" :closeOutside="false">
|
||||
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
|
||||
</x-modal-input>
|
||||
@endif
|
||||
@if ($server->isFunctional() && !$server->settings->is_cloudflare_tunnel)
|
||||
<div wire:click="manualCloudflareConfig" class="w-full underline cursor-pointer">
|
||||
I have configured Cloudflare Tunnels manually
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
@@ -1,6 +1,6 @@
|
||||
<div>
|
||||
@if ($server->id !== 0)
|
||||
<h2 class="pt-4">Danger Zone</h2>
|
||||
<h2>Danger Zone</h2>
|
||||
<div class="">Woah. I hope you know what are you doing.</div>
|
||||
<h4 class="pt-4">Delete Server</h4>
|
||||
<div class="pb-4">This will remove this server from Coolify. Beware! There is no coming
|
||||
@@ -16,7 +16,7 @@
|
||||
<x-modal-confirmation title="Confirm Server Deletion?" isErrorButton buttonTitle="Delete"
|
||||
submitAction="delete" :actions="['This server will be permanently deleted.']" confirmationText="{{ $server->name }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
|
||||
shortConfirmationLabel="Server Name" step2ButtonText="Continue" step3ButtonText="Permanently Delete" />
|
||||
shortConfirmationLabel="Server Name" step3ButtonText="Permanently Delete" />
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
@@ -2,6 +2,6 @@
|
||||
<x-slot:title>
|
||||
{{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify
|
||||
</x-slot>
|
||||
<x-server.navbar :server="$server" :parameters="$parameters" />
|
||||
{{-- <x-server.navbar :server="$server" :parameters="$parameters" /> --}}
|
||||
<livewire:destination.show :server="$server" />
|
||||
</div>
|
||||
|
@@ -119,55 +119,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="{{ $server->isFunctional() ? 'w-96' : 'w-full' }}">
|
||||
<div class="w-full">
|
||||
@if (!$server->isLocalhost())
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox instantSave id="server.settings.is_build_server"
|
||||
label="Use it as a build server?" />
|
||||
<div class="flex flex-col gap-2 pt-6">
|
||||
<div class="flex gap-1 items-center">
|
||||
<h3 class="text-lg font-semibold">Cloudflare Tunnels</h3>
|
||||
<x-helper class="inline-flex"
|
||||
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
|
||||
</div>
|
||||
@if ($server->settings->is_cloudflare_tunnel)
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel"
|
||||
label="Enabled" />
|
||||
</div>
|
||||
@elseif (!$server->isFunctional())
|
||||
<div
|
||||
class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded dark:bg-yellow-900 dark:text-yellow-300">
|
||||
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please
|
||||
validate your server first.</span> Then you will need a Cloudflare token and an SSH
|
||||
domain configured.
|
||||
<br />
|
||||
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please
|
||||
click <span wire:click="manualCloudflareConfig"
|
||||
class="underline cursor-pointer">here</span>, then you should validate the server.
|
||||
<br /><br />
|
||||
For more information, please read our <a
|
||||
href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank"
|
||||
class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">documentation</a>.
|
||||
</div>
|
||||
@endif
|
||||
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional())
|
||||
<x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels"
|
||||
class="w-full" :closeOutside="false">
|
||||
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
|
||||
</x-modal-input>
|
||||
@endif
|
||||
@if ($server->isFunctional() && !$server->settings->is_cloudflare_tunnel)
|
||||
<div wire:click="manualCloudflareConfig" class="w-full underline cursor-pointer">
|
||||
I have configured Cloudflare Tunnels manually
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
|
||||
<h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span></h3>
|
||||
<div class="pb-4">Read the docs <a class='underline dark:text-white'
|
||||
href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>.
|
||||
</div>
|
||||
<div class="w-96">
|
||||
@if ($server->settings->is_swarm_worker)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox"
|
||||
id="server.settings.is_swarm_manager"
|
||||
@@ -189,110 +153,49 @@
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Worker?" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($server->isFunctional())
|
||||
<h3 class="pt-4">Settings</h3>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox
|
||||
helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Removes stopped containers manged by Coolify (as containers are none persistent, no data will be lost).</li>
|
||||
<li>Deletes unused images.</li>
|
||||
<li>Clears build cache.</li>
|
||||
<li>Removes old versions of the Coolify helper image.</li>
|
||||
<li>Optionally delete unused volumes (if enabled in advanced options).</li>
|
||||
<li>Optionally remove unused networks (if enabled in advanced options).</li>
|
||||
</ul>"
|
||||
instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" />
|
||||
</div>
|
||||
<x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Docker Cleanup"
|
||||
submitAction="manualCleanup" :actions="[
|
||||
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
|
||||
'Permanently deletes all unused images',
|
||||
'Clears build cache',
|
||||
'Removes old versions of the Coolify helper image',
|
||||
'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
|
||||
'Optionally permanently deletes all unused networks (if enabled in advanced options).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Trigger Docker Cleanup" />
|
||||
</div>
|
||||
@if ($server->settings->force_docker_cleanup)
|
||||
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
|
||||
label="Docker cleanup frequency" required
|
||||
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
|
||||
@else
|
||||
<x-forms.input id="server.settings.docker_cleanup_threshold"
|
||||
label="Docker cleanup threshold (%)" required
|
||||
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
|
||||
@endif
|
||||
<div x-data="{ open: false }" class="mt-4 max-w-md">
|
||||
<button @click="open = !open" type="button"
|
||||
class="flex items-center justify-between w-full text-left text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100">
|
||||
<span>Advanced Options</span>
|
||||
<svg :class="{ 'rotate-180': open }" class="w-5 h-5 transition-transform duration-200"
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="open" class="mt-2 space-y-2">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2"><strong>Warning: Enable these
|
||||
options only if you fully understand their implications and
|
||||
consequences!</strong><br>Improper use will result in data loss and could cause
|
||||
functional issues.</p>
|
||||
<x-forms.checkbox instantSave id="server.settings.delete_unused_volumes"
|
||||
label="Delete Unused Volumes"
|
||||
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br>
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li>
|
||||
<li>Data from stopped containers volumes will be permanently lost.</li>
|
||||
<li>No way to recover deleted volume data.</li>
|
||||
</ul>" />
|
||||
<x-forms.checkbox instantSave id="server.settings.delete_unused_networks"
|
||||
label="Delete Unused Networks"
|
||||
helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br>
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li>
|
||||
<li>Custom networks for stopped containers will be permanently deleted.</li>
|
||||
<li>Functionality may be lost and containers may not be able to communicate with each other.</li>
|
||||
</ul>" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-4 sm:flex-nowrap">
|
||||
<x-forms.input id="server.settings.concurrent_builds" label="Number of concurrent builds" required
|
||||
helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." />
|
||||
<x-forms.input id="server.settings.dynamic_timeout" label="Deployment timeout (seconds)" required
|
||||
helper="You can define the maximum duration for a deployment to run before timing it out." />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center pt-4 pb-2">
|
||||
<h3>Sentinel</h3>
|
||||
{{-- @if ($server->isSentinelEnabled()) --}}
|
||||
{{-- <x-forms.button wire:click='restartSentinel'>Restart</x-forms.button> --}}
|
||||
{{-- @endif --}}
|
||||
</div>
|
||||
@if (isDev())
|
||||
<x-forms.button wire:click="getPushData"> Push Test </x-forms.button>
|
||||
{{-- <div class="w-64">
|
||||
<x-forms.checkbox instantSave id="server.settings.is_metrics_enabled" label="Enable Metrics" />
|
||||
<x-forms.button>Start Sentinel</x-forms.button>
|
||||
</div> --}}
|
||||
<div class="flex gap-2 items-center pt-4 pb-2">
|
||||
<h3>Sentinel</h3>
|
||||
@if ($server->isSentinelEnabled())
|
||||
<div class="flex gap-2 items-center"
|
||||
wire:poll.{{ $server->settings->sentinel_push_interval_seconds }}s="checkSyncStatus">
|
||||
@if ($server->isSentinelLive())
|
||||
<x-status.running status="In-sync" noLoading />
|
||||
<x-forms.button wire:click='restartSentinel'>Restart</x-forms.button>
|
||||
@else
|
||||
<x-status.stopped status="Out-of-sync" noLoading />
|
||||
<x-forms.button wire:click='restartSentinel'>Sync</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox instantSave id="server.settings.is_sentinel_enabled" label="Enable Sentinel" />
|
||||
@if ($server->isSentinelEnabled())
|
||||
<x-forms.checkbox instantSave id="server.settings.is_metrics_enabled"
|
||||
label="Enable Metrics" />
|
||||
@else
|
||||
<x-forms.checkbox instantSave disabled id="server.settings.is_metrics_enabled"
|
||||
label="Enable Metrics" />
|
||||
@endif
|
||||
</div>
|
||||
@if ($server->isSentinelEnabled())
|
||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap items-end">
|
||||
<x-forms.input type="password" id="server.settings.sentinel_token" label="Metrics token"
|
||||
required helper="Token for collector (Sentinel)." />
|
||||
<x-forms.input type="password" id="server.settings.sentinel_token" label="Sentinel token"
|
||||
required helper="Token for Sentinel." />
|
||||
<x-forms.button wire:click="regenerateSentinelToken">Regenerate</x-forms.button>
|
||||
</div>
|
||||
|
||||
<x-forms.input id="server.settings.sentinel_custom_url" required label="Coolify URL"
|
||||
helper="URL to your Coolify instance. If it is empty that means you do not have a FQDN set for your Coolify instance." />
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap">
|
||||
<x-forms.input id="server.settings.sentinel_metrics_refresh_rate_seconds"
|
||||
label="Metrics rate (seconds)" required
|
||||
@@ -300,11 +203,14 @@
|
||||
<x-forms.input id="server.settings.sentinel_metrics_history_days"
|
||||
label="Metrics history (days)" required
|
||||
helper="How many days should the metrics data should be reserved." />
|
||||
<x-forms.input id="server.settings.sentinel_push_interval_seconds"
|
||||
label="Push interval (seconds)" required
|
||||
helper="How many seconds should the metrics data should be pushed to the collector." />
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div>Metrics are disabled until a few bugs are fixed.</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<x-slot:title>
|
||||
{{ data_get_str($server, 'name')->limit(10) }} > Server LogDrains | Coolify
|
||||
</x-slot>
|
||||
<x-server.navbar :server="$server" :parameters="$parameters" />
|
||||
{{-- <x-server.navbar :server="$server" :parameters="$parameters" /> --}}
|
||||
@if ($server->isFunctional())
|
||||
<h2>Log Drains</h2>
|
||||
<div class="pb-4">Sends service logs to 3rd party tools.</div>
|
||||
|
@@ -2,6 +2,5 @@
|
||||
<x-slot:title>
|
||||
Server Connection | Coolify
|
||||
</x-slot>
|
||||
<x-server.navbar :server="$server" :parameters="$parameters" />
|
||||
<livewire:server.show-private-key :server="$server" :privateKeys="$privateKeys" />
|
||||
</div>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
Proxy Dynamic Configuration | Coolify
|
||||
</x-slot>
|
||||
<x-server.navbar :server="$server" :parameters="$parameters" />
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-server.sidebar :server="$server" :parameters="$parameters" />
|
||||
<div class="w-full">
|
||||
@if ($server->isFunctional())
|
||||
|
@@ -3,7 +3,7 @@
|
||||
Proxy Logs | Coolify
|
||||
</x-slot>
|
||||
<x-server.navbar :server="$server" :parameters="$parameters" />
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-server.sidebar :server="$server" :parameters="$parameters" />
|
||||
<div class="w-full">
|
||||
<h2 class="pb-4">Logs</h2>
|
||||
|
@@ -1,12 +0,0 @@
|
||||
<div>
|
||||
<x-modal submitWireAction="proxyStatusUpdated" modalId="startProxy">
|
||||
<x-slot:modalBody>
|
||||
<livewire:activity-monitor header="Proxy Startup Logs" />
|
||||
</x-slot:modalBody>
|
||||
<x-slot:modalSubmit>
|
||||
<x-forms.button onclick="startProxy.close()" type="submit">
|
||||
Close
|
||||
</x-forms.button>
|
||||
</x-slot:modalSubmit>
|
||||
</x-modal>
|
||||
</div>
|
@@ -4,7 +4,7 @@
|
||||
</x-slot>
|
||||
<x-server.navbar :server="$server" :parameters="$parameters" />
|
||||
@if ($server->isFunctional())
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-server.sidebar :server="$server" :parameters="$parameters" />
|
||||
<div class="w-full">
|
||||
<livewire:server.proxy :server="$server" />
|
||||
|
@@ -2,24 +2,38 @@
|
||||
<x-slot:title>
|
||||
{{ data_get_str($server, 'name')->limit(10) }} > Server Resources | Coolify
|
||||
</x-slot>
|
||||
<x-server.navbar :server="$server" :parameters="$parameters" />
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'managed' }" class="flex flex-col h-full gap-8 md:flex-row">
|
||||
<div class="flex flex-row gap-4 md:flex-col">
|
||||
<a :class="activeTab === 'managed' && 'dark:text-white'"
|
||||
@click.prevent="activeTab = 'managed'; window.location.hash = 'managed'" href="#">Managed</a>
|
||||
<a :class="activeTab === 'unmanaged' && 'dark:text-white'"
|
||||
@click.prevent="activeTab = 'unmanaged'; window.location.hash = 'unmanaged'" href="#">Unmanaged</a>
|
||||
</div>
|
||||
{{-- <x-server.navbar :server="$server" :parameters="$parameters" /> --}}
|
||||
<div x-data="{ activeTab: 'managed' }" class="flex flex-col h-full gap-8 md:flex-row">
|
||||
<div class="w-full">
|
||||
<div x-cloak x-show="activeTab === 'managed'" class="h-full">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex gap-2">
|
||||
<h2>Resources</h2>
|
||||
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>
|
||||
</div>
|
||||
<div class="subtitle">Here you can find all resources that are managed by Coolify.</div>
|
||||
<div>Here you can find all resources that are managed by Coolify.</div>
|
||||
<div class="flex flex-row gap-4 py-10">
|
||||
<div @class([
|
||||
'box-without-bg cursor-pointer bg-coolgray-100 text-white w-full text-center items-center justify-center',
|
||||
'bg-coollabs' => $activeTab === 'managed',
|
||||
]) wire:click="loadManagedContainers">
|
||||
Managed
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<x-loading wire:loading wire:target="loadManagedContainers" />
|
||||
</div>
|
||||
@if ($server->definedResources()->count() > 0)
|
||||
</div>
|
||||
<div @class([
|
||||
'box-without-bg cursor-pointer bg-coolgray-100 text-white w-full text-center items-center justify-center',
|
||||
'bg-coollabs' => $activeTab === 'unmanaged',
|
||||
]) wire:click="loadUnmanagedContainers">
|
||||
Unmanaged
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<x-loading wire:loading wire:target="loadUnmanagedContainers" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if ($containers->count() > 0)
|
||||
@if ($activeTab === 'managed')
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<div class="overflow-x-auto">
|
||||
@@ -78,19 +92,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div>No resources found.</div>
|
||||
@endif
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'unmanaged'" class="h-full">
|
||||
<div class="flex flex-col" x-init="$wire.loadUnmanagedContainers()">
|
||||
<div class="flex gap-2">
|
||||
<h2>Resources</h2>
|
||||
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>
|
||||
</div>
|
||||
<div class="subtitle">Here you can find all other containers running on the server.</div>
|
||||
</div>
|
||||
@if ($unmanagedContainers->count() > 0)
|
||||
@elseif ($activeTab === 'unmanaged')
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<div class="overflow-x-auto">
|
||||
@@ -114,7 +116,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($unmanagedContainers->sortBy('name',SORT_NATURAL) as $resource)
|
||||
@forelse ($containers->sortBy('name',SORT_NATURAL) as $resource)
|
||||
<tr>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ data_get($resource, 'Names') }}
|
||||
@@ -152,11 +154,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div>No resources found.</div>
|
||||
@if ($activeTab === 'managed')
|
||||
<div>No managed resources found.</div>
|
||||
@elseif ($activeTab === 'unmanaged')
|
||||
<div>No unmanaged resources found.</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div>
|
||||
<div class="flex items-end gap-2 pb-6 ">
|
||||
<div class="flex items-end gap-2">
|
||||
<h2>Private Key</h2>
|
||||
<x-modal-input buttonTitle="+ Add" title="New Private Key">
|
||||
<livewire:security.private-key.create />
|
||||
@@ -9,29 +9,25 @@
|
||||
</x-forms.button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 pb-6">
|
||||
@if (data_get($server, 'privateKey.uuid'))
|
||||
<div>
|
||||
Currently attached Private Key:
|
||||
<a
|
||||
href="{{ route('security.private-key.show', ['private_key_uuid' => data_get($server, 'privateKey.uuid')]) }}">
|
||||
<button class="dark:text-white btn-link">{{ data_get($server, 'privateKey.name') }}</button>
|
||||
</a>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="pb-4">Change your server's private key.</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="">No private key attached.</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
<h3 class="pb-4">Choose another Key</h3>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="grid xl:grid-cols-2 grid-cols-1 gap-2">
|
||||
@forelse ($privateKeys as $private_key)
|
||||
<div class="box group cursor-pointer"
|
||||
wire:click='setPrivateKey({{ $private_key->id }})'>
|
||||
<div class="box-without-bg justify-between dark:bg-coolgray-100 bg-white items-center">
|
||||
<div class="flex flex-col ">
|
||||
<div class="box-title">{{ $private_key->name }}</div>
|
||||
<div class="box-description">{{ $private_key->description }}</div>
|
||||
</div>
|
||||
@if (data_get($server, 'privateKey.uuid') !== $private_key->uuid)
|
||||
<x-forms.button wire:click='setPrivateKey({{ $private_key->id }})'>
|
||||
Use this key
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button disabled>
|
||||
Currently used
|
||||
</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div>No private keys found. </div>
|
||||
|
@@ -3,11 +3,75 @@
|
||||
{{ data_get_str($server, 'name')->limit(10) }} > Server Configurations | Coolify
|
||||
</x-slot>
|
||||
<x-server.navbar :server="$server" :parameters="$parameters" />
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
|
||||
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
|
||||
@if ($server->isFunctional())
|
||||
<a class="menu-item" :class="activeTab === 'advanced' && 'menu-item-active'"
|
||||
@click.prevent="activeTab = 'advanced'; window.location.hash = 'advanced'" href="#">Advanced
|
||||
</a>
|
||||
@endif
|
||||
<a class="menu-item" :class="activeTab === 'private-key' && 'menu-item-active'"
|
||||
@click.prevent="activeTab = 'private-key'; window.location.hash = 'private-key'" href="#">Private
|
||||
Key</a>
|
||||
@if ($server->isFunctional())
|
||||
<a class="menu-item" :class="activeTab === 'cloudflare-tunnels' && 'menu-item-active'"
|
||||
@click.prevent="activeTab = 'cloudflare-tunnels'; window.location.hash = 'cloudflare-tunnels'"
|
||||
href="#">Cloudflare Tunnels</a>
|
||||
<a class="menu-item" :class="activeTab === 'resources' && 'menu-item-active'"
|
||||
@click.prevent="activeTab = 'resources'; window.location.hash = 'resources'"
|
||||
href="#">Resources</a>
|
||||
<a class="menu-item" :class="activeTab === 'destinations' && 'menu-item-active'"
|
||||
@click.prevent="activeTab = 'destinations'; window.location.hash = 'destinations'"
|
||||
href="#">Destinations</a>
|
||||
<a class="menu-item" :class="activeTab === 'log-drains' && 'menu-item-active'"
|
||||
@click.prevent="activeTab = 'log-drains'; window.location.hash = 'log-drains'" href="#">Log
|
||||
Drains</a>
|
||||
<a class="menu-item" :class="activeTab === 'metrics' && 'menu-item-active'"
|
||||
@click.prevent="activeTab = 'metrics'; window.location.hash = 'metrics'" href="#">Metrics</a>
|
||||
@endif
|
||||
@if (!$server->isLocalhost())
|
||||
<a class="menu-item" :class="activeTab === 'danger' && 'menu-item-active'"
|
||||
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger</a>
|
||||
@endif
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div x-cloak x-show="activeTab === 'general'" class="h-full">
|
||||
<livewire:server.form :server="$server" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'advanced'" class="h-full">
|
||||
<livewire:server.advanced :server="$server" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'private-key'" class="h-full">
|
||||
<livewire:server.private-key.show :server="$server" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'cloudflare-tunnels'" class="h-full">
|
||||
<livewire:server.cloudflare-tunnels :server="$server" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'resources'" class="h-full">
|
||||
<livewire:server.resources :server="$server" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'destinations'" class="h-full">
|
||||
<livewire:server.destination.show :server="$server" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'log-drains'" class="h-full">
|
||||
<livewire:server.log-drains :server="$server" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'metrics'" class="h-full">
|
||||
@if ($server->isFunctional() && $server->isMetricsEnabled())
|
||||
<div class="pt-10">
|
||||
<livewire:server.charts :server="$server" />
|
||||
</div>
|
||||
@else
|
||||
No metrics available.
|
||||
@endif
|
||||
</div>
|
||||
@if (!$server->isLocalhost())
|
||||
<div x-cloak x-show="activeTab === 'danger'" class="h-full">
|
||||
<livewire:server.delete :server="$server" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -41,7 +41,8 @@
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="inline-flex items-center relative w-full">
|
||||
<input autocomplete="off" wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
|
||||
<input autocomplete="off"
|
||||
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
|
||||
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" x-model="search"
|
||||
@focus="open = true" @click.away="open = false" @input="open = true"
|
||||
class="w-full input " :placeholder="placeholder"
|
||||
@@ -65,8 +66,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input id="settings.public_ipv4" type="password" label="Instance's IPv4"
|
||||
helper="Enter the IPv4 address of the instance.<br><br>It is useful if you have several IPv4 addresses and Coolify could not detect the correct one."
|
||||
placeholder="1.2.3.4" />
|
||||
<x-forms.input id="settings.public_ipv6" type="password" label="Instance's IPv6"
|
||||
helper="Enter the IPv6 address of the instance.<br><br>It is useful if you have several IPv6 addresses and Coolify could not detect the correct one."
|
||||
placeholder="2001:db8::1" />
|
||||
</div>
|
||||
<h4 class="w-full pt-6">DNS Validation</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_dns_validation_enabled" label="Enabled" />
|
||||
@@ -83,7 +92,7 @@
|
||||
|
||||
</div>
|
||||
<h4 class="pt-6">API</h4>
|
||||
<div class="md:w-96">
|
||||
<div class="md:w-96 pb-2">
|
||||
<x-forms.checkbox instantSave id="is_api_enabled" label="Enabled" />
|
||||
</div>
|
||||
<x-forms.input id="settings.allowed_ips" label="Allowed IPs"
|
||||
@@ -95,7 +104,7 @@
|
||||
<x-forms.checkbox instantSave id="is_registration_enabled" label="Registration Allowed" />
|
||||
<x-forms.checkbox instantSave id="do_not_track" label="Do Not Track" />
|
||||
</div>
|
||||
<h5 class="pt-4 font-bold text-white">Update</h5>
|
||||
<h4 class="pt-6">Update</h4>
|
||||
<div class="text-right md:w-96">
|
||||
@if (!is_null(env('AUTOUPDATE', null)))
|
||||
<div class="text-right md:w-96">
|
||||
@@ -119,6 +128,34 @@
|
||||
helper="Cron expression for auto update frequency (automatically update coolify).<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every day at 00:00" />
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h4 class="pt-6">Advanced</h4>
|
||||
<div class="text-right md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_registration_enabled" label="Registration Allowed" />
|
||||
<x-forms.checkbox instantSave id="do_not_track" label="Do Not Track" />
|
||||
</div>
|
||||
|
||||
<h5 class="py-4 font-bold text-white">Confirmation Settings</h5>
|
||||
@if ($disable_two_step_confirmation)
|
||||
<div class="md:w-96 pb-4">
|
||||
<x-forms.checkbox instantSave id="disable_two_step_confirmation" label="Disable Two Step Confirmation"
|
||||
helper="When disabled, you will not need to confirm actions with a text and user password. This significantly reduces security and may lead to accidental deletions or unwanted changes. Use with extreme caution, especially on production servers." />
|
||||
</div>
|
||||
@else
|
||||
<x-modal-confirmation title="Disable Two Step Confirmation?" buttonTitle="Disable Two Step Confirmation"
|
||||
isErrorButton submitAction="toggleTwoStepConfirmation" :actions="[
|
||||
'Tow Step confimation will be disabled globally.',
|
||||
'Disabling two step confirmation reduces security (as anyone can easily delete anything).',
|
||||
'The risk of accidental actions will increase.',
|
||||
]"
|
||||
confirmationText="DISABLE TWO STEP CONFIRMATION"
|
||||
confirmationLabel="Please type the confirmation text to disable two step confirmation."
|
||||
shortConfirmationLabel="Confirmation text" step3ButtonText="Disable Two Step Confirmation" />
|
||||
@endif
|
||||
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error md:w-[40rem] w-full mb-32">
|
||||
<p class="font-bold">Warning!</p>
|
||||
<p>Disabling two step confirmation reduces security (as anyone can easily delete anything) and increases the
|
||||
risk of accidental actions. This is not recommended for production servers.</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user