Merge pull request #3554 from coollabsio/next

Next
This commit is contained in:
peaklabs-dev
2024-09-25 00:00:49 +02:00
committed by GitHub
136 changed files with 1278 additions and 1726 deletions

View File

@@ -19,11 +19,7 @@ DB_PORT=5432
# Set to true to enable Ray
RAY_ENABLED=false
# Set custom ray port
RAY_PORT=
# Clockwork Configuration (remove?)
CLOCKWORK_ENABLED=false
CLOCKWORK_QUEUE_COLLECT=true
# RAY_PORT=
# Enable Laravel Telescope for debugging
TELESCOPE_ENABLED=false

View File

@@ -1 +1,13 @@
> Always use `next` branch as destination branch for PRs, not `main`
## Submit Checklist (REMOVE THIS SECTION BEFORE SUBMITTING)
- [ ] I have selected the `next` branch as the destination for my PR, not `main`.
- [ ] I have listed all changes in the `Changes` section.
- [ ] I have filled out the `Issues` section with the issue/discussion link(s) (if applicable).
- [ ] I have tested my changes.
- [ ] I have considered backwards compatibility.
- [ ] I have removed this checklist and any unused sections.
## Changes
-
## Issues
- fix #

View File

@@ -6,15 +6,19 @@ You can ask for guidance anytime on our [Discord server](https://coollabs.io/dis
## Table of Contents
1. [Setup Development Environment](#1-setup-development-environment)
2. [Verify Installation](#2-verify-installation-optional)
3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
4. [Set up Environment Variables](#4-set-up-environment-variables)
5. [Start Coolify](#5-start-coolify)
6. [Start Development](#6-start-development)
7. [Development Notes](#7-development-notes)
8. [Create a Pull Request](#8-create-a-pull-request)
9. [Additional Contribution Guidelines](#additional-contribution-guidelines)
- [Contributing to Coolify](#contributing-to-coolify)
- [Table of Contents](#table-of-contents)
- [1. Setup Development Environment](#1-setup-development-environment)
- [2. Verify Installation (Optional)](#2-verify-installation-optional)
- [3. Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
- [4. Set up Environment Variables](#4-set-up-environment-variables)
- [5. Start Coolify](#5-start-coolify)
- [6. Start Development](#6-start-development)
- [7. Development Notes](#7-development-notes)
- [8. Create a Pull Request](#8-create-a-pull-request)
- [Additional Contribution Guidelines](#additional-contribution-guidelines)
- [Contributing a New Service](#contributing-a-new-service)
- [Contributing to Documentation](#contributing-to-documentation)
## 1. Setup Development Environment

View File

@@ -2,8 +2,8 @@
namespace App\Actions\Application;
use App\Models\Application;
use App\Actions\Server\CleanupDocker;
use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction;
class StopApplication
@@ -21,6 +21,7 @@ class StopApplication
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
return;
}
@@ -32,10 +33,11 @@ class StopApplication
}
if ($dockerCleanup) {
CleanupDocker::run($server, true);
CleanupDocker::dispatch($server, true);
}
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
}
}

View File

@@ -4,13 +4,13 @@ namespace App\Actions\CoolifyTask;
use App\Enums\ActivityTypes;
use App\Enums\ProcessStatus;
use App\Helpers\SshMultiplexingHelper;
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Server;
use Illuminate\Process\ProcessResult;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process;
use Spatie\Activitylog\Models\Activity;
use App\Helpers\SshMultiplexingHelper;
class RunRemoteProcess
{

View File

@@ -37,7 +37,6 @@ class StartPostgresql
$this->generate_init_scripts();
$this->add_custom_conf();
$docker_compose = [
'services' => [
$container_name => [

View File

@@ -28,7 +28,7 @@ class StopDatabase
$this->stopContainer($database, $database->uuid, 300);
if (! $isDeleteOperation) {
if ($dockerCleanup) {
CleanupDocker::run($server, true);
CleanupDocker::dispatch($server, true);
}
}

View File

@@ -543,7 +543,7 @@ class GetContainersStatus
}
}
}
$exitedServices = $exitedServices->unique('id');
$exitedServices = $exitedServices->unique('uuid');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;

View File

@@ -47,7 +47,8 @@ class StartProxy
"echo 'Pulling docker image.'",
'docker compose pull',
"echo 'Stopping existing coolify-proxy.'",
'docker compose down -v --remove-orphans > /dev/null 2>&1',
'docker stop -t 10 coolify-proxy || true',
'docker rm coolify-proxy || true',
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo 'Proxy started successfully.'",

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Server;
use App\Events\CloudflareTunnelConfigured;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -40,12 +41,17 @@ class ConfigureCloudflared
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
ray($e);
$server->settings->is_cloudflare_tunnel = false;
$server->settings->save();
throw $e;
} finally {
CloudflareTunnelConfigured::dispatch($server->team_id);
$commands = collect([
'rm -fr /tmp/cloudflared',
]);
instant_remote_process($commands, $server);
}
}
}

View File

@@ -2,8 +2,8 @@
namespace App\Actions\Service;
use App\Models\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
class DeleteService
@@ -70,7 +70,7 @@ class DeleteService
$service->forceDelete();
if ($dockerCleanup) {
CleanupDocker::run($server, true);
CleanupDocker::dispatch($server, true);
}
}
}

View File

@@ -2,8 +2,8 @@
namespace App\Actions\Service;
use App\Models\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
class StopService
@@ -24,11 +24,12 @@ class StopService
if (! $isDeleteOperation) {
$service->delete_connected_networks($service->uuid);
if ($dockerCleanup) {
CleanupDocker::run($server, true);
CleanupDocker::dispatch($server, true);
}
}
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
}
}

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class CleanupQueue extends Command
{
protected $signature = 'cleanup:queue';
protected $description = 'Cleanup Queue';
public function handle()
{
echo "Running queue cleanup...\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
foreach ($keys as $key) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
Redis::connection()->del($keyWithoutPrefix);
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class CleanupRedis extends Command
{
protected $signature = 'cleanup:redis';
protected $description = 'Cleanup Redis';
public function handle()
{
echo "Cleanup Redis keys.\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
collect($keys)->each(function ($key) use ($prefix) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
Redis::connection()->del($keyWithoutPrefix);
});
$queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
collect($queueOverlaps)->each(function ($key) {
Redis::connection()->del($key);
});
}
}

View File

@@ -2,10 +2,12 @@
namespace App\Console\Commands;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
@@ -35,6 +37,16 @@ class CleanupStuckedResources extends Command
private function cleanup_stucked_resources()
{
try {
$servers = Server::all()->filter(function ($server) {
return $server->isFunctional();
});
foreach ($servers as $server) {
CleanupHelperContainersJob::dispatch($server);
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
}
try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) {

View File

@@ -5,7 +5,6 @@ namespace App\Console\Commands;
use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\InstanceSettings;
@@ -18,7 +17,7 @@ use Illuminate\Support\Facades\Http;
class Init extends Command
{
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}';
protected $signature = 'app:init {--force-cloud}';
protected $description = 'Cleanup instance related stuffs';
@@ -26,9 +25,63 @@ class Init extends Command
public function handle()
{
if (isCloud() && ! $this->option('force-cloud')) {
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
return;
}
$this->servers = Server::all();
$this->alive();
if (isCloud()) {
} else {
$this->send_alive_signal();
get_public_ips();
}
// Backward compatibility
$this->disable_metrics();
$this->replace_slash_in_environment_name();
$this->restore_coolify_db_backup();
//
$this->update_traefik_labels();
if (! isCloud() || $this->option('force-cloud')) {
$this->cleanup_unused_network_from_coolify_proxy();
}
if (isCloud()) {
$this->cleanup_unnecessary_dynamic_proxy_configuration();
} else {
$this->cleanup_in_progress_application_deployments();
}
$this->call('cleanup:redis');
$this->call('cleanup:stucked-resources');
if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
} else {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
$settings = InstanceSettings::get();
if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
}
}
}
}
private function disable_metrics()
{
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
foreach ($this->servers as $server) {
if ($server->settings->is_metrics_enabled === true) {
@@ -39,62 +92,6 @@ class Init extends Command
}
}
}
$full_cleanup = $this->option('full-cleanup');
$cleanup_deployments = $this->option('cleanup-deployments');
$cleanup_proxy_networks = $this->option('cleanup-proxy-networks');
$this->replace_slash_in_environment_name();
if ($cleanup_deployments) {
echo "Running cleanup deployments.\n";
$this->cleanup_in_progress_application_deployments();
return;
}
if ($cleanup_proxy_networks) {
echo "Running cleanup proxy networks.\n";
$this->cleanup_unused_network_from_coolify_proxy();
return;
}
if ($full_cleanup) {
// Required for falsely deleted coolify db
$this->restore_coolify_db_backup();
$this->update_traefik_labels();
$this->cleanup_unused_network_from_coolify_proxy();
$this->cleanup_unnecessary_dynamic_proxy_configuration();
$this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_helper_containers();
$this->call('cleanup:queue');
$this->call('cleanup:stucked-resources');
if (! isCloud()) {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
}
$settings = InstanceSettings::get();
if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
}
}
if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
}
return;
}
$this->cleanup_stucked_helper_containers();
$this->call('cleanup:stucked-resources');
}
private function update_traefik_labels()
@@ -108,7 +105,6 @@ class Init extends Command
private function cleanup_unnecessary_dynamic_proxy_configuration()
{
if (isCloud()) {
foreach ($this->servers as $server) {
try {
if (! $server->isFunctional()) {
@@ -128,13 +124,9 @@ class Init extends Command
}
}
}
private function cleanup_unused_network_from_coolify_proxy()
{
if (isCloud()) {
return;
}
foreach ($this->servers as $server) {
if (! $server->isFunctional()) {
continue;
@@ -175,6 +167,7 @@ class Init extends Command
private function restore_coolify_db_backup()
{
if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
@@ -197,17 +190,9 @@ class Init extends Command
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
}
}
private function cleanup_stucked_helper_containers()
{
foreach ($this->servers as $server) {
if ($server->isFunctional()) {
CleanupHelperContainersJob::dispatch($server);
}
}
}
private function alive()
private function send_alive_signal()
{
$id = config('app.id');
$version = config('version');
@@ -225,23 +210,7 @@ class Init extends Command
echo "Error in alive: {$e->getMessage()}\n";
}
}
// private function cleanup_ssh()
// {
// TODO: it will cleanup id.root@host.docker.internal
// try {
// $files = Storage::allFiles('ssh/keys');
// foreach ($files as $file) {
// Storage::delete($file);
// }
// $files = Storage::allFiles('ssh/mux');
// foreach ($files as $file) {
// Storage::delete($file);
// }
// } catch (\Throwable $e) {
// echo "Error in cleaning ssh: {$e->getMessage()}\n";
// }
// }
private function cleanup_in_progress_application_deployments()
{
// Cleanup any failed deployments
@@ -263,6 +232,7 @@ class Init extends Command
private function replace_slash_in_environment_name()
{
if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
$environments = Environment::all();
foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) {
@@ -272,3 +242,4 @@ class Init extends Command
}
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CloudflareTunnelConfigured implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@@ -24,28 +24,24 @@ class SshMultiplexingHelper
public static function ensureMultiplexedConnection(Server $server)
{
if (! self::isMultiplexingEnabled()) {
// ray('SSH Multiplexing: DISABLED')->red();
return;
}
// ray('SSH Multiplexing: ENABLED')->green();
// ray('Ensuring multiplexed connection for server:', $server);
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
$sshKeyLocation = $sshConfig['sshKeyLocation'];
self::validateSshKey($sshKeyLocation);
$checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$checkCommand .= "{$server->user}@{$server->ip}";
$process = Process::run($checkCommand);
if ($process->exitCode() !== 0) {
// ray('SSH Multiplexing: Existing connection check failed or not found')->orange();
// ray('Establishing new connection');
self::establishNewMultiplexedConnection($server);
} else {
// ray('SSH Multiplexing: Existing connection is valid')->green();
}
}
@@ -55,37 +51,24 @@ class SshMultiplexingHelper
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
// ray('Establishing new multiplexed connection')->blue();
// ray('SSH Key Location:', $sshKeyLocation);
// ray('Mux Socket:', $muxSocket);
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "
.self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval)
."{$server->user}@{$server->ip}";
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
// ray('Establish Command:', $establishCommand);
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$establishCommand .= "{$server->user}@{$server->ip}";
$establishProcess = Process::run($establishCommand);
// ray('Establish Process Exit Code:', $establishProcess->exitCode());
// ray('Establish Process Output:', $establishProcess->output());
// ray('Establish Process Error Output:', $establishProcess->errorOutput());
if ($establishProcess->exitCode() !== 0) {
// ray('Failed to establish multiplexed connection')->red();
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
}
// ray('Successfully established multiplexed connection')->green();
// Check if the mux socket file was created
if (! file_exists($muxSocket)) {
// ray('Mux socket file not found after connection establishment')->orange();
}
}
public static function removeMuxFile(Server $server)
@@ -93,20 +76,12 @@ class SshMultiplexingHelper
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
$process = Process::run($closeCommand);
// ray('Closing multiplexed connection')->blue();
// ray('Close command:', $closeCommand);
// ray('Close process exit code:', $process->exitCode());
// ray('Close process output:', $process->output());
// ray('Close process error output:', $process->errorOutput());
if ($process->exitCode() !== 0) {
// ray('Failed to close multiplexed connection')->orange();
} else {
// ray('Successfully closed multiplexed connection')->green();
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$closeCommand .= "{$server->user}@{$server->ip}";
Process::run($closeCommand);
}
public static function generateScpCommand(Server $server, string $source, string $dest)
@@ -116,16 +91,18 @@ class SshMultiplexingHelper
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp ";
if (self::isMultiplexingEnabled()) {
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
}
self::addCloudflareProxyCommand($scp_command, $server);
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
@@ -144,16 +121,18 @@ class SshMultiplexingHelper
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$ssh_command = "timeout $timeout ssh ";
if (self::isMultiplexingEnabled()) {
$muxPersistTime = config('constants.ssh.mux_persist_time');
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
}
self::addCloudflareProxyCommand($ssh_command, $server);
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
}
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
@@ -183,13 +162,6 @@ class SshMultiplexingHelper
}
}
private static function addCloudflareProxyCommand(string &$command, Server $server): void
{
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
}
private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
{
$options = "-i {$sshKeyLocation} "

View File

@@ -2529,6 +2529,131 @@ class ApplicationsController extends Controller
}
#[OA\Post(
summary: 'Execute Command',
description: "Execute a command on the application's current container.",
path: '/applications/{uuid}/execute',
operationId: 'execute-command-application',
security: [
['bearerAuth' => []],
],
tags: ['Applications'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Command to execute.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
],
),
),
),
responses: [
new OA\Response(
response: 200,
description: "Execute a command on the application's current container.",
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Command executed.'],
'response' => ['type' => 'string'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function execute_command_by_uuid(Request $request)
{
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
$allowedFields = ['command'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'command' => 'string|required',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
$status = getContainerStatus($application->destination->server, $container['Names']);
if ($status !== 'running') {
return response()->json([
'message' => 'Application is not running.',
], 400);
}
$commands = collect([
executeInDocker($container['Names'], $request->command),
]);
$res = instant_remote_process(command: $commands, server: $application->destination->server);
return response()->json([
'message' => 'Command executed.',
'response' => $res,
]);
}
private function validateDataApplications(Request $request, Server $server)
{
$teamId = getTeamIdFromToken();

View File

@@ -27,9 +27,9 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Process;
use RuntimeException;
use Symfony\Component\Yaml\Yaml;
use Throwable;

View File

@@ -9,8 +9,8 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
{

View File

@@ -3,6 +3,7 @@
namespace App\Jobs;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -10,7 +11,6 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Carbon\Carbon;
class CleanupStaleMultiplexedConnections implements ShouldQueue
{
@@ -32,6 +32,7 @@ class CleanupStaleMultiplexedConnections implements ShouldQueue
if (! $server) {
$this->removeMultiplexFile($muxFile);
continue;
}

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -25,16 +24,6 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->uuid;
}
public function handle()
{
GetContainersStatus::run($this->server);

View File

@@ -23,7 +23,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
@@ -80,16 +79,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
public function middleware(): array
{
return [new WithoutOverlapping($this->backup->id)];
}
public function uniqueId(): int
{
return $this->backup->id;
}
public function handle(): void
{
try {

View File

@@ -1,62 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Team;
use App\Notifications\Database\DailyBackup;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DatabaseBackupStatusJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public function __construct() {}
public function handle()
{
// $teams = Team::all();
// foreach ($teams as $team) {
// $scheduled_backups = $team->scheduledDatabaseBackups()->get();
// if ($scheduled_backups->isEmpty()) {
// continue;
// }
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// }
// }
// $scheduled_backups = ScheduledDatabaseBackup::all();
// $databases = collect();
// $teams = collect();
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// $database = $scheduled_backup->database;
// $team = $database->team();
// $teams->put($team->id, $team);
// $databases->put("{$team->id}:{$database->name}", [
// 'failed_count' => $failed->count(),
// ]);
// }
// foreach ($databases as $name => $database) {
// [$team_id, $name] = explode(':', $name);
// $team = $teams->get($team_id);
// $team?->notify(new DailyBackup($databases));
// }
}
}

View File

@@ -80,7 +80,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|| $this->resource instanceof StandaloneClickhouse;
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
if (($this->dockerCleanup || $isDatabase) && $server) {
CleanupDocker::run($server, true);
CleanupDocker::dispatch($server, true);
}
if ($this->deleteConnectedNetworks && ! $isDatabase) {
@@ -92,7 +92,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
} finally {
$this->resource->forceDelete();
if ($this->dockerCleanup) {
CleanupDocker::run($server, true);
CleanupDocker::dispatch($server, true);
}
Artisan::queue('cleanup:stucked-resources');
}

View File

@@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
@@ -26,16 +25,6 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
public function middleware(): array
{
return [new WithoutOverlapping($this->server->id)];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function handle(): void
{
try {

View File

@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
@@ -25,16 +24,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public GithubApp $github_app) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->github_app->uuid))];
}
public function uniqueId(): int
{
return $this->github_app->uuid;
}
public function handle()
{
try {

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
@@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server) {}
public function handle(): void

View File

@@ -13,7 +13,6 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ScheduledTaskJob implements ShouldQueue
@@ -56,24 +55,17 @@ class ScheduledTaskJob implements ShouldQueue
{
if ($this->resource instanceof Application) {
$timezone = $this->resource->destination->server->settings->server_timezone;
return $timezone;
} elseif ($this->resource instanceof Service) {
$timezone = $this->resource->server->settings->server_timezone;
return $timezone;
}
return 'UTC';
}
public function middleware(): array
{
return [new WithoutOverlapping($this->task->id)];
}
public function uniqueId(): int
{
return $this->task->id;
}
public function handle(): void
{

View File

@@ -16,7 +16,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
@@ -24,7 +23,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $tries = 1;
public $timeout = 60;
@@ -45,16 +44,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function handle()
{
try {

View File

@@ -10,7 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\Middleware\;
use Illuminate\Queue\SerializesModels;
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +26,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Team $team) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->team->uuid))];
}
public function uniqueId(): int
{
return $this->team->uuid;
}
public function handle()
{
try {

View File

@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +25,6 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->uuid;
}
public function handle()
{
if (! $this->server->isServerReady($this->tries)) {

View File

@@ -1,6 +1,7 @@
<?php
namespace App\Livewire\Destination;
use Livewire\Component;
class Form extends Component

View File

@@ -2,10 +2,10 @@
namespace App\Livewire;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class NavbarDeleteTeam extends Component
{
@@ -20,6 +20,7 @@ class NavbarDeleteTeam extends Component
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application\Deployment;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component

View File

@@ -5,12 +5,12 @@ namespace App\Livewire\Project\Application;
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Process;
class Previews extends Component
{

View File

@@ -3,10 +3,10 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Spatie\Url\Url;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
class BackupEdit extends Component
{
@@ -15,7 +15,9 @@ class BackupEdit extends Component
public $s3s;
public bool $delete_associated_backups_locally = false;
public bool $delete_associated_backups_s3 = false;
public bool $delete_associated_backups_sftp = false;
public ?string $status = null;
@@ -56,6 +58,7 @@ class BackupEdit extends Component
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
@@ -182,7 +185,7 @@ class BackupEdit extends Component
['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
]
],
]);
}
}

View File

@@ -3,19 +3,23 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Livewire\Component;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\On;
use Livewire\Component;
class BackupExecutions extends Component
{
public ?ScheduledDatabaseBackup $backup = null;
public $database;
public $executions = [];
public $setDeletableBackup;
public $delete_backup_s3 = true;
public $delete_backup_sftp = true;
public function getListeners()
@@ -42,12 +46,14 @@ class BackupExecutions extends Component
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$execution = $this->backup->executions()->where('id', $executionId)->first();
if (is_null($execution)) {
$this->dispatch('error', 'Backup execution not found.');
return;
}
@@ -103,6 +109,7 @@ class BackupExecutions extends Component
return $server;
}
}
return null;
}
@@ -113,6 +120,7 @@ class BackupExecutions extends Component
return 'UTC';
}
$serverTimezone = $server->settings->server_timezone;
return $serverTimezone;
}
@@ -125,6 +133,7 @@ class BackupExecutions extends Component
} catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
@@ -134,7 +143,7 @@ class BackupExecutions extends Component
'checkboxes' => [
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
]
],
]);
}
}

