Merge branch 'next' into mosquitto
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
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -15,41 +14,43 @@ class StartSentinel
|
||||
if ($restart) {
|
||||
StopSentinel::run($server);
|
||||
}
|
||||
$metrics_history = $server->settings->sentinel_metrics_history_days;
|
||||
$refresh_rate = $server->settings->sentinel_metrics_refresh_rate_seconds;
|
||||
$token = $server->settings->sentinel_token;
|
||||
$endpoint = InstanceSettings::get()->fqdn;
|
||||
if (isDev()) {
|
||||
$endpoint = 'http://host.docker.internal:8000';
|
||||
}
|
||||
$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.');
|
||||
}
|
||||
// Ensure the endpoint is using HTTPS
|
||||
$endpoint = str($endpoint)->replace('http://', 'https://')->value();
|
||||
$environments = [
|
||||
'TOKEN' => $token,
|
||||
'ENDPOINT' => $endpoint,
|
||||
'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,
|
||||
];
|
||||
if (isDev()) {
|
||||
data_set($environments, 'GIN_MODE', 'debug');
|
||||
}
|
||||
$mount_dir = '/data/coolify/sentinel';
|
||||
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 --pull always --rm -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 ghcr.io/coollabsio/sentinel:$version";
|
||||
|
||||
return instant_remote_process([
|
||||
$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, true);
|
||||
], $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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ 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,7 +38,7 @@ 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();
|
||||
|
||||
@@ -115,7 +115,10 @@ class Kernel extends ConsoleKernel
|
||||
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
|
||||
$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) {
|
||||
|
||||
@@ -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.']);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ 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;
|
||||
@@ -40,6 +42,8 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
|
||||
public Collection $allDatabaseUuids;
|
||||
|
||||
public Collection $allTcpProxyUuids;
|
||||
|
||||
public Collection $allServiceApplicationIds;
|
||||
|
||||
public Collection $allApplicationPreviewsIds;
|
||||
@@ -60,6 +64,8 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
|
||||
public bool $foundProxy = false;
|
||||
|
||||
public bool $foundLogDrainContainer = false;
|
||||
|
||||
public function backoff(): int
|
||||
{
|
||||
return isDev() ? 1 : 3;
|
||||
@@ -87,6 +93,11 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
throw new \Exception('No data provided');
|
||||
}
|
||||
$data = collect($this->data);
|
||||
|
||||
$this->serverStatus();
|
||||
|
||||
$this->server->sentinelHeartbeat();
|
||||
|
||||
$this->containers = collect(data_get($data, 'containers'));
|
||||
if ($this->containers->isEmpty()) {
|
||||
return;
|
||||
@@ -122,6 +133,10 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
$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');
|
||||
@@ -153,7 +168,6 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
}
|
||||
|
||||
} else {
|
||||
$name = data_get($container, 'name');
|
||||
$uuid = $labels->get('com.docker.compose.service');
|
||||
$type = $labels->get('coolify.type');
|
||||
if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) {
|
||||
@@ -182,12 +196,25 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
$this->updateNotFoundServiceStatus();
|
||||
|
||||
$this->updateAdditionalServersStatus();
|
||||
|
||||
$this->checkLogDrainContainer();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -247,9 +274,18 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
private function updateProxyStatus()
|
||||
{
|
||||
// If proxy is not found, start it
|
||||
if (! $this->foundProxy && $this->server->isProxyShouldRun()) {
|
||||
ray('Proxy not found, starting it.');
|
||||
StartProxy::dispatch($this->server);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -361,4 +397,11 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
{
|
||||
return str($containerStatus)->contains('running');
|
||||
}
|
||||
|
||||
private function checkLogDrainContainer()
|
||||
{
|
||||
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
|
||||
InstallLogDrain::dispatch($this->server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
@@ -314,7 +313,7 @@ class General extends Component
|
||||
public function set_redirect()
|
||||
{
|
||||
try {
|
||||
$has_www = collect($this->application->fqdns)->filter(fn($fqdn) => str($fqdn)->contains('www.'))->count();
|
||||
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
|
||||
if ($has_www === 0 && $this->application->redirect === 'www') {
|
||||
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list and as an A DNS record (if applicable).');
|
||||
|
||||
@@ -335,6 +334,7 @@ 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(',');
|
||||
@@ -409,11 +409,13 @@ class General extends Component
|
||||
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);
|
||||
@@ -423,7 +425,7 @@ class General extends Component
|
||||
echo $config;
|
||||
}, $fileName, [
|
||||
'Content-Type' => 'application/json',
|
||||
'Content-Disposition' => 'attachment; filename=' . $fileName,
|
||||
'Content-Disposition' => 'attachment; filename='.$fileName,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
|
||||
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.');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -21,6 +21,7 @@ class EditDomain extends Component
|
||||
{
|
||||
$this->application = ServiceApplication::find($this->applicationId);
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
@@ -28,6 +29,7 @@ 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(',');
|
||||
@@ -48,6 +50,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,10 +30,7 @@ class ServiceApplicationView extends Component
|
||||
'application.is_stripprefix_enabled' => 'nullable|boolean',
|
||||
];
|
||||
|
||||
public function updatedApplicationFqdn()
|
||||
{
|
||||
|
||||
}
|
||||
public function updatedApplicationFqdn() {}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
@@ -82,6 +79,7 @@ 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(',');
|
||||
@@ -101,6 +99,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,18 +84,21 @@ 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 checkSyncStatus()
|
||||
{
|
||||
$this->server->refresh();
|
||||
$this->server->settings->refresh();
|
||||
}
|
||||
|
||||
public function regenerateSentinelToken()
|
||||
{
|
||||
try {
|
||||
$this->server->generateSentinelToken();
|
||||
$this->server->settings->generateSentinelToken();
|
||||
$this->server->settings->refresh();
|
||||
$this->dispatch('success', 'Metrics token regenerated.');
|
||||
$this->restartSentinel(notification: false);
|
||||
$this->dispatch('success', 'Token regenerated & Sentinel restarted.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -143,21 +134,35 @@ class Form extends Component
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
|
||||
public function checkPortForServerApi()
|
||||
public function updatedServerSettingsIsSentinelEnabled($value)
|
||||
{
|
||||
try {
|
||||
if ($this->server->settings->is_server_api_enabled === true) {
|
||||
$this->server->checkServerApi();
|
||||
$this->dispatch('success', 'Server API is reachable.');
|
||||
$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 {
|
||||
StartSentinel::run($this->server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
} 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);
|
||||
|
||||
@@ -165,56 +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);
|
||||
}
|
||||
@@ -271,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 === '') {
|
||||
@@ -289,28 +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.');
|
||||
}
|
||||
|
||||
public function startSentinel()
|
||||
{
|
||||
StartSentinel::run($this->server);
|
||||
$this->dispatch('success', 'Sentinel started.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
$this->loadUnmanagedContainers();
|
||||
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->dispatch('success', 'Private key updated successfully.');
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class Index extends Component
|
||||
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
|
||||
|
||||
protected Server $server;
|
||||
|
||||
public $timezones;
|
||||
|
||||
protected $rules = [
|
||||
@@ -57,7 +58,6 @@ class Index extends Component
|
||||
'settings.instance_timezone' => 'Instance Timezone',
|
||||
];
|
||||
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (isInstanceAdmin()) {
|
||||
@@ -171,7 +171,6 @@ class Index extends Component
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.index');
|
||||
|
||||
@@ -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);
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
|
||||
});
|
||||
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], [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -95,7 +97,8 @@ class Server extends BaseModel
|
||||
}
|
||||
}
|
||||
});
|
||||
static::deleting(function ($server) {
|
||||
|
||||
static::forceDeleting(function ($server) {
|
||||
$server->destinations()->each(function ($destination) {
|
||||
$destination->delete();
|
||||
});
|
||||
@@ -525,22 +528,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()
|
||||
@@ -550,7 +551,7 @@ $schema://$host {
|
||||
|
||||
public function isServerApiEnabled()
|
||||
{
|
||||
return $this->settings->is_server_api_enabled;
|
||||
return $this->settings->is_sentinel_enabled;
|
||||
}
|
||||
|
||||
public function checkServerApi()
|
||||
@@ -591,7 +592,15 @@ $schema://$host {
|
||||
{
|
||||
if ($this->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$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 (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?');
|
||||
@@ -600,17 +609,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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,7 +623,15 @@ $schema://$host {
|
||||
{
|
||||
if ($this->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$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 (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?');
|
||||
@@ -627,14 +640,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();
|
||||
@@ -1054,6 +1062,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',
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -997,8 +997,8 @@ class Service extends BaseModel
|
||||
break;
|
||||
case $image->contains('mysql'):
|
||||
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT'];
|
||||
$dbNameVariables = ['MYSQL_DATABASE'];
|
||||
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
@@ -1326,9 +1326,9 @@ class Service extends BaseModel
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -335,10 +335,11 @@ 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()
|
||||
->unique();
|
||||
->filter()
|
||||
->unique();
|
||||
}
|
||||
foreach ($domains as $loop => $domain) {
|
||||
try {
|
||||
@@ -388,7 +389,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
||||
if ($path !== '/') {
|
||||
// Middleware handling
|
||||
$middlewares = collect([]);
|
||||
if ($is_stripprefix_enabled && !str($image)->contains('ghost')) {
|
||||
if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) {
|
||||
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
|
||||
$middlewares->push("{$https_label}-stripprefix");
|
||||
}
|
||||
@@ -402,7 +403,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
||||
$labels = $labels->merge($redirect_to_non_www);
|
||||
$middlewares->push($to_non_www_name);
|
||||
}
|
||||
if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) {
|
||||
if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) {
|
||||
$labels = $labels->merge($redirect_to_www);
|
||||
$middlewares->push($to_www_name);
|
||||
}
|
||||
@@ -417,7 +418,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
||||
$middlewares = collect([]);
|
||||
if ($is_gzip_enabled) {
|
||||
$middlewares->push('gzip');
|
||||
}
|
||||
}
|
||||
if (str($image)->contains('ghost')) {
|
||||
$middlewares->push('redir-ghost');
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
@@ -3983,13 +3987,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) {
|
||||
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"]);
|
||||
@@ -4007,6 +4012,21 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire
|
||||
try {
|
||||
return instant_remote_process($commands, $server);
|
||||
} catch (\Exception $e) {
|
||||
// continue
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
@@ -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,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,7 @@ class DatabaseSeeder extends Seeder
|
||||
S3StorageSeeder::class,
|
||||
StandalonePostgresqlSeeder::class,
|
||||
OauthSettingSeeder::class,
|
||||
SentinelSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4959,7 +4959,7 @@ components:
|
||||
type: boolean
|
||||
is_reachable:
|
||||
type: boolean
|
||||
is_server_api_enabled:
|
||||
is_sentinel_enabled:
|
||||
type: boolean
|
||||
is_swarm_manager:
|
||||
type: boolean
|
||||
|
||||
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 |
@@ -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 !!}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')]) }}">
|
||||
|
||||
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
|
||||
|
||||
@@ -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,191 +119,98 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="{{ $server->isFunctional() ? 'w-96' : 'w-full' }}">
|
||||
<div class="w-full">
|
||||
@if (!$server->isLocalhost())
|
||||
<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 class="w-96">
|
||||
<x-forms.checkbox instantSave id="server.settings.is_build_server"
|
||||
label="Use it as a build server?" />
|
||||
</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>
|
||||
@if ($server->settings->is_swarm_worker)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox"
|
||||
id="server.settings.is_swarm_manager"
|
||||
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 Manager?" />
|
||||
@else
|
||||
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_manager"
|
||||
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 Manager?" />
|
||||
@endif
|
||||
<div class="w-96">
|
||||
@if ($server->settings->is_swarm_worker)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox"
|
||||
id="server.settings.is_swarm_manager"
|
||||
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 Manager?" />
|
||||
@else
|
||||
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_manager"
|
||||
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 Manager?" />
|
||||
@endif
|
||||
|
||||
@if ($server->settings->is_swarm_manager)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox"
|
||||
id="server.settings.is_swarm_worker"
|
||||
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?" />
|
||||
@else
|
||||
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_worker"
|
||||
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
|
||||
@if ($server->settings->is_swarm_manager)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox"
|
||||
id="server.settings.is_swarm_worker"
|
||||
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?" />
|
||||
@else
|
||||
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_worker"
|
||||
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>
|
||||
@if (isDev())
|
||||
<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 --}}
|
||||
@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>
|
||||
@if (isDev())
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox instantSave id="server.settings.is_metrics_enabled" label="Enable Metrics" />
|
||||
<x-forms.button wire:click="startSentinel">Start Sentinel</x-forms.button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<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>
|
||||
<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
|
||||
helper="The interval for gathering metrics. Lower means more disk space will be used." />
|
||||
<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_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
|
||||
helper="The interval for gathering metrics. Lower means more disk space will be used." />
|
||||
<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>
|
||||
</div>
|
||||
@else
|
||||
<div>Metrics are disabled until a few bugs are fixed.</div>
|
||||
@endif
|
||||
@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>
|
||||
|
||||
@@ -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>
|
||||
@@ -2,24 +2,30 @@
|
||||
<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 class="flex flex-col">
|
||||
<div class="flex gap-2">
|
||||
<h2>Resources</h2>
|
||||
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>
|
||||
</div>
|
||||
@if ($server->definedResources()->count() > 0)
|
||||
<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>
|
||||
<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>
|
||||
</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 +84,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 +108,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 +146,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div>No resources found.</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
@if ($activeTab === 'managed')
|
||||
<div>No managed resources found.</div>
|
||||
@elseif ($activeTab === 'unmanaged')
|
||||
<div>No unmanaged resources found.</div>
|
||||
@endif
|
||||
@endif
|
||||
</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>
|
||||
@else
|
||||
<div class="">No private key attached.</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="pb-4">Change your server's private key.</div>
|
||||
</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" />
|
||||
<livewire:server.form :server="$server" />
|
||||
@if ($server->isFunctional() && $server->isMetricsEnabled())
|
||||
<div class="pt-10">
|
||||
<livewire:server.charts :server="$server" />
|
||||
<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>
|
||||
@endif
|
||||
<livewire:server.delete :server="$server" />
|
||||
<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>
|
||||
|
||||
@@ -147,8 +147,11 @@ Route::group([
|
||||
if (! $server) {
|
||||
return response()->json(['message' => 'Server not found'], 404);
|
||||
}
|
||||
if ($server->settings->sentinel_token !== $naked_token) {
|
||||
return response()->json(['message' => 'Unauthorized'], 401);
|
||||
}
|
||||
$data = request()->all();
|
||||
$server->update(['sentinel_update_at' => now()]);
|
||||
|
||||
PushServerUpdateJob::dispatch($server, $data);
|
||||
|
||||
return response()->json(['message' => 'ok'], 200);
|
||||
|
||||
2
storage/pail/.gitignore
vendored
Normal file
2
storage/pail/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -39,4 +39,3 @@ services:
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
|
||||
48
templates/compose/mindsdb.yaml
Normal file
48
templates/compose/mindsdb.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
# documentation: https://docs.mindsdb.com/what-is-mindsdb
|
||||
# slogan: MindsDB is the platform for building AI from enterprise data, enabling smarter organizations.
|
||||
# tags: mysql, postgresdb, machine-learning, ai
|
||||
# logo: svgs/mindsdb.svg
|
||||
# port: 47334
|
||||
|
||||
services:
|
||||
mindsdb:
|
||||
image: mindsdb/mindsdb:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_MINDSDB_47334
|
||||
- SERVICE_FQDN_API_47335=/api
|
||||
- MINDSDB_DOCKER_ENV=true
|
||||
- MINDSDB_STORAGE_DIR=/mindsdb/var
|
||||
- FLASK_DEBUG=${FLASK_DEBUG:-1} # This will make sure http requests are logged regardless of log level
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- LANGFUSE_HOST=${LANGFUSE_HOST}
|
||||
- LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY}
|
||||
- LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY}
|
||||
- LANGFUSE_RELEASE=${LANGFUSE_RELEASE:-local}
|
||||
- LANGFUSE_DEBUG=${LANGFUSE_DEBUG:-False}
|
||||
- LANGFUSE_TIMEOUT=${LANGFUSE_TIMEOUT:-10}
|
||||
- LANGFUSE_SAMPLE_RATE=${LANGFUSE_SAMPLE_RATE:-1.0}
|
||||
- MINDSDB_DB_CON=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgresql/${POSTGRES_DB:-mindsdb-db}
|
||||
volumes:
|
||||
- mindsdb-data:/mindsdb/var
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:47334/api/util/ping"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 15
|
||||
|
||||
postgresql:
|
||||
image: postgres:16-alpine
|
||||
volumes:
|
||||
- mindsdb-postgresql-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=${SERVICE_USER_POSTGRES}
|
||||
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-mindsdb-db}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 15
|
||||
@@ -12,9 +12,9 @@ services:
|
||||
- SERVICE_FQDN_PLAUSIBLE
|
||||
- DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@plausible-db:5432/${POSTGRES_DB:-plausible-db}
|
||||
- CLICKHOUSE_DATABASE_URL=http://plausible-events-db:8123/plausible_events_db
|
||||
- BASE_URL=$SERVICE_FQDN_PLAUSIBLE
|
||||
- SECRET_KEY_BASE=$SERVICE_BASE64_64_PLAUSIBLE
|
||||
- TOTP_VAULT_KEY=$SERVICE_REALBASE64_32_TOTP
|
||||
- BASE_URL=${SERVICE_FQDN_PLAUSIBLE}
|
||||
- SECRET_KEY_BASE=${SERVICE_BASE64_64_PLAUSIBLE}
|
||||
- TOTP_VAULT_KEY=${SERVICE_REALBASE64_32_TOTP}
|
||||
depends_on:
|
||||
plausible-db:
|
||||
condition: service_healthy
|
||||
@@ -30,7 +30,7 @@ services:
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://127.0.0.1:8000/ping",
|
||||
"http://127.0.0.1:8000/api/health",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
|
||||
@@ -11,10 +11,11 @@ services:
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
|
||||
- POSTGRES_DB=${POSTGRES_DB:-windmill}
|
||||
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
|
||||
- POSTGRES_USER=${SERVICE_USER_POSTGRES}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-windmill-db}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -23,9 +24,9 @@ services:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
environment:
|
||||
- SERVICE_FQDN_WINDMILL_8000
|
||||
- DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@db/windmill
|
||||
- MODE=${MODE:-server}
|
||||
- BASE_URL=$SERVICE_FQDN_WINDMILL
|
||||
- DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db/${POSTGRES_DB:-windmill-db}
|
||||
- MODE=server
|
||||
- BASE_URL=${SERVICE_FQDN_WINDMILL}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -40,9 +41,9 @@ services:
|
||||
windmill-worker-1:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@db/windmill
|
||||
- MODE=${MODE:-worker}
|
||||
- WORKER_GROUP=${WORKER_GROUP:-default}
|
||||
- DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db/${POSTGRES_DB:-windmill-db}
|
||||
- MODE=worker
|
||||
- WORKER_GROUP=default
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -59,9 +60,9 @@ services:
|
||||
windmill-worker-2:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@db/windmill
|
||||
- MODE=${MODE:-worker}
|
||||
- WORKER_GROUP=${WORKER_GROUP:-default}
|
||||
- DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db/${POSTGRES_DB:-windmill-db}
|
||||
- MODE=worker
|
||||
- WORKER_GROUP=default
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -78,9 +79,9 @@ services:
|
||||
windmill-worker-3:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@db/windmill
|
||||
- MODE=${MODE:-worker}
|
||||
- WORKER_GROUP=${WORKER_GROUP:-default}
|
||||
- DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db/${POSTGRES_DB:-windmill-db}
|
||||
- MODE=worker
|
||||
- WORKER_GROUP=default
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -97,11 +98,11 @@ services:
|
||||
windmill-worker-native:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@db/windmill
|
||||
- MODE=${MODE:-worker}
|
||||
- WORKER_GROUP=${WORKER_GROUP:-native}
|
||||
- NUM_WORKERS=${NUM_WORKERS:-8}
|
||||
- SLEEP_QUEUE=${SLEEP_QUEUE:-200}
|
||||
- DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db/${POSTGRES_DB:-windmill-db}
|
||||
- MODE=worker
|
||||
- WORKER_GROUP=native
|
||||
- NUM_WORKERS=8
|
||||
- SLEEP_QUEUE=200
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -122,3 +123,4 @@ services:
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
@@ -1538,6 +1538,20 @@
|
||||
"minversion": "0.0.0",
|
||||
"port": "8081"
|
||||
},
|
||||
"mindsdb": {
|
||||
"documentation": "https://docs.mindsdb.com/what-is-mindsdb?utm_source=coolify.io",
|
||||
"slogan": "MindsDB is the platform for building AI from enterprise data, enabling smarter organizations.",
|
||||
"compose": "c2VydmljZXM6CiAgbWluZHNkYjoKICAgIGltYWdlOiAnbWluZHNkYi9taW5kc2RiOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NSU5EU0RCXzQ3MzM0CiAgICAgIC0gU0VSVklDRV9GUUROX0FQSV80NzMzNT0vYXBpCiAgICAgIC0gTUlORFNEQl9ET0NLRVJfRU5WPXRydWUKICAgICAgLSBNSU5EU0RCX1NUT1JBR0VfRElSPS9taW5kc2RiL3ZhcgogICAgICAtICdGTEFTS19ERUJVRz0ke0ZMQVNLX0RFQlVHOi0xfScKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtPUEVOQUlfQVBJX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX0hPU1Q9JHtMQU5HRlVTRV9IT1NUfScKICAgICAgLSAnTEFOR0ZVU0VfUFVCTElDX0tFWT0ke0xBTkdGVVNFX1BVQkxJQ19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TRUNSRVRfS0VZPSR7TEFOR0ZVU0VfU0VDUkVUX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1JFTEVBU0U9JHtMQU5HRlVTRV9SRUxFQVNFOi1sb2NhbH0nCiAgICAgIC0gJ0xBTkdGVVNFX0RFQlVHPSR7TEFOR0ZVU0VfREVCVUc6LUZhbHNlfScKICAgICAgLSAnTEFOR0ZVU0VfVElNRU9VVD0ke0xBTkdGVVNFX1RJTUVPVVQ6LTEwfScKICAgICAgLSAnTEFOR0ZVU0VfU0FNUExFX1JBVEU9JHtMQU5HRlVTRV9TQU1QTEVfUkFURTotMS4wfScKICAgICAgLSAnTUlORFNEQl9EQl9DT049cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzcWwvJHtQT1NUR1JFU19EQjotbWluZHNkYi1kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5kc2RiLWRhdGE6L21pbmRzZGIvdmFyJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjQ3MzM0L2FwaS91dGlsL3BpbmcnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5kc2RiLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW1pbmRzZGItZGJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxNQo=",
|
||||
"tags": [
|
||||
"mysql",
|
||||
"postgresdb",
|
||||
"machine-learning",
|
||||
"ai"
|
||||
],
|
||||
"logo": "svgs/mindsdb.svg",
|
||||
"minversion": "0.0.0",
|
||||
"port": "47334"
|
||||
},
|
||||
"minecraft": {
|
||||
"documentation": "https://github.com/itzg/docker-minecraft-server?utm_source=coolify.io",
|
||||
"slogan": "Minecraft Server that will automatically download selected version at startup.",
|
||||
@@ -2639,7 +2653,7 @@
|
||||
"windmill": {
|
||||
"documentation": "https://www.windmill.dev/docs/?utm_source=coolify.io",
|
||||
"slogan": "Windmill is a developer platform to build production-grade multi-steps automations and internal apps.",
|
||||
"compose": "c2VydmljZXM6CiAgZGI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2JwogICAgc2htX3NpemU6IDFnCiAgICB2b2x1bWVzOgogICAgICAtICdkYi1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXdpbmRtaWxsfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSBwb3N0Z3JlcycKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgd2luZG1pbGwtc2VydmVyOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dpbmRtaWxsLWxhYnMvd2luZG1pbGw6bWFpbicKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XSU5ETUlMTF84MDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiL3dpbmRtaWxsJwogICAgICAtICdNT0RFPSR7TU9ERTotc2VydmVyfScKICAgICAgLSBCQVNFX1VSTD0kU0VSVklDRV9GUUROX1dJTkRNSUxMCiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnd29ya2VyLWxvZ3M6L3RtcC93aW5kbWlsbC9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwMDAvaGVhbHRoJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLTE6CiAgICBpbWFnZTogJ2doY3IuaW8vd2luZG1pbGwtbGFicy93aW5kbWlsbDptYWluJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiL3dpbmRtaWxsJwogICAgICAtICdNT0RFPSR7TU9ERTotd29ya2VyfScKICAgICAgLSAnV09SS0VSX0dST1VQPSR7V09SS0VSX0dST1VQOi1kZWZhdWx0fScKICAgIGRlcGVuZHNfb246CiAgICAgIGRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnd29ya2VyLWRlcGVuZGVuY3ktY2FjaGU6L3RtcC93aW5kbWlsbC9jYWNoZScKICAgICAgLSAnd29ya2VyLWxvZ3M6L3RtcC93aW5kbWlsbC9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdleGl0IDAnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICB3aW5kbWlsbC13b3JrZXItMjoKICAgIGltYWdlOiAnZ2hjci5pby93aW5kbWlsbC1sYWJzL3dpbmRtaWxsOm1haW4nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGdyZXM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAZGIvd2luZG1pbGwnCiAgICAgIC0gJ01PREU9JHtNT0RFOi13b3JrZXJ9JwogICAgICAtICdXT1JLRVJfR1JPVVA9JHtXT1JLRVJfR1JPVVA6LWRlZmF1bHR9JwogICAgZGVwZW5kc19vbjoKICAgICAgZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgICAtICd3b3JrZXItZGVwZW5kZW5jeS1jYWNoZTovdG1wL3dpbmRtaWxsL2NhY2hlJwogICAgICAtICd3b3JrZXItbG9nczovdG1wL3dpbmRtaWxsL2xvZ3MnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ2V4aXQgMCcKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIHdpbmRtaWxsLXdvcmtlci0zOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dpbmRtaWxsLWxhYnMvd2luZG1pbGw6bWFpbicKICAgIGVudmlyb25tZW50OgogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9wb3N0Z3JlczokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYi93aW5kbWlsbCcKICAgICAgLSAnTU9ERT0ke01PREU6LXdvcmtlcn0nCiAgICAgIC0gJ1dPUktFUl9HUk9VUD0ke1dPUktFUl9HUk9VUDotZGVmYXVsdH0nCiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJ3dvcmtlci1kZXBlbmRlbmN5LWNhY2hlOi90bXAvd2luZG1pbGwvY2FjaGUnCiAgICAgIC0gJ3dvcmtlci1sb2dzOi90bXAvd2luZG1pbGwvbG9ncycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLW5hdGl2ZToKICAgIGltYWdlOiAnZ2hjci5pby93aW5kbWlsbC1sYWJzL3dpbmRtaWxsOm1haW4nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGdyZXM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAZGIvd2luZG1pbGwnCiAgICAgIC0gJ01PREU9JHtNT0RFOi13b3JrZXJ9JwogICAgICAtICdXT1JLRVJfR1JPVVA9JHtXT1JLRVJfR1JPVVA6LW5hdGl2ZX0nCiAgICAgIC0gJ05VTV9XT1JLRVJTPSR7TlVNX1dPUktFUlM6LTh9JwogICAgICAtICdTTEVFUF9RVUVVRT0ke1NMRUVQX1FVRVVFOi0yMDB9JwogICAgZGVwZW5kc19vbjoKICAgICAgZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3dvcmtlci1sb2dzOi90bXAvd2luZG1pbGwvbG9ncycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgbHNwOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dpbmRtaWxsLWxhYnMvd2luZG1pbGwtbHNwOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xzcC1jYWNoZTovcm9vdC8uY2FjaGUnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ2V4aXQgMCcKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwo=",
|
||||
"compose": "c2VydmljZXM6CiAgZGI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2JwogICAgc2htX3NpemU6IDFnCiAgICB2b2x1bWVzOgogICAgICAtICdkYi1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotd2luZG1pbGwtZGJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgd2luZG1pbGwtc2VydmVyOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dpbmRtaWxsLWxhYnMvd2luZG1pbGw6bWFpbicKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XSU5ETUlMTF84MDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRiLyR7UE9TVEdSRVNfREI6LXdpbmRtaWxsLWRifScKICAgICAgLSBNT0RFPXNlcnZlcgogICAgICAtICdCQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9XSU5ETUlMTH0nCiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnd29ya2VyLWxvZ3M6L3RtcC93aW5kbWlsbC9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwMDAvaGVhbHRoJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLTE6CiAgICBpbWFnZTogJ2doY3IuaW8vd2luZG1pbGwtbGFicy93aW5kbWlsbDptYWluJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRiLyR7UE9TVEdSRVNfREI6LXdpbmRtaWxsLWRifScKICAgICAgLSBNT0RFPXdvcmtlcgogICAgICAtIFdPUktFUl9HUk9VUD1kZWZhdWx0CiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJ3dvcmtlci1kZXBlbmRlbmN5LWNhY2hlOi90bXAvd2luZG1pbGwvY2FjaGUnCiAgICAgIC0gJ3dvcmtlci1sb2dzOi90bXAvd2luZG1pbGwvbG9ncycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLTI6CiAgICBpbWFnZTogJ2doY3IuaW8vd2luZG1pbGwtbGFicy93aW5kbWlsbDptYWluJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRiLyR7UE9TVEdSRVNfREI6LXdpbmRtaWxsLWRifScKICAgICAgLSBNT0RFPXdvcmtlcgogICAgICAtIFdPUktFUl9HUk9VUD1kZWZhdWx0CiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJ3dvcmtlci1kZXBlbmRlbmN5LWNhY2hlOi90bXAvd2luZG1pbGwvY2FjaGUnCiAgICAgIC0gJ3dvcmtlci1sb2dzOi90bXAvd2luZG1pbGwvbG9ncycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLTM6CiAgICBpbWFnZTogJ2doY3IuaW8vd2luZG1pbGwtbGFicy93aW5kbWlsbDptYWluJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRiLyR7UE9TVEdSRVNfREI6LXdpbmRtaWxsLWRifScKICAgICAgLSBNT0RFPXdvcmtlcgogICAgICAtIFdPUktFUl9HUk9VUD1kZWZhdWx0CiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJ3dvcmtlci1kZXBlbmRlbmN5LWNhY2hlOi90bXAvd2luZG1pbGwvY2FjaGUnCiAgICAgIC0gJ3dvcmtlci1sb2dzOi90bXAvd2luZG1pbGwvbG9ncycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLW5hdGl2ZToKICAgIGltYWdlOiAnZ2hjci5pby93aW5kbWlsbC1sYWJzL3dpbmRtaWxsOm1haW4nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AZGIvJHtQT1NUR1JFU19EQjotd2luZG1pbGwtZGJ9JwogICAgICAtIE1PREU9d29ya2VyCiAgICAgIC0gV09SS0VSX0dST1VQPW5hdGl2ZQogICAgICAtIE5VTV9XT1JLRVJTPTgKICAgICAgLSBTTEVFUF9RVUVVRT0yMDAKICAgIGRlcGVuZHNfb246CiAgICAgIGRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICd3b3JrZXItbG9nczovdG1wL3dpbmRtaWxsL2xvZ3MnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ2V4aXQgMCcKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIGxzcDoKICAgIGltYWdlOiAnZ2hjci5pby93aW5kbWlsbC1sYWJzL3dpbmRtaWxsLWxzcDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdsc3AtY2FjaGU6L3Jvb3QvLmNhY2hlJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdleGl0IDAnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAyMHMK",
|
||||
"tags": [
|
||||
"windmill",
|
||||
"workflow",
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
class ExampleTest extends DuskTestCase
|
||||
{
|
||||
/**
|
||||
* A basic browser test example.
|
||||
*/
|
||||
public function testBasicExample(): void
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser->visit('/')
|
||||
->assertSee('Laravel');
|
||||
});
|
||||
}
|
||||
}
|
||||
32
tests/Browser/LoginTest.php
Normal file
32
tests/Browser/LoginTest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\DuskTestCase;
|
||||
use Throwable;
|
||||
|
||||
class LoginTest extends DuskTestCase
|
||||
{
|
||||
/**
|
||||
* A basic test for the login page.
|
||||
* Login with the test user and assert that the user is redirected to the dashboard.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function testLogin()
|
||||
{
|
||||
$email = 'test@example.com';
|
||||
$password = 'password';
|
||||
$this->browse(function (Browser $browser) use ($password, $email) {
|
||||
$browser->visit('/login')
|
||||
->type('email', $email)
|
||||
->type('password', $password)
|
||||
->press('Login')
|
||||
->assertPathIs('/')
|
||||
->screenshot('login');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ abstract class DuskTestCase extends BaseTestCase
|
||||
})->all());
|
||||
|
||||
return RemoteWebDriver::create(
|
||||
$_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515',
|
||||
'http://localhost:4444',
|
||||
DesiredCapabilities::chrome()->setCapability(
|
||||
ChromeOptions::CAPABILITY,
|
||||
$options
|
||||
@@ -50,23 +50,8 @@ abstract class DuskTestCase extends BaseTestCase
|
||||
/**
|
||||
* Determine if the browser window should start maximized.
|
||||
*/
|
||||
protected function shouldStartMaximized(): bool
|
||||
{
|
||||
return isset($_SERVER['DUSK_START_MAXIMIZED']) ||
|
||||
isset($_ENV['DUSK_START_MAXIMIZED']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the Dusk command has disabled headless mode.
|
||||
*/
|
||||
protected function hasHeadlessDisabled(): bool
|
||||
{
|
||||
return isset($_SERVER['DUSK_HEADLESS_DISABLED']) ||
|
||||
isset($_ENV['DUSK_HEADLESS_DISABLED']);
|
||||
}
|
||||
|
||||
protected function baseUrl()
|
||||
{
|
||||
return rtrim(config('app.url'), '/');
|
||||
return 'http://localhost:8000';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.361"
|
||||
"version": "4.0.0-beta.360"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.362"
|
||||
@@ -11,6 +11,9 @@
|
||||
},
|
||||
"realtime": {
|
||||
"version": "1.0.3"
|
||||
},
|
||||
"sentinel": {
|
||||
"version": "next"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user