View File

@@ -14,9 +14,9 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Livewire\Component;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class FileStorage extends Component
{
@@ -89,6 +89,7 @@ class FileStorage extends Component
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
@@ -142,7 +143,7 @@ class FileStorage extends Component
],
'fileDeletionCheckboxes' => [
['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
]
],
]);
}
}

View File

@@ -33,7 +33,7 @@ class StackForm extends Component
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword');
$isPassword = data_get($field, 'isPassword', false);
$this->fields->put($key, [
'serviceName' => $serviceName,
'key' => $key,
@@ -47,7 +47,15 @@ class StackForm extends Component
$this->validationAttributes["fields.$key.value"] = $fieldKey;
}
}
$this->fields = $this->fields->sortBy('name');
$this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
return $group->sortBy(function ($field) {
return data_get($field, 'isPassword') ? 1 : 0;
})->mapWithKeys(function ($field) {
return [$field['key'] => $field];
});
})->flatMap(function ($group) {
return $group;
});
}
public function saveCompose($raw)

View File

@@ -8,10 +8,10 @@ use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
class Destination extends Component
{
@@ -121,6 +121,7 @@ class Destination extends Component
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Shared;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
@@ -17,7 +18,6 @@ use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
use App\Helpers\SshMultiplexingHelper;
class GetLogs extends Component
{

View File

@@ -7,7 +7,9 @@ use Livewire\Component;
class Executions extends Component
{
public $executions = [];
public $selectedKey;
public $task;
public function getListeners()
@@ -42,6 +44,7 @@ class Executions extends Component
return $this->task->service->destination->server;
}
}
return null;
}
@@ -52,6 +55,7 @@ class Executions extends Component
return 'UTC';
}
$serverTimezone = $server->settings->server_timezone;
return $serverTimezone;
}
@@ -64,6 +68,7 @@ class Executions extends Component
} catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
}

View File

@@ -3,8 +3,8 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Show extends Component
@@ -42,6 +42,7 @@ class Show extends Component
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}

View File

@@ -8,9 +8,13 @@ use Livewire\Component;
class Create extends Component
{
public string $name = '';
public string $value = '';
public ?string $from = null;
public ?string $description = null;
public ?string $publicKey = null;
protected $rules = [

View File

@@ -2,8 +2,8 @@
namespace App\Livewire\Security\PrivateKey;
use Livewire\Component;
use App\Models\PrivateKey;
use Livewire\Component;
class Index extends Component
{

View File

@@ -31,13 +31,12 @@ class ConfigureCloudflareTunnels extends Component
{
try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::run($server, $this->cloudflare_token);
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain;
$server->save();
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
$this->dispatch('refreshServerShow');
$this->dispatch('warning', 'Cloudflare Tunnels configuration started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -3,9 +3,9 @@
namespace App\Livewire\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Delete extends Component
{
@@ -17,6 +17,7 @@ class Delete extends Component
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try {

View File

@@ -24,11 +24,16 @@ class Form extends Component
public $timezones;
protected $listeners = [
'serverInstalled',
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured',
'refreshServerShow' => 'serverInstalled',
'revalidate' => '$refresh',
];
}
protected $rules = [
'server.name' => 'required',
@@ -96,6 +101,12 @@ class Form extends Component
}
}
public function cloudflareTunnelConfigured()
{
$this->serverInstalled();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
}
public function serverInstalled()
{
$this->server->refresh();
@@ -238,4 +249,12 @@ class Form extends Component
$this->server->settings->save();
$this->dispatch('success', 'Server timezone updated.');
}
public function manualCloudflareConfig()
{
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
}

View File

@@ -6,9 +6,9 @@ use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Livewire\Component;
use Illuminate\Support\Facades\Process;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
class Deploy extends Component
{

View File

@@ -4,9 +4,9 @@ namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\User;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class AdminView extends Component
{
@@ -79,6 +79,7 @@ class AdminView extends Component
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if (! auth()->user()->isInstanceAdmin()) {

View File

@@ -6,10 +6,10 @@ use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Process;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use RuntimeException;
use Spatie\Activitylog\Models\Activity;
@@ -237,7 +237,6 @@ class Application extends BaseModel
instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
public function additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
@@ -1096,6 +1095,7 @@ class Application extends BaseModel
throw new \Exception($e->getMessage());
}
$services = data_get($yaml, 'services');
$commands = collect([]);
$services = collect($services)->map(function ($service) use ($commands) {
$serviceVolumes = collect(data_get($service, 'volumes', []));

View File

@@ -35,14 +35,17 @@ class ScheduledDatabaseBackup extends BaseModel
{
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
}
public function server()
{
if ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
return $server;
}
}
return null;
}
}

View File

@@ -4,8 +4,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use App\Models\Service;
use App\Models\Application;
class ScheduledTask extends BaseModel
{
@@ -37,19 +35,23 @@ class ScheduledTask extends BaseModel
if ($this->application) {
if ($this->application->destination && $this->application->destination->server) {
$server = $this->application->destination->server;
return $server;
}
} elseif ($this->service) {
if ($this->service->destination && $this->service->destination->server) {
$server = $this->service->destination->server;
return $server;
}
} elseif ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
return $server;
}
}
return null;
}
}

View File

@@ -165,14 +165,13 @@ class Server extends BaseModel
$dynamic_conf_path = $this->proxyPath().'/dynamic';
$proxy_type = $this->proxyType();
$redirect_url = $this->proxy->redirect_url;
ray($proxy_type);
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
} elseif ($proxy_type === 'CADDY') {
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
}
if (empty($redirect_url)) {
if ($proxy_type === 'CADDY') {
if ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ':80, :443 {
respond 404
}';
@@ -242,7 +241,7 @@ respond 404
$conf;
$base64 = base64_encode($conf);
} elseif ($proxy_type === 'CADDY') {
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ":80, :443 {
redir $redirect_url
}";
@@ -258,9 +257,6 @@ respond 404
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
if (config('app.env') == 'local') {
ray($conf);
}
if ($proxy_type === 'CADDY') {
$this->reloadCaddy();
}
@@ -884,6 +880,35 @@ $schema://$host {
return $this->hasMany(Service::class);
}
public function port(): Attribute
{
return Attribute::make(
get: function ($value) {
return preg_replace('/[^0-9]/', '', $value);
}
);
}
public function user(): Attribute
{
return Attribute::make(
get: function ($value) {
$sanitizedValue = preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
return $sanitizedValue;
}
);
}
public function ip(): Attribute
{
return Attribute::make(
get: function ($value) {
return preg_replace('/[^0-9a-zA-Z.:%-]/', '', $value);
}
);
}
public function getIp(): Attribute
{
return Attribute::make(

View File

@@ -6,9 +6,9 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Storage;
use OpenApi\Attributes as OA;
use Spatie\Url\Url;
@@ -144,6 +144,7 @@ class Service extends BaseModel
foreach ($dbs as $db) {
$containersToStop[] = "{$db->name}-{$this->uuid}";
}
return $containersToStop;
}

View File

@@ -52,7 +52,7 @@ class ForceDisabled extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).";
$message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).";
return $message;
}
@@ -60,7 +60,7 @@ class ForceDisabled extends Notification implements ShouldQueue
public function toTelegram(): array
{
return [
'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).",
'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).",
];
}
}

View File

@@ -3,11 +3,11 @@
namespace App\Traits;
use App\Enums\ApplicationDeploymentStatus;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use App\Helpers\SshMultiplexingHelper;
trait ExecuteRemoteCommand
{

View File

@@ -134,6 +134,9 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
return 'exited';
}
$container = format_docker_command_output_to_json($container);
if ($container->isEmpty()) {
return 'exited';
}
if ($all_data) {
return $container[0];
}

View File

@@ -1247,6 +1247,10 @@ function get_public_ips()
}
$settings->update(['public_ipv4' => $ipv4]);
}
} catch (\Exception $e) {
echo "Error: {$e->getMessage()}\n";
}
try {
$ipv6 = $second->output();
if ($ipv6) {
$ipv6 = trim($ipv6);
@@ -2924,10 +2928,11 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}
$parsedServices = collect([]);
ray()->clearAll();
// ray()->clearAll();
$allMagicEnvironments = collect([]);
foreach ($services as $serviceName => $service) {
$predefinedPort = null;
$magicEnvironments = collect([]);
$image = data_get_str($service, 'image');
$environment = collect(data_get($service, 'environment', []));
@@ -2936,6 +2941,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$isDatabase = isDatabaseImage(data_get_str($service, 'image'));
if ($isService) {
$containerName = "$serviceName-{$resource->uuid}";
if ($serviceName === 'registry') {
$tempServiceName = 'docker-registry';
} else {
$tempServiceName = $serviceName;
}
if (str(data_get($service, 'image'))->contains('glitchtip')) {
$tempServiceName = 'glitchtip';
}
if ($serviceName === 'supabase-kong') {
$tempServiceName = 'supabase';
}
$serviceDefinition = data_get($allServices, $tempServiceName);
$predefinedPort = data_get($serviceDefinition, 'port');
if ($serviceName === 'plausible') {
$predefinedPort = '8000';
}
if ($isDatabase) {
$savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName,
@@ -2987,8 +3010,10 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
// SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
if (substr_count(str($key)->value(), '_') === 3) {
$fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
$port = $key->afterLast('_')->value();
} else {
$fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
$port = null;
}
if ($isApplication) {
$fqdn = generateFqdn($server, "{$resource->name}-$uuid");
@@ -2999,19 +3024,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
}
}
if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) {
$path = $value->value();
if ($path !== '/') {
$fqdn = "$fqdn$path";
}
}
$fqdnWithPort = $fqdn;
if ($port) {
$fqdnWithPort = "$fqdn:$port";
}
if ($isApplication && is_null($resource->fqdn)) {
data_forget($resource, 'environment_variables');
data_forget($resource, 'environment_variables_preview');
$resource->fqdn = $fqdn;
$resource->fqdn = $fqdnWithPort;
$resource->save();
} elseif ($isService && is_null($savedService->fqdn)) {
$savedService->fqdn = $fqdn;
$savedService->fqdn = $fqdnWithPort;
$savedService->save();
}
@@ -3040,7 +3070,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}
$allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments);
if ($magicEnvironments->count() > 0) {
foreach ($magicEnvironments as $key => $value) {
$key = str($key);
@@ -3455,6 +3484,18 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$value = $value->after('?');
}
if ($originalValue->value() === $value->value()) {
// This means the variable does not have a default value, so it needs to be created in Coolify
$parsedKeyValue = replaceVariables($value);
$resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->firstOrCreate([
'key' => $parsedKeyValue,
$nameOfId => $resource->id,
], [
'is_build_time' => false,
'is_preview' => false,
]);
// Add the variable to the environment so it will be shown in the deployable compose file
$environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value;
continue;
}
$resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([
@@ -3547,6 +3588,17 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($environment->count() > 0) {
$environment = $environment->filter(function ($value, $key) {
return ! str($key)->startsWith('SERVICE_FQDN_');
})->map(function ($value, $key) use ($resource) {
// if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
if (str($value)->isEmpty()) {
if ($resource->environment_variables()->where('key', $key)->exists()) {
$value = $resource->environment_variables()->where('key', $key)->first()->value;
} else {
$value = null;
}
}
return $value;
});
}
$serviceLabels = $labels->merge($defaultLabels);
@@ -3631,6 +3683,14 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
data_forget($service, 'volumes.*.is_directory');
data_forget($service, 'exclude_from_hc');
$volumesParsed = $volumesParsed->map(function ($volume) {
data_forget($volume, 'content');
data_forget($volume, 'is_directory');
data_forget($volume, 'isDirectory');
return $volume;
});
$payload = collect($service)->merge([
'container_name' => $containerName,
'restart' => $restart->value(),
@@ -3661,6 +3721,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$parsedServices->put($serviceName, $payload);
}
$topLevel->put('services', $parsedServices);
$customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets'];
$topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) {

View File

@@ -84,7 +84,11 @@
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"Illuminate\\Foundation\\ComposerScripts::postUpdate"
],
"post-install-cmd": [],
"post-install-cmd": [
"cp -r 'hooks/' '.git/hooks/'",
"php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"",
"php -r \"chmod('.git/hooks/pre-commit', 0777);\""
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],

View File

@@ -1,424 +0,0 @@
<?php
return [
/*
|------------------------------------------------------------------------------------------------------------------
| Enable Clockwork
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork is enabled by default only when your application is in debug mode. Here you can explicitly enable or
| disable Clockwork. When disabled, no data is collected and the api and web ui are inactive.
| Unless explicitly enabled, Clockwork only runs on localhost, *.local, *.test and *.wip domains.
|
*/
'enable' => env('CLOCKWORK_ENABLE', null),
/*
|------------------------------------------------------------------------------------------------------------------
| Features
|------------------------------------------------------------------------------------------------------------------
|
| You can enable or disable various Clockwork features here. Some features have additional settings (eg. slow query
| threshold for database queries).
|
*/
'features' => [
// Cache usage stats and cache queries including results
'cache' => [
'enabled' => env('CLOCKWORK_CACHE_ENABLED', true),
// Collect cache queries
'collect_queries' => env('CLOCKWORK_CACHE_QUERIES', true),
// Collect values from cache queries (high performance impact with a very high number of queries)
'collect_values' => env('CLOCKWORK_CACHE_COLLECT_VALUES', false)
],
// Database usage stats and queries
'database' => [
'enabled' => env('CLOCKWORK_DATABASE_ENABLED', true),
// Collect database queries (high performance impact with a very high number of queries)
'collect_queries' => env('CLOCKWORK_DATABASE_COLLECT_QUERIES', true),
// Collect details of models updates (high performance impact with a lot of model updates)
'collect_models_actions' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_ACTIONS', true),
// Collect details of retrieved models (very high performance impact with a lot of models retrieved)
'collect_models_retrieved' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_RETRIEVED', false),
// Query execution time threshold in milliseconds after which the query will be marked as slow
'slow_threshold' => env('CLOCKWORK_DATABASE_SLOW_THRESHOLD'),
// Collect only slow database queries
'slow_only' => env('CLOCKWORK_DATABASE_SLOW_ONLY', false),
// Detect and report duplicate queries
'detect_duplicate_queries' => env('CLOCKWORK_DATABASE_DETECT_DUPLICATE_QUERIES', false)
],
// Dispatched events
'events' => [
'enabled' => env('CLOCKWORK_EVENTS_ENABLED', true),
// Ignored events (framework events are ignored by default)
'ignored_events' => [
// App\Events\UserRegistered::class,
// 'user.registered'
],
],
// Laravel log (you can still log directly to Clockwork with laravel log disabled)
'log' => [
'enabled' => env('CLOCKWORK_LOG_ENABLED', true)
],
// Sent notifications
'notifications' => [
'enabled' => env('CLOCKWORK_NOTIFICATIONS_ENABLED', true),
],
// Performance metrics
'performance' => [
// Allow collecting of client metrics. Requires separate clockwork-browser npm package.
'client_metrics' => env('CLOCKWORK_PERFORMANCE_CLIENT_METRICS', true)
],
// Dispatched queue jobs
'queue' => [
'enabled' => env('CLOCKWORK_QUEUE_ENABLED', true)
],
// Redis commands
'redis' => [
'enabled' => env('CLOCKWORK_REDIS_ENABLED', true)
],
// Routes list
'routes' => [
'enabled' => env('CLOCKWORK_ROUTES_ENABLED', false),
// Collect only routes from particular namespaces (only application routes by default)
'only_namespaces' => [ 'App' ]
],
// Rendered views
'views' => [
'enabled' => env('CLOCKWORK_VIEWS_ENABLED', true),
// Collect views including view data (high performance impact with a high number of views)
'collect_data' => env('CLOCKWORK_VIEWS_COLLECT_DATA', false),
// Use Twig profiler instead of Laravel events for apps using laravel-twigbridge (more precise, but does
// not support collecting view data)
'use_twig_profiler' => env('CLOCKWORK_VIEWS_USE_TWIG_PROFILER', false)
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Enable web UI
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork comes with a web UI accessible via http://your.app/clockwork. Here you can enable or disable this
| feature. You can also set a custom path for the web UI.
|
*/
'web' => env('CLOCKWORK_WEB', true),
/*
|------------------------------------------------------------------------------------------------------------------
| Enable toolbar
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can show a toolbar with basic metrics on all responses. Here you can enable or disable this feature.
| Requires a separate clockwork-browser npm library.
| For installation instructions see https://underground.works/clockwork/#docs-viewing-data
|
*/
'toolbar' => env('CLOCKWORK_TOOLBAR', true),
/*
|------------------------------------------------------------------------------------------------------------------
| HTTP requests collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork collects data about HTTP requests to your app. Here you can choose which requests should be collected.
|
*/
'requests' => [
// With on-demand mode enabled, Clockwork will only profile requests when the browser extension is open or you
// manually pass a "clockwork-profile" cookie or get/post data key.
// Optionally you can specify a "secret" that has to be passed as the value to enable profiling.
'on_demand' => env('CLOCKWORK_REQUESTS_ON_DEMAND', false),
// Collect only errors (requests with HTTP 4xx and 5xx responses)
'errors_only' => env('CLOCKWORK_REQUESTS_ERRORS_ONLY', false),
// Response time threshold in milliseconds after which the request will be marked as slow
'slow_threshold' => env('CLOCKWORK_REQUESTS_SLOW_THRESHOLD'),
// Collect only slow requests
'slow_only' => env('CLOCKWORK_REQUESTS_SLOW_ONLY', false),
// Sample the collected requests (e.g. set to 100 to collect only 1 in 100 requests)
'sample' => env('CLOCKWORK_REQUESTS_SAMPLE', false),
// List of URIs that should not be collected
'except' => [
'/horizon/.*', // Laravel Horizon requests
'/telescope/.*', // Laravel Telescope requests
'/_tt/.*', // Laravel Telescope toolbar
'/_debugbar/.*', // Laravel DebugBar requests
],
// List of URIs that should be collected, any other URI will not be collected if not empty
'only' => [
// '/api/.*'
],
// Don't collect OPTIONS requests, mostly used in the CSRF pre-flight requests and are rarely of interest
'except_preflight' => env('CLOCKWORK_REQUESTS_EXCEPT_PREFLIGHT', true)
],
/*
|------------------------------------------------------------------------------------------------------------------
| Artisan commands collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed artisan commands. Here you can enable and configure which commands
| should be collected.
|
*/
'artisan' => [
// Enable or disable collection of executed Artisan commands
'collect' => env('CLOCKWORK_ARTISAN_COLLECT', false),
// List of commands that should not be collected (built-in commands are not collected by default)
'except' => [
// 'inspire'
],
// List of commands that should be collected, any other command will not be collected if not empty
'only' => [
// 'inspire'
],
// Enable or disable collection of command output
'collect_output' => env('CLOCKWORK_ARTISAN_COLLECT_OUTPUT', false),
// Enable or disable collection of built-in Laravel commands
'except_laravel_commands' => env('CLOCKWORK_ARTISAN_EXCEPT_LARAVEL_COMMANDS', true)
],
/*
|------------------------------------------------------------------------------------------------------------------
| Queue jobs collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed queue jobs. Here you can enable and configure which queue jobs should
| be collected.
|
*/
'queue' => [
// Enable or disable collection of executed queue jobs
'collect' => env('CLOCKWORK_QUEUE_COLLECT', false),
// List of queue jobs that should not be collected
'except' => [
// App\Jobs\ExpensiveJob::class
],
// List of queue jobs that should be collected, any other queue job will not be collected if not empty
'only' => [
// App\Jobs\BuggyJob::class
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Tests collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed tests. Here you can enable and configure which tests should be
| collected.
|
*/
'tests' => [
// Enable or disable collection of ran tests
'collect' => env('CLOCKWORK_TESTS_COLLECT', false),
// List of tests that should not be collected
'except' => [
// Tests\Unit\ExampleTest::class
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Enable data collection when Clockwork is disabled
|------------------------------------------------------------------------------------------------------------------
|
| You can enable this setting to collect data even when Clockwork is disabled, e.g. for future analysis.
|
*/
'collect_data_always' => env('CLOCKWORK_COLLECT_DATA_ALWAYS', false),
/*
|------------------------------------------------------------------------------------------------------------------
| Metadata storage
|------------------------------------------------------------------------------------------------------------------
|
| Configure how is the metadata collected by Clockwork stored. Three options are available:
| - files - A simple fast storage implementation storing data in one-per-request files.
| - sql - Stores requests in a sql database. Supports MySQL, PostgreSQL and SQLite. Requires PDO.
| - redis - Stores requests in redis. Requires phpredis.
*/
'storage' => env('CLOCKWORK_STORAGE', 'files'),
// Path where the Clockwork metadata is stored
'storage_files_path' => env('CLOCKWORK_STORAGE_FILES_PATH', storage_path('clockwork')),
// Compress the metadata files using gzip, trading a little bit of performance for lower disk usage
'storage_files_compress' => env('CLOCKWORK_STORAGE_FILES_COMPRESS', false),
// SQL database to use, can be a name of database configured in database.php or a path to a SQLite file
'storage_sql_database' => env('CLOCKWORK_STORAGE_SQL_DATABASE', storage_path('clockwork.sqlite')),
// SQL table name to use, the table is automatically created and updated when needed
'storage_sql_table' => env('CLOCKWORK_STORAGE_SQL_TABLE', 'clockwork'),
// Redis connection, name of redis connection or cluster configured in database.php
'storage_redis' => env('CLOCKWORK_STORAGE_REDIS', 'default'),
// Redis prefix for Clockwork keys ("clockwork" if not set)
'storage_redis_prefix' => env('CLOCKWORK_STORAGE_REDIS_PREFIX', 'clockwork'),
// Maximum lifetime of collected metadata in minutes, older requests will automatically be deleted, false to disable
'storage_expiration' => env('CLOCKWORK_STORAGE_EXPIRATION', 60 * 24 * 7),
/*
|------------------------------------------------------------------------------------------------------------------
| Authentication
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can be configured to require authentication before allowing access to the collected data. This might be
| useful when the application is publicly accessible. Setting to true will enable a simple authentication with a
| pre-configured password. You can also pass a class name of a custom implementation.
|
*/
'authentication' => env('CLOCKWORK_AUTHENTICATION', false),
// Password for the simple authentication
'authentication_password' => env('CLOCKWORK_AUTHENTICATION_PASSWORD', 'VerySecretPassword'),
/*
|------------------------------------------------------------------------------------------------------------------
| Stack traces collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect stack traces for log messages and certain data like database queries. Here you can set
| whether to collect stack traces, limit the number of collected frames and set further configuration. Collecting
| long stack traces considerably increases metadata size.
|
*/
'stack_traces' => [
// Enable or disable collecting of stack traces
'enabled' => env('CLOCKWORK_STACK_TRACES_ENABLED', true),
// Limit the number of frames to be collected
'limit' => env('CLOCKWORK_STACK_TRACES_LIMIT', 10),
// List of vendor names to skip when determining caller, common vendors are automatically added
'skip_vendors' => [
// 'phpunit'
],
// List of namespaces to skip when determining caller
'skip_namespaces' => [
// 'Laravel'
],
// List of class names to skip when determining caller
'skip_classes' => [
// App\CustomLog::class
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Serialization
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork serializes the collected data to json for storage and transfer. Here you can configure certain aspects
| of serialization. Serialization has a large effect on the cpu time and memory usage.
|
*/
// Maximum depth of serialized multi-level arrays and objects
'serialization_depth' => env('CLOCKWORK_SERIALIZATION_DEPTH', 10),
// A list of classes that will never be serialized (e.g. a common service container class)
'serialization_blackbox' => [
\Illuminate\Container\Container::class,
\Illuminate\Foundation\Application::class,
\Laravel\Lumen\Application::class
],
/*
|------------------------------------------------------------------------------------------------------------------
| Register helpers
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork comes with a "clock" global helper function. You can use this helper to quickly log something and to
| access the Clockwork instance.
|
*/
'register_helpers' => env('CLOCKWORK_REGISTER_HELPERS', true),
/*
|------------------------------------------------------------------------------------------------------------------
| Send headers for AJAX request
|------------------------------------------------------------------------------------------------------------------
|
| When trying to collect data, the AJAX method can sometimes fail if it is missing required headers. For example, an
| API might require a version number using Accept headers to route the HTTP request to the correct codebase.
|
*/
'headers' => [
// 'Accept' => 'application/vnd.com.whatever.v1+json',
],
/*
|------------------------------------------------------------------------------------------------------------------
| Server timing
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork supports the W3C Server Timing specification, which allows for collecting a simple performance metrics
| in a cross-browser way. E.g. in Chrome, your app, database and timeline event timings will be shown in the Dev
| Tools network tab. This setting specifies the max number of timeline events that will be sent. Setting to false
| will disable the feature.
|
*/
'server_timing' => env('CLOCKWORK_SERVER_TIMING', 10)
];

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.342',
'release' => '4.0.0-beta.343',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.342';
return '4.0.0-beta.343';

View File

@@ -2,8 +2,8 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class UpdateServerSettingsDefaultTimezone extends Migration
{

View File

@@ -1,13 +1,14 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
class EncryptExistingPrivateKeys extends Migration
{
public function up()
{
try {
DB::table('private_keys')->chunkById(100, function ($keys) {
foreach ($keys as $key) {
DB::table('private_keys')
@@ -15,5 +16,10 @@ class EncryptExistingPrivateKeys extends Migration
->update(['private_key' => Crypt::encryptString($key->private_key)]);
}
});
} catch (\Exception $e) {
echo 'Encrypting private keys failed.';
echo $e->getMessage();
}
}
}

View File

@@ -1,25 +0,0 @@
<?php
use App\Models\PrivateKey;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Storage;
class PopulateSshKeysAndClearMuxDirectory extends Migration
{
public function up()
{
// Storage::disk('ssh-keys')->deleteDirectory('');
// Storage::disk('ssh-keys')->makeDirectory('');
// Storage::disk('ssh-mux')->deleteDirectory('');
// Storage::disk('ssh-mux')->makeDirectory('');
// PrivateKey::chunk(100, function ($keys) {
// foreach ($keys as $key) {
// $key->storeInFileSystem();
// if ($key->id === 0) {
// Storage::disk('ssh-keys')->put('id.root@host.docker.internal', $key->private_key);
// }
// }
// });
}
}

View File

@@ -3,6 +3,7 @@
use App\Models\PrivateKey;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class AddSshKeyFingerprintToPrivateKeysTable extends Migration
@@ -13,13 +14,20 @@ class AddSshKeyFingerprintToPrivateKeysTable extends Migration
$table->string('fingerprint')->after('private_key')->nullable();
});
PrivateKey::whereNull('fingerprint')->each(function ($key) {
try {
DB::table('private_keys')->chunkById(100, function ($keys) {
foreach ($keys as $key) {
$fingerprint = PrivateKey::generateFingerprint($key->private_key);
if ($fingerprint) {
$key->fingerprint = $fingerprint;
$key->save();
}
}
});
} catch (\Exception $e) {
echo 'Generating fingerprints failed.';
echo $e->getMessage();
}
}
public function down()

View File

@@ -11,6 +11,7 @@ class PopulateSshKeysDirectorySeeder extends Seeder
{
public function run()
{
try {
Storage::disk('ssh-keys')->deleteDirectory('');
Storage::disk('ssh-keys')->makeDirectory('');
Storage::disk('ssh-mux')->deleteDirectory('');
@@ -23,7 +24,17 @@ class PopulateSshKeysDirectorySeeder extends Seeder
}
});
Process::run('chown -R 9999:9999 '.storage_path('app/ssh/keys'));
Process::run('chown -R 9999:9999 '.storage_path('app/ssh/mux'));
if (isDev()) {
$user = env('PUID').':'.env('PGID');
Process::run("chown -R $user ".storage_path('app/ssh/keys'));
Process::run("chown -R $user ".storage_path('app/ssh/mux'));
} else {
Process::run('chown -R 9999:root '.storage_path('app/ssh/keys'));
Process::run('chown -R 9999:root '.storage_path('app/ssh/mux'));
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
ray($e->getMessage());
}
}
}

View File

@@ -2,6 +2,8 @@
namespace Database\Seeders;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Database\Seeder;
@@ -16,6 +18,10 @@ class ServerSeeder extends Seeder
'ip' => 'coolify-testing-host',
'team_id' => 0,
'private_key_id' => 1,
'proxy' => [
'type' => ProxyTypes::TRAEFIK->value,
'status' => ProxyStatus::EXITED->value,
],
]);
}
}

View File

@@ -1,3 +1,3 @@
#!/command/execlineb -P
s6-setuidgid webuser
php /var/www/html/artisan app:init --full-cleanup
php /var/www/html/artisan app:init

21
hooks/pre-commit Normal file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
# Detect whether /dev/tty is available & functional
if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then
exec < /dev/tty
fi
# Get list of stashed PHP files
stashed_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.php')
# If there are no stashed PHP files, exit early
if [ -z "$stashed_files" ]; then
exit 0
fi
# Set files variable to only include stashed PHP files
files="$stashed_files"
$(pwd)/vendor/bin/pint $files -q
if [ $? -eq 0 ]; then
git add $files
fi

View File

@@ -30,7 +30,7 @@
"service.stop": "This service will be stopped.",
"resource.docker_cleanup": "Run Docker Cleanup (remove unused images and builder cache).",
"resource.non_persistent": "All non-persistent data will be deleted.",
"resource.delete_volumes": "All volumes associated with this resource will be permanently deleted.",
"resource.delete_connected_networks": "All non-predefined networks associated with this resource will be permanently deleted.",
"resource.delete_configurations": "All configuration files will be permanently deleted from the server."
"resource.delete_volumes": "Permanently delete all volumes associated with this resource.",
"resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.",
"resource.delete_configurations": "Permanently delete all configuration files from the server."
}

View File

@@ -19,11 +19,7 @@ DB_PORT=5432
# Set to true to enable Ray
RAY_ENABLED=false
# Set custom ray port
RAY_PORT=
# Clockwork Configuration
CLOCKWORK_ENABLED=false
CLOCKWORK_QUEUE_COLLECT=true
# RAY_PORT=
# Enable Laravel Telescope for debugging
TELESCOPE_ENABLED=false

View File

@@ -398,90 +398,10 @@ if [ ! -f ~/.ssh/authorized_keys ]; then
chmod 600 ~/.ssh/authorized_keys
fi
checkSshKeyInAuthorizedKeys() {
grep -qw "root@coolify" ~/.ssh/authorized_keys
return $?
}
checkSshKeyInCoolifyData() {
[ -s /data/coolify/ssh/keys/id.root@host.docker.internal ]
return $?
}
generateAuthorizedKeys() {
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
rm -f /data/coolify/ssh/keys/id.root@host.docker.internal.pub
}
generateSshKey() {
echo " - Generating SSH key."
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.root@host.docker.internal -q -N "" -C root@coolify
chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal
generateAuthorizedKeys
}
syncSshKeys() {
DB_RUNNING=$(docker inspect coolify-db --format '{{ .State.Status }}' 2>/dev/null)
# Check if SSH key exists in Coolify data but not in authorized_keys
if checkSshKeyInCoolifyData && ! checkSshKeyInAuthorizedKeys; then
# Add the existing Coolify SSH key to authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
# Check if SSH key exists in authorized_keys but not in Coolify data
elif checkSshKeyInAuthorizedKeys && ! checkSshKeyInCoolifyData; then
# Ensure Coolify DB is running before proceeding
if [ "$DB_RUNNING" = "running" ]; then
# Retrieve DB user and SSH key from Coolify database
DB_USER=$(docker inspect coolify-db --format '{{ .Config.Env }}' | grep -oP 'POSTGRES_USER=\K[^ ]+')
DB_SSH_KEY=$(docker exec coolify-db psql -U $DB_USER -d coolify -t -c "SELECT \"private_key\" FROM \"private_keys\" WHERE id = 0 AND team_id = 0 LIMIT 1;" -A -t)
if [ -z "$DB_SSH_KEY" ]; then
# If no key found in DB, generate a new one
echo " - SSH key not found in database. Generating new key."
generateSshKey
else
# If key found in DB, save it and update authorized_keys
echo " - SSH key found in database. Saving to file."
echo "$DB_SSH_KEY" > /data/coolify/ssh/keys/id.root@host.docker.internal
chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal
chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal
# Generate public key from private key and update authorized_keys
ssh-keygen -y -f /data/coolify/ssh/keys/id.root@host.docker.internal -C root@coolify > /data/coolify/ssh/keys/id.root@host.docker.internal.pub
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
rm -f /data/coolify/ssh/keys/id.root@host.docker.internal.pub
chmod 600 ~/.ssh/authorized_keys
fi
fi
# If SSH key doesn't exist in either location
elif ! checkSshKeyInAuthorizedKeys && ! checkSshKeyInCoolifyData; then
# Ensure Coolify DB is running before proceeding
if [ "$DB_RUNNING" = "running" ]; then
# Retrieve DB user and SSH key from Coolify database
DB_USER=$(docker inspect coolify-db --format '{{ .Config.Env }}' | grep -oP 'POSTGRES_USER=\K[^ ]+')
DB_SSH_KEY=$(docker exec coolify-db psql -U $DB_USER -d coolify -t -c "SELECT \"private_key\" FROM \"private_keys\" WHERE id = 0 AND team_id = 0 LIMIT 1;" -A -t)
if [ -z "$DB_SSH_KEY" ]; then
# If no key found in DB, generate a new one
echo " - SSH key not found in database. Generating new key."
generateSshKey
else
# If key found in DB, save it and update authorized_keys
echo " - SSH key found in database. Saving to file."
echo "$DB_SSH_KEY" > /data/coolify/ssh/keys/id.root@host.docker.internal
chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal
ssh-keygen -y -f /data/coolify/ssh/keys/id.root@host.docker.internal -C root@coolify > /data/coolify/ssh/keys/id.root@host.docker.internal.pub
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
fi
else
generateSshKey
fi
fi
}
set +e
IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l)
set -e
if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
echo " - Generating SSH key."
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify

View File

@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.339"
"version": "4.0.0-beta.343"
},
"nightly": {
"version": "4.0.0-beta.340"
"version": "4.0.0-beta.344"
},
"helper": {
"version": "1.0.1"

6
package-lock.json generated
View File

@@ -1860,9 +1860,9 @@
}
},
"node_modules/rollup": {
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"version": "3.29.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"

View File

@@ -8,7 +8,7 @@
'hideLabel' => false,
])
<div class="flex flex-row items-center gap-4 px-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100">
<div class="flex flex-row items-center py-1 form-control min-w-fit dark:hover:bg-coolgray-100">
@if (!$hideLabel)
<label class="flex gap-4 px-0 min-w-fit label">
<span class="flex gap-2">

View File

@@ -3,7 +3,7 @@
'w-full' => !$isMultiline,
])>
@if ($label)
<label class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
<label class="flex gap-1 items-center mb-1 text-sm font-medium">{{ $label }}
@if ($required)
<x-highlighted text="*" />
@endif
@@ -16,7 +16,7 @@
<div class="relative" x-data="{ type: 'password' }">
@if ($allowToPeak)
<div x-on:click="changePasswordFieldType"
class="absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer hover:dark:text-white">
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer hover:dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />

View File

@@ -139,7 +139,7 @@
@endif
@else
@if ($buttonFullWidth)
<x-forms.button @click="modalOpen=true" class="flex w-full gap-2" wire:target>
<x-forms.button @click="modalOpen=true" class="flex gap-2 w-full" wire:target>
{{ $buttonTitle }}
</x-forms.button>
@else
@@ -162,17 +162,17 @@
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
<div class="flex items-center justify-between pb-3">
<h3 class="text-2xl font-bold pr-8">{{ $title }}</h3>
<div class="flex justify-between items-center pb-3">
<h3 class="pr-8 text-2xl font-bold">{{ $title }}</h3>
<button @click="modalOpen = false; resetModal()"
class="absolute top-2 right-2 flex items-center justify-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="relative w-auto pb-8">
<div class="relative pb-8 w-auto">
@if (!empty($checkboxes))
<!-- Step 1: Select actions -->
<div x-show="step === 1">
@@ -180,9 +180,9 @@
<h4>Actions</h4>
</div>
@foreach ($checkboxes as $index => $checkbox)
<div class="flex items-center justify-between mb-2">
<div class="flex justify-between items-center mb-2">
<label for="{{ $checkbox['id'] }}"
class="text-sm leading-5 text-gray-700 dark:text-gray-300 flex-grow pr-4">
class="flex-grow pr-4 text-sm leading-5 text-gray-700 dark:text-gray-300">
{{ $checkbox['label'] }}
</label>
<x-forms.checkbox :id="$checkbox['id']" :wire:model="$checkbox['id']"
@@ -196,7 +196,7 @@
<!-- Step 2: Confirm deletion -->
<div x-show="step === 2">
<div class="bg-error border-l-4 border-red-500 text-white p-4 mb-4" role="alert">
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert">
<p class="font-bold">Warning</p>
<p>This operation is permanent and cannot be undone. Please think again before proceeding!
</p>
@@ -205,7 +205,7 @@
<ul class="mb-4 space-y-2">
@foreach ($actions as $action)
<li class="flex items-center text-red-500">
<svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor"
<svg class="flex-shrink-0 mr-2 w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
@@ -216,7 +216,7 @@
@foreach ($checkboxes as $checkbox)
<template x-if="selectedActions.includes('{{ $checkbox['id'] }}')">
<li class="flex items-center text-red-500">
<svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor"
<svg class="flex-shrink-0 mr-2 w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
@@ -228,16 +228,16 @@
</ul>
@if ($confirmWithText)
<div class="mb-4">
<h4 class="text-lg font-semibold mb-2">Confirm Actions</h4>
<p class="text-sm mb-2">{{ $confirmationLabel }}</p>
<h4 class="mb-2 text-lg font-semibold">Confirm Actions</h4>
<p class="mb-2 text-sm">{{ $confirmationLabel }}</p>
<div class="relative mb-2">
<input type="text" x-model="confirmationText"
class="w-full p-2 pr-10 rounded text-black input cursor-text" readonly>
class="p-2 pr-10 w-full text-black rounded cursor-text input" readonly>
<button @click="copyConfirmationText()"
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
class="absolute right-2 top-1/2 text-gray-500 transform -translate-y-1/2 hover:text-gray-700"
title="Copy confirmation text" x-ref="copyButton">
<template x-if="!copied">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20"
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 20 20"
fill="currentColor">
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
<path
@@ -245,7 +245,7 @@
</svg>
</template>
<template x-if="copied">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500"
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-green-500"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
@@ -256,18 +256,18 @@
</div>
<label for="userConfirmationText"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mt-4">
class="block mt-4 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ $shortConfirmationLabel }}
</label>
<input type="text" x-model="userConfirmationText"
class="w-full p-2 rounded text-black input mt-1">
class="p-2 mt-1 w-full text-black rounded input">
</div>
@endif
</div>
<!-- Step 3: Password confirmation -->
<div x-show="step === 3 && confirmWithPassword">
<div class="bg-error border-l-4 border-red-500 text-white p-4 mb-4" role="alert">
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert">
<p class="font-bold">Final Confirmation</p>
<p>Please enter your password to confirm this destructive action.</p>
</div>
@@ -276,11 +276,11 @@
class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Your Password
</label>
<input type="password" id="password-confirm" x-model="password" class="input w-full"
<input type="password" id="password-confirm" x-model="password" class="w-full input"
placeholder="Enter your password">
<p x-show="passwordError" x-text="passwordError" class="text-red-500 text-sm mt-1"></p>
<p x-show="passwordError" x-text="passwordError" class="mt-1 text-sm text-red-500"></p>
@error('password')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
</div>

View File

@@ -1,5 +1,5 @@
<x-emails.layout>
Your server ({{ $name }}) disabled because it is not paid! All automations and integrations are stopped.
Please update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).
Please update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).
</x-emails.layout>

View File

@@ -273,27 +273,23 @@
Please let me know your server details.
</x-slot:question>
<x-slot:actions>
<form wire:submit='saveServer' class="flex flex-col w-full gap-4 lg:pr-10">
<div class="flex flex-col gap-2 lg:flex-row">
<form wire:submit='saveServer' class="flex flex-col w-full gap-4 lg:w-96">
<x-forms.input required placeholder="Choose a name for your Server. Could be anything."
label="Name" id="remoteServerName" wire:model="remoteServerName" />
<x-forms.input placeholder="Description, so others will know more about this."
label="Description" id="remoteServerDescription"
wire:model="remoteServerDescription" />
</div>
<div class="flex flex-col gap-2 lg:flex-row ">
<x-forms.input required placeholder="127.0.0.1" label="IP Address" id="remoteServerHost"
wire:model="remoteServerHost" />
</div>
<div x-data="{ showAdvanced: false }" class="flex flex-col gap-2">
<button @click="showAdvanced = !showAdvanced" type="button"
class="text-left text-sm text-gray-600 dark:text-gray-300 hover:underline">
Advanced Settings
</button>
<div x-show="showAdvanced" class="flex flex-col gap-2 lg:flex-row">
<div x-show="showAdvanced" class="flex flex-col gap-2">
<x-forms.input placeholder="Port number of your server. Default is 22." label="Port"
id="remoteServerPort" wire:model="remoteServerPort" />
<div class="w-full">
<div>
<x-forms.input placeholder="Default is root." label="User"
id="remoteServerUser" wire:model="remoteServerUser" />
<div class="text-xs text-gray-600 dark:text-gray-300">Non-root user is
@@ -303,11 +299,6 @@
</div>
</div>
</div>
<div class="lg:w-64">
<x-forms.checkbox
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<br><span class='dark:text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>"
id="isCloudflareTunnel" label="Cloudflare Tunnel" wire:model="isCloudflareTunnel" />
</div>
<x-forms.button type="submit">Continue</x-forms.button>
</form>
</x-slot:actions>

View File

@@ -6,18 +6,10 @@
Save
</x-forms.button>
@if ($destination->network !== 'coolify')
<x-modal-confirmation
title="Confirm Destination Deletion?"
buttonTitle="Delete Destination"
isErrorButton
submitAction="delete"
:actions="['This will delete the selected destination/network.']"
confirmationText="{{ $destination->name }}"
<x-modal-confirmation title="Confirm Destination Deletion?" buttonTitle="Delete Destination" isErrorButton
submitAction="delete" :actions="['This will delete the selected destination/network.']" confirmationText="{{ $destination->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Destination Name below"
shortConfirmationLabel="Destination Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Destination"
/>
shortConfirmationLabel="Destination Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" />
@endif
</div>

View File

@@ -8,6 +8,6 @@
confirmationText="{{ $team }}"
confirmationLabel="Please confirm the execution of the actions by entering the Team Name below"
shortConfirmationLabel="Team Name"
step3ButtonText="Permanently Delete Team"
step3ButtonText="Permanently Delete"
/>
</div>

View File

@@ -68,17 +68,10 @@
<option value="www">Redirect to www.</option>
<option value="non-www">Redirect to non-www.</option>
</x-forms.select>
<x-modal-confirmation
title="Confirm Redirection Setting?"
buttonTitle="Set Direction"
submitAction="set_redirect"
:actions="['All traffic will be redirected to the selected direction.']"
confirmationText="{{ $application->fqdn . '/' }}"
<x-modal-confirmation title="Confirm Redirection Setting?" buttonTitle="Set Direction"
submitAction="set_redirect" :actions="['All traffic will be redirected to the selected direction.']" confirmationText="{{ $application->fqdn . '/' }}"
confirmationLabel="Please confirm the execution of the action by entering the Application URL below"
shortConfirmationLabel="Application URL"
:confirmWithPassword="false"
step2ButtonText="Set Direction"
>
shortConfirmationLabel="Application URL" :confirmWithPassword="false" step2ButtonText="Set Direction">
<x-slot:customButton>
<div class="w-[7.2rem]">Set Direction</div>
</x-slot:customButton>
@@ -311,18 +304,15 @@
helper="If you know what are you doing, you can enable this to edit the labels directly. Coolify won't update labels automatically. <br><br>Be careful, it could break the proxy configuration after you restart the container."
id="application.settings.is_container_label_readonly_enabled" instantSave></x-forms.checkbox>
</div>
<x-modal-confirmation
title="Confirm Labels Reset to Coolify Defaults?"
buttonTitle="Reset Labels to Coolify Defaults"
buttonFullWidth
submitAction="resetDefaultLabels"
:actions="['All your custom proxy labels will be lost.', 'Proxy labels (traefik, caddy, etc) will be reset to the coolify defaults.']"
confirmationText="{{ $application->fqdn . '/' }}"
<x-modal-confirmation title="Confirm Labels Reset to Coolify Defaults?"
buttonTitle="Reset Labels to Coolify Defaults" buttonFullWidth submitAction="resetDefaultLabels"
:actions="[
'All your custom proxy labels will be lost.',
'Proxy labels (traefik, caddy, etc) will be reset to the coolify defaults.',
]" confirmationText="{{ $application->fqdn . '/' }}"
confirmationLabel="Please confirm the execution of the actions by entering the Application URL below"
shortConfirmationLabel="Application URL"
:confirmWithPassword="false"
step2ButtonText="Permanently Reset Labels"
/>
shortConfirmationLabel="Application URL" :confirmWithPassword="false"
step2ButtonText="Permanently Reset Labels" />
@endif
<h3 class="pt-8">Pre/Post Deployment Commands</h3>

View File

@@ -57,7 +57,7 @@
confirmationText="{{ data_get($execution, 'filename') }}"
confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
shortConfirmationLabel="Backup Filename"
step3ButtonText="Permanently Delete Backup"
step3ButtonText="Permanently Delete"
/>
</div>
</div>

View File

@@ -27,27 +27,24 @@
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$database->getMorphClass() === 'App\Models\StandaloneMysql' ||
$database->getMorphClass() === 'App\Models\StandaloneMariadb')
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" href="{{ route('project.database.backup.index', $parameters) }}">
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.backup.index', $parameters) }}">
<button>Backups</button>
</a>
@endif
</nav>
<div class="flex flex-wrap gap-2 items-center">
@if (!str($database->status)->startsWith('exited'))
<x-modal-confirmation
title="Confirm Database Restart?"
buttonTitle="Restart"
submitAction="restart"
:actions="['This database will be unavailable during the restart.', 'If the database is currently in use data could be lost.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Restart Database"
:dispatchEvent="true"
dispatchEventType="restartEvent"
>
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
:actions="[
'This database will be unavailable during the restart.',
'If the database is currently in use data could be lost.',
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
:dispatchEvent="true" dispatchEventType="restartEvent">
<x-slot:button-title>
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
@@ -55,21 +52,17 @@
Restart
</x-slot:button-title>
</x-modal-confirmation>
<x-modal-confirmation
title="Confirm Database Stopping?"
buttonTitle="Stop"
submitAction="stop"
:checkboxes="$checkboxes"
:actions="['This database will be stopped.', 'If the database is currently in use data could be lost.', 'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).']"
:confirmWithText="false"
:confirmWithPassword="false"
step1ButtonText="Continue Stopping Database"
step2ButtonText="Stop Database"
:dispatchEvent="true"
dispatchEventType="stopEvent"
>
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
:checkboxes="$checkboxes" :actions="[
'This database will be stopped.',
'If the database is currently in use data could be lost.',
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
]" :confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue"
step2ButtonText="Stop Database" :dispatchEvent="true" dispatchEventType="stopEvent">
<x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
@@ -81,7 +74,9 @@
</x-modal-confirmation>
@else
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>

View File

@@ -2,21 +2,13 @@
<div class="flex items-end gap-2">
<x-forms.input id="filename" label="Filename" />
<x-forms.button type="submit">Save</x-forms.button>
<x-modal-confirmation
title="Confirm init-script deletion?"
buttonTitle="Delete"
isErrorButton
submitAction="delete"
:actions="[
<x-modal-confirmation title="Confirm init-script deletion?" buttonTitle="Delete" isErrorButton
submitAction="delete" :actions="[
'The init-script of this database will be permanently deleted.',
'If you are actively using this init-script, it could cause errors on redeployment.'
]"
confirmationText="{{ $filename }}"
'If you are actively using this init-script, it could cause errors on redeployment.',
]" confirmationText="{{ $filename }}"
confirmationLabel="Please confirm the execution of the actions by entering the init-script name below"
shortConfirmationLabel="Init-script Name"
:confirmWithPassword=false
step2ButtonText="Permanently Delete Init-script"
/>
shortConfirmationLabel="Init-script Name" :confirmWithPassword=false step2ButtonText="Permanently Delete" />
</div>
<x-forms.textarea id="content" label="Content" />
</form>

View File

@@ -1,12 +1,5 @@
<x-modal-confirmation
title="Confirm Environment Deletion?"
buttonTitle="Delete Environment"
isErrorButton
submitAction="delete"
:actions="['This will delete the selected environment.']"
<x-modal-confirmation title="Confirm Environment Deletion?" buttonTitle="Delete Environment" isErrorButton
submitAction="delete" :actions="['This will delete the selected environment.']"
confirmationLabel="Please confirm the execution of the actions by entering the Environment Name below"
shortConfirmationLabel="Environment Name"
confirmationText="{{ $environmentName }}"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Environment"
/>
shortConfirmationLabel="Environment Name" confirmationText="{{ $environmentName }}" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />

View File

@@ -1,12 +1,7 @@
<x-modal-confirmation
title="Confirm Project Deletion?"
buttonTitle="Delete Project"
isErrorButton
submitAction="delete"
:actions="['This will delete the selected project', 'All Environments inside the project will be deleted as well.']"
confirmationLabel="Please confirm the execution of the actions by entering the Project Name below"
shortConfirmationLabel="Project Name"
confirmationText="{{ $projectName }}"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Project"
/>
<x-modal-confirmation title="Confirm Project Deletion?" buttonTitle="Delete Project" isErrorButton submitAction="delete"
:actions="[
'This will delete the selected project',
'All Environments inside the project will be deleted as well.',
]" confirmationLabel="Please confirm the execution of the actions by entering the Project Name below"
shortConfirmationLabel="Project Name" confirmationText="{{ $projectName }}" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />

View File

@@ -491,7 +491,9 @@
</div>
<div class="pb-4 text-xs">Trademarks Policy: The respective trademarks mentioned here are owned by the
respective
companies, and use of them does not imply any affiliation or endorsement.</div>
companies, and use of them does not imply any affiliation or endorsement.<br>Find more services <a
class="dark:text-white underline" target="_blank"
href="https://coolify.io/docs/services">here</a>.</div>
<input class="input" autofocus wire:model.live.debounce.200ms="search" autofocus
placeholder="Search...">
@if ($loadingServices)

View File

@@ -15,56 +15,32 @@
<form wire:submit='submit' class="flex flex-col gap-2">
<div class="flex gap-2">
@if ($fileStorage->is_directory)
<x-modal-confirmation
title="Confirm Directory Conversion to File?"
buttonTitle="Convert to file"
submitAction="convertToFile"
:actions="['All files in this directory will be permanently deleted and an empty file will be created in its place.']"
confirmationText="{{ $fs_path }}"
<x-modal-confirmation title="Confirm Directory Conversion to File?" buttonTitle="Convert to file"
submitAction="convertToFile" :actions="[
'All files in this directory will be permanently deleted and an empty file will be created in its place.',
]" confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath"
:confirmWithPassword="false"
step2ButtonText="Convert to file"
/>
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to file" />
@else
<x-modal-confirmation
title="Confirm File Conversion to Directory?"
buttonTitle="Convert to directory"
submitAction="convertToDirectory"
:actions="['The selected file will be permanently deleted and an empty directory will be created in its place.']"
confirmationText="{{ $fs_path }}"
<x-modal-confirmation title="Confirm File Conversion to Directory?" buttonTitle="Convert to directory"
submitAction="convertToDirectory" :actions="[
'The selected file will be permanently deleted and an empty directory will be created in its place.',
]" confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath"
:confirmWithPassword="false"
step2ButtonText="Convert to directory"
/>
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to directory" />
@endif
@if ($fileStorage->is_directory)
<x-modal-confirmation
title="Confirm Directory Deletion?"
buttonTitle="Delete Directory"
isErrorButton
submitAction="delete"
:checkboxes="$directoryDeletionCheckboxes"
:actions="['The selected directory and all its contents will be permanently deleted from the container.']"
confirmationText="{{ $fs_path }}"
<x-modal-confirmation title="Confirm Directory Deletion?" buttonTitle="Delete Directory" isErrorButton
submitAction="delete" :checkboxes="$directoryDeletionCheckboxes" :actions="[
'The selected directory and all its contents will be permanently deleted from the container.',
]" confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath"
step3ButtonText="Permanently Delete Directory"
/>
shortConfirmationLabel="Filepath" step3ButtonText="Permanently Delete" />
@else
<x-modal-confirmation
title="Confirm File Deletion?"
buttonTitle="Delete File"
isErrorButton
submitAction="delete"
:checkboxes="$fileDeletionCheckboxes"
:actions="['The selected file will be permanently deleted from the container.']"
confirmationText="{{ $fs_path }}"
<x-modal-confirmation title="Confirm File Deletion?" buttonTitle="Delete File" isErrorButton
submitAction="delete" :checkboxes="$fileDeletionCheckboxes" :actions="['The selected file will be permanently deleted from the container.']" confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath"
step3ButtonText="Permanently Delete File"
/>
shortConfirmationLabel="Filepath" step3ButtonText="Permanently Delete" />
@endif
@if (!$fileStorage->is_based_on_git)

View File

@@ -7,20 +7,11 @@
<h2>{{ Str::headline($application->name) }}</h2>
@endif
<x-forms.button type="submit">Save</x-forms.button>
<x-modal-confirmation
title="Confirm Service Application Deletion?"
buttonTitle="Delete"
isErrorButton
submitAction="delete"
{{-- :checkboxes="$checkboxes" --}}
:actions="[
'The selected service application container will be stopped and permanently deleted.'
]"
<x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
submitAction="delete" {{-- :checkboxes="$checkboxes" --}} :actions="['The selected service application container will be stopped and permanently deleted.']"
confirmationText="{{ Str::headline($application->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
shortConfirmationLabel="Service Application Name"
step3ButtonText="Permanently Delete Service Application"
/>
shortConfirmationLabel="Service Application Name" step3ButtonText="Permanently Delete" />
</div>
<div class="flex flex-col gap-2">
<div class="flex gap-2">

View File

@@ -5,7 +5,7 @@
<div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming back!
</div>
<x-modal-confirmation title="Confirm Resource Deletion?" buttonTitle="Delete" isErrorButton submitAction="delete"
buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['All containers of this resource will be stopped and permanently deleted.']" confirmationText="{{ $resourceName }}"
buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['Permanently delete all containers of this resource.']" confirmationText="{{ $resourceName }}"
confirmationLabel="Please confirm the execution of the actions by entering the NAME of the resource below"
shortConfirmationLabel="Resource Name" step3ButtonText="Delete Permanently" />
shortConfirmationLabel="Resource Name" step3ButtonText="Permanently Delete" />
</div>

View File

@@ -4,7 +4,9 @@
<x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value"
x-bind:label="$wire.is_multiline === false && 'Value'" required />
@if (data_get($parameters, 'application_uuid'))
<x-forms.checkbox id="is_build_time" label="Build Variable?" />
<x-forms.checkbox id="is_build_time"
helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`"
label="Build Variable?" />
@endif
<x-forms.checkbox id="is_multiline" label="Is Multiline?" />
@if (!$shared)

View File

@@ -11,20 +11,11 @@
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4" />
</g>
</svg>
<x-modal-confirmation
title="Confirm Environment Variable Deletion?"
isErrorButton
buttonTitle="Delete"
submitAction="delete"
:actions="[
'The selected environment variable will be permanently deleted.'
]"
confirmationText="{{ $env->key }}"
<x-modal-confirmation title="Confirm Environment Variable Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="['The selected environment variable will be permanently deleted.']" confirmationText="{{ $env->key }}"
confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below"
shortConfirmationLabel="Environment Variable Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Environment Variable"
/>
shortConfirmationLabel="Environment Variable Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
</div>
@else
@if ($isDisabled)
@@ -51,10 +42,14 @@
@endif
<div class="flex flex-col w-full gap-2 lg:flex-row">
@if ($type === 'service')
<x-forms.checkbox instantSave id="env.is_build_time" label="Build Variable?" />
<x-forms.checkbox instantSave id="env.is_build_time"
helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`"
label="Build Variable?" />
@else
@if ($env->is_shared)
<x-forms.checkbox instantSave id="env.is_build_time" label="Build Variable?" />
<x-forms.checkbox instantSave id="env.is_build_time"
helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`"
label="Build Variable?" />
<x-forms.checkbox instantSave id="env.is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@@ -62,7 +57,9 @@
@if ($isSharedVariable)
<x-forms.checkbox instantSave id="env.is_multiline" label="Is Multiline?" />
@else
<x-forms.checkbox instantSave id="env.is_build_time" label="Build Variable?" />
<x-forms.checkbox instantSave id="env.is_build_time"
helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for dockerfile, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`"
label="Build Variable?" />
<x-forms.checkbox instantSave id="env.is_multiline" label="Is Multiline?" />
@if (!data_get($env, 'is_multiline'))
<x-forms.checkbox instantSave id="env.is_literal"
@@ -80,20 +77,12 @@
<x-forms.button wire:click='lock'>
Lock
</x-forms.button>
<x-modal-confirmation
title="Confirm Environment Variable Deletion?"
isErrorButton
buttonTitle="Delete"
submitAction="delete"
:actions="[
'The selected environment variable will be permanently deleted.'
]"
<x-modal-confirmation title="Confirm Environment Variable Deletion?" isErrorButton
buttonTitle="Delete" submitAction="delete" :actions="['The selected environment variable will be permanently deleted.']"
confirmationText="{{ $env->key }}"
confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below"
shortConfirmationLabel="Environment Variable Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Environment Variable"
/>
shortConfirmationLabel="Environment Variable Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@else
<x-forms.button type="submit">
Update
@@ -101,20 +90,12 @@
<x-forms.button wire:click='lock'>
Lock
</x-forms.button>
<x-modal-confirmation
title="Confirm Environment Variable Deletion?"
isErrorButton
buttonTitle="Delete"
submitAction="delete"
:actions="[
'The selected environment variable will be permanently deleted.'
]"
<x-modal-confirmation title="Confirm Environment Variable Deletion?" isErrorButton
buttonTitle="Delete" submitAction="delete" :actions="['The selected environment variable will be permanently deleted.']"
confirmationText="{{ $env->key }}"
confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below"
shortConfirmationLabel="Environment Variable Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Environment Variable"
/>
shortConfirmationLabel="Environment Variable Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@endif
</div>
@endif

View File

@@ -11,26 +11,22 @@
<form wire:submit="submit" class="w-full">
<div class="flex flex-col gap-2 pb-2">
<div class="flex items-end gap-2 pt-4">
<div class="flex gap-2 items-end pt-4">
<h2>Scheduled Task</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
<x-modal-confirmation
title="Confirm Scheduled Task Deletion?"
isErrorButton
buttonTitle="Delete"
submitAction="delete({{ $task->id }})"
:actions="['The selected scheduled task will be permanently deleted.']"
confirmationText="{{ $task->name }}"
<x-modal-confirmation title="Confirm Scheduled Task Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete({{ $task->id }})" :actions="['The selected scheduled task will be permanently deleted.']" confirmationText="{{ $task->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Scheduled Task Name below"
shortConfirmationLabel="Scheduled Task Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Scheduled Task"
/>
shortConfirmationLabel="Scheduled Task Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
</div>
<div class="flex w-full gap-2">
<div class="w-48">
<x-forms.checkbox instantSave id="task.enabled" label="Enabled" />
</div>
<div class="flex gap-2 w-full">
<x-forms.input placeholder="Name" id="task.name" label="Name" required />
<x-forms.input placeholder="php artisan schedule:run" id="task.command" label="Command" required />
<x-forms.input placeholder="0 0 * * * or daily" id="task.frequency" label="Frequency" required />
@@ -48,6 +44,7 @@
<div class="pt-4">
<h3 class="py-4">Recent executions <span class="text-xs text-neutral-500">(click to check output)</span></h3>
<livewire:project.shared.scheduled-task.executions :task="$task" key="{{ $task->id }}" selectedKey="" :executions="$task->executions->take(20)" />
<livewire:project.shared.scheduled-task.executions :task="$task" key="{{ $task->id }}" selectedKey=""
:executions="$task->executions->take(20)" />
</div>
</div>

View File

@@ -46,17 +46,13 @@
<x-forms.button type="submit">
Update
</x-forms.button>
<x-modal-confirmation
title="Confirm persistent storage deletion?"
isErrorButton
buttonTitle="Delete"
submitAction="delete"
:actions="['The selected persistent storage/volume will be permanently deleted.', 'If the persistent storage/volume is actvily used by a resource data will be lost.']"
confirmationText="{{ $storage->name }}"
<x-modal-confirmation title="Confirm persistent storage deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="[
'The selected persistent storage/volume will be permanently deleted.',
'If the persistent storage/volume is actvily used by a resource data will be lost.',
]" confirmationText="{{ $storage->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Storage Name below"
shortConfirmationLabel="Storage Name"
step3ButtonText="Permanently Delete Persistent Storage/Volume"
/>
shortConfirmationLabel="Storage Name" step3ButtonText="Permanently Delete" />
</div>
@endif
</form>

View File

@@ -1,8 +1,6 @@
<form wire:submit.prevent='submit' class="flex flex-col w-full gap-2">
<x-forms.input id="cloudflare_token" required label="Cloudflare Token" />
<form wire:submit.prevent='submit' class="flex flex-col gap-2 w-full">
<x-forms.input id="cloudflare_token" required label="Cloudflare Token" type="password" />
<x-forms.input id="ssh_domain" label="Configured SSH Domain" required
helper="The SSH Domain you configured in Cloudflare" />
<x-forms.button type="submit" isHighlighted @click="modalOpen=false">Automated Configuration (experimental)</x-forms.button>
<h3 class="text-center">Or</h3>
<x-forms.button wire:click.prevent='alreadyConfigured' @click="modalOpen=false">I have already set up the tunnel manually on the server.</x-forms.button>
helper="The SSH domain you configured in Cloudflare. Make sure there is no protocol like http(s):// so you provide a FQDN not a URL. <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels/#automated' target='_blank'>Documentation</a>" />
<x-forms.button type="submit" isHighlighted @click="modalOpen=false">Continue</x-forms.button>
</form>

Some files were not shown because too many files have changed in this diff Show More