@@ -24,7 +24,7 @@ class StartClickhouse
|
|||||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting database.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class StartDragonfly
|
|||||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting database.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class StartKeydb
|
|||||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting database.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class StartMariadb
|
|||||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting database.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class StartMongodb
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting database.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class StartMysql
|
|||||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting database.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class StartPostgresql
|
|||||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting database.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
|
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class StartRedis
|
|||||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting database.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class CloudCleanupSubscriptions extends Command
|
|||||||
}
|
}
|
||||||
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
|
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
|
||||||
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
|
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
|
||||||
$this->info("Resetting invoice paid status for team {$team->id} {$team->name}");
|
$this->info("Resetting invoice paid status for team {$team->id}");
|
||||||
|
|
||||||
$team->subscription->update([
|
$team->subscription->update([
|
||||||
'stripe_invoice_paid' => false,
|
'stripe_invoice_paid' => false,
|
||||||
@@ -61,9 +61,9 @@ class CloudCleanupSubscriptions extends Command
|
|||||||
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
|
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
|
||||||
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
|
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
|
||||||
if (! $confirm) {
|
if (! $confirm) {
|
||||||
$this->info("Skipping team {$team->id} {$team->name}");
|
$this->info("Skipping team {$team->id}");
|
||||||
} else {
|
} else {
|
||||||
$this->info("Cancelling subscription for team {$team->id} {$team->name}");
|
$this->info("Cancelling subscription for team {$team->id}");
|
||||||
$team->subscription->update([
|
$team->subscription->update([
|
||||||
'stripe_invoice_paid' => false,
|
'stripe_invoice_paid' => false,
|
||||||
'stripe_trial_already_ended' => false,
|
'stripe_trial_already_ended' => false,
|
||||||
|
|||||||
@@ -16,24 +16,26 @@ class Configuration extends Component
|
|||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->application = Application::query()
|
$project = currentTeam()
|
||||||
->whereHas('environment.project', function ($query) {
|
->projects()
|
||||||
$query->where('team_id', currentTeam()->id)
|
->select('id', 'uuid', 'team_id')
|
||||||
->where('uuid', request()->route('project_uuid'));
|
->where('uuid', request()->route('project_uuid'))
|
||||||
})
|
->firstOrFail();
|
||||||
->whereHas('environment', function ($query) {
|
$environment = $project->environments()
|
||||||
$query->where('name', request()->route('environment_name'));
|
->select('id', 'name', 'project_id')
|
||||||
})
|
->where('name', request()->route('environment_name'))
|
||||||
|
->firstOrFail();
|
||||||
|
$application = $environment->applications()
|
||||||
|
->with(['destination'])
|
||||||
->where('uuid', request()->route('application_uuid'))
|
->where('uuid', request()->route('application_uuid'))
|
||||||
->with(['destination' => function ($query) {
|
|
||||||
$query->select('id', 'server_id');
|
|
||||||
}])
|
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
if ($this->application->destination && $this->application->destination->server_id) {
|
$this->application = $application;
|
||||||
|
if ($application->destination && $application->destination->server) {
|
||||||
|
$mainServer = $application->destination->server;
|
||||||
$this->servers = Server::ownedByCurrentTeam()
|
$this->servers = Server::ownedByCurrentTeam()
|
||||||
->select('id', 'name')
|
->select('id', 'name')
|
||||||
->where('id', '!=', $this->application->destination->server_id)
|
->where('id', '!=', $mainServer->id)
|
||||||
->get();
|
->get();
|
||||||
} else {
|
} else {
|
||||||
$this->servers = collect();
|
$this->servers = collect();
|
||||||
|
|||||||
@@ -168,18 +168,42 @@ class ExecuteContainerCommand extends Component
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
// Validate container name format
|
||||||
|
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $this->selected_container)) {
|
||||||
|
throw new \InvalidArgumentException('Invalid container name format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify container exists in our allowed list
|
||||||
$container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
|
$container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
|
||||||
if (is_null($container)) {
|
if (is_null($container)) {
|
||||||
throw new \RuntimeException('Container not found.');
|
throw new \RuntimeException('Container not found.');
|
||||||
}
|
}
|
||||||
$server = data_get($this->container, 'server');
|
|
||||||
|
// Verify server ownership and status
|
||||||
|
$server = data_get($container, 'server');
|
||||||
|
if (! $server || ! $server instanceof Server) {
|
||||||
|
throw new \RuntimeException('Invalid server configuration.');
|
||||||
|
}
|
||||||
|
|
||||||
if ($server->isForceDisabled()) {
|
if ($server->isForceDisabled()) {
|
||||||
throw new \RuntimeException('Server is disabled.');
|
throw new \RuntimeException('Server is disabled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Additional ownership verification based on resource type
|
||||||
|
$resourceServer = match ($this->type) {
|
||||||
|
'application' => $this->resource->destination->server,
|
||||||
|
'database' => $this->resource->destination->server,
|
||||||
|
'service' => $this->resource->server,
|
||||||
|
default => throw new \RuntimeException('Invalid resource type.')
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($server->id !== $resourceServer->id && ! $this->resource->additional_servers->contains('id', $server->id)) {
|
||||||
|
throw new \RuntimeException('Server ownership verification failed.');
|
||||||
|
}
|
||||||
|
|
||||||
$this->dispatch(
|
$this->dispatch(
|
||||||
'send-terminal-command',
|
'send-terminal-command',
|
||||||
isset($container),
|
true,
|
||||||
data_get($container, 'container.Names'),
|
data_get($container, 'container.Names'),
|
||||||
data_get($container, 'server.uuid')
|
data_get($container, 'server.uuid')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,11 +29,20 @@ class Terminal extends Component
|
|||||||
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
|
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
|
||||||
|
|
||||||
if ($isContainer) {
|
if ($isContainer) {
|
||||||
|
// Validate container identifier format (alphanumeric, dashes, and underscores only)
|
||||||
|
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $identifier)) {
|
||||||
|
throw new \InvalidArgumentException('Invalid container identifier format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify container exists and belongs to the user's team
|
||||||
$status = getContainerStatus($server, $identifier);
|
$status = getContainerStatus($server, $identifier);
|
||||||
if ($status !== 'running') {
|
if ($status !== 'running') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
|
|
||||||
|
// Escape the identifier for shell usage
|
||||||
|
$escapedIdentifier = escapeshellarg($identifier);
|
||||||
|
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
|
||||||
} else {
|
} else {
|
||||||
$command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi');
|
$command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace App\Livewire\Server;
|
|||||||
use App\Actions\Server\StartSentinel;
|
use App\Actions\Server\StartSentinel;
|
||||||
use App\Actions\Server\StopSentinel;
|
use App\Actions\Server\StopSentinel;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Computed;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -79,9 +79,6 @@ class Show extends Component
|
|||||||
#[Validate(['required'])]
|
#[Validate(['required'])]
|
||||||
public string $serverTimezone;
|
public string $serverTimezone;
|
||||||
|
|
||||||
#[Locked]
|
|
||||||
public array $timezones;
|
|
||||||
|
|
||||||
public function getListeners()
|
public function getListeners()
|
||||||
{
|
{
|
||||||
$teamId = auth()->user()->currentTeam()->id;
|
$teamId = auth()->user()->currentTeam()->id;
|
||||||
@@ -96,13 +93,21 @@ class Show extends Component
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
||||||
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
|
|
||||||
$this->syncData();
|
$this->syncData();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function timezones(): array
|
||||||
|
{
|
||||||
|
return collect(timezone_identifiers_list())
|
||||||
|
->sort()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
public function syncData(bool $toModel = false)
|
public function syncData(bool $toModel = false)
|
||||||
{
|
{
|
||||||
if ($toModel) {
|
if ($toModel) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use App\Models\InstanceSettings;
|
|||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Computed;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -17,9 +17,6 @@ class Index extends Component
|
|||||||
|
|
||||||
protected Server $server;
|
protected Server $server;
|
||||||
|
|
||||||
#[Locked]
|
|
||||||
public $timezones;
|
|
||||||
|
|
||||||
#[Validate('boolean')]
|
#[Validate('boolean')]
|
||||||
public bool $is_auto_update_enabled;
|
public bool $is_auto_update_enabled;
|
||||||
|
|
||||||
@@ -101,12 +98,20 @@ class Index extends Component
|
|||||||
$this->is_api_enabled = $this->settings->is_api_enabled;
|
$this->is_api_enabled = $this->settings->is_api_enabled;
|
||||||
$this->auto_update_frequency = $this->settings->auto_update_frequency;
|
$this->auto_update_frequency = $this->settings->auto_update_frequency;
|
||||||
$this->update_check_frequency = $this->settings->update_check_frequency;
|
$this->update_check_frequency = $this->settings->update_check_frequency;
|
||||||
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
|
|
||||||
$this->instance_timezone = $this->settings->instance_timezone;
|
$this->instance_timezone = $this->settings->instance_timezone;
|
||||||
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
|
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function timezones(): array
|
||||||
|
{
|
||||||
|
return collect(timezone_identifiers_list())
|
||||||
|
->sort()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
public function instantSave($isSave = true)
|
public function instantSave($isSave = true)
|
||||||
{
|
{
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Enums\ApplicationDeploymentStatus;
|
use App\Enums\ApplicationDeploymentStatus;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Process\InvokedProcess;
|
use Illuminate\Process\InvokedProcess;
|
||||||
@@ -104,7 +105,7 @@ use Visus\Cuid2\Cuid2;
|
|||||||
|
|
||||||
class Application extends BaseModel
|
class Application extends BaseModel
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
private static $parserVersion = '4';
|
private static $parserVersion = '4';
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use App\Notifications\Server\Reachable;
|
|||||||
use App\Notifications\Server\Unreachable;
|
use App\Notifications\Server\Unreachable;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@@ -48,7 +49,7 @@ use Symfony\Component\Yaml\Yaml;
|
|||||||
|
|
||||||
class Server extends BaseModel
|
class Server extends BaseModel
|
||||||
{
|
{
|
||||||
use SchemalessAttributesTrait, SoftDeletes;
|
use HasFactory, SchemalessAttributesTrait, SoftDeletes;
|
||||||
|
|
||||||
public static $batch_counter = 0;
|
public static $batch_counter = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -127,13 +127,6 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function name(): Attribute
|
|
||||||
{
|
|
||||||
return new Attribute(
|
|
||||||
get: fn () => sanitize_string($this->getRawOriginal('name')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRecepients($notification)
|
public function getRecepients($notification)
|
||||||
{
|
{
|
||||||
$recipients = data_get($notification, 'emails', null);
|
$recipients = data_get($notification, 'emails', null);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const SPECIFIC_SERVICES = [
|
|||||||
|
|
||||||
// Based on /etc/os-release
|
// Based on /etc/os-release
|
||||||
const SUPPORTED_OS = [
|
const SUPPORTED_OS = [
|
||||||
'ubuntu debian raspbian',
|
'ubuntu debian raspbian pop',
|
||||||
'centos fedora rhel ol rocky amzn almalinux',
|
'centos fedora rhel ol rocky amzn almalinux',
|
||||||
'sles opensuse-leap opensuse-tumbleweed',
|
'sles opensuse-leap opensuse-tumbleweed',
|
||||||
'arch',
|
'arch',
|
||||||
|
|||||||
@@ -90,8 +90,11 @@ function metrics_dir(): string
|
|||||||
return base_configuration_dir().'/metrics';
|
return base_configuration_dir().'/metrics';
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitize_string(string $input): string
|
function sanitize_string(?string $input = null): ?string
|
||||||
{
|
{
|
||||||
|
if (is_null($input)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// Remove any HTML/PHP tags
|
// Remove any HTML/PHP tags
|
||||||
$sanitized = strip_tags($input);
|
$sanitized = strip_tags($input);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'coolify' => [
|
'coolify' => [
|
||||||
'version' => '4.0.0-beta.374',
|
'version' => '4.0.0-beta.375',
|
||||||
'self_hosted' => env('SELF_HOSTED', true),
|
'self_hosted' => env('SELF_HOSTED', true),
|
||||||
'autoupdate' => env('AUTOUPDATE'),
|
'autoupdate' => env('AUTOUPDATE'),
|
||||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||||
|
|||||||
@@ -49,6 +49,22 @@ return [
|
|||||||
'search_path' => 'public',
|
'search_path' => 'public',
|
||||||
'sslmode' => 'prefer',
|
'sslmode' => 'prefer',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'testing' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DATABASE_TEST_URL'),
|
||||||
|
'host' => env('DB_TEST_HOST', 'postgres'),
|
||||||
|
'port' => env('DB_TEST_PORT', '5432'),
|
||||||
|
'database' => env('DB_TEST_DATABASE', 'coolify_test'),
|
||||||
|
'username' => env('DB_TEST_USERNAME', 'coolify'),
|
||||||
|
'password' => env('DB_TEST_PASSWORD', 'password'),
|
||||||
|
'charset' => 'utf8',
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => 'prefer',
|
||||||
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
22
database/factories/ApplicationFactory.php
Normal file
22
database/factories/ApplicationFactory.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class ApplicationFactory extends Factory
|
||||||
|
{
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->unique()->name(),
|
||||||
|
'destination_id' => 1,
|
||||||
|
'git_repository' => fake()->url(),
|
||||||
|
'git_branch' => fake()->word(),
|
||||||
|
'build_pack' => 'nixpacks',
|
||||||
|
'ports_exposes' => '3000',
|
||||||
|
'environment_id' => 1,
|
||||||
|
'destination_id' => 1,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
17
database/factories/ServerFactory.php
Normal file
17
database/factories/ServerFactory.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class ServerFactory extends Factory
|
||||||
|
{
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->unique()->name(),
|
||||||
|
'ip' => fake()->unique()->ipv4(),
|
||||||
|
'private_key_id' => 1,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_DRIVER" value="array"/>
|
<env name="CACHE_DRIVER" value="array"/>
|
||||||
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
<env name="DB_CONNECTION" value="testing"/>
|
||||||
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
<env name="DB_TEST_DATABASE" value="coolify_test"/>
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
<env name="SESSION_DRIVER" value="array"/>
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
|||||||
1
public/svgs/plex.svg
Normal file
1
public/svgs/plex.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="15%" fill="#282a2d"/><path d="M256 70H148l108 186-108 186h108l108-186z" fill="#e5a00d"/></svg>
|
||||||
|
After Width: | Height: | Size: 191 B |
@@ -7,6 +7,7 @@
|
|||||||
'action' => 'delete',
|
'action' => 'delete',
|
||||||
'content' => null,
|
'content' => null,
|
||||||
'closeOutside' => true,
|
'closeOutside' => true,
|
||||||
|
'minWidth' => '36rem',
|
||||||
])
|
])
|
||||||
<div x-data="{ modalOpen: false }" :class="{ 'z-40': modalOpen }" @keydown.window.escape="modalOpen=false"
|
<div x-data="{ modalOpen: false }" :class="{ 'z-40': modalOpen }" @keydown.window.escape="modalOpen=false"
|
||||||
class="relative w-auto h-auto" wire:ignore>
|
class="relative w-auto h-auto" wire:ignore>
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
x-transition:leave="ease-in duration-100"
|
x-transition:leave="ease-in duration-100"
|
||||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||||
class="relative w-full py-6 border rounded drop-shadow min-w-full lg:min-w-[36rem] max-w-fit bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300">
|
class="relative w-full py-6 border rounded drop-shadow min-w-full lg:min-w-[{{ $minWidth }}] max-w-fit bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300">
|
||||||
<div class="flex items-center justify-between pb-3">
|
<div class="flex items-center justify-between pb-3">
|
||||||
<h3 class="text-2xl font-bold">{{ $title }}</h3>
|
<h3 class="text-2xl font-bold">{{ $title }}</h3>
|
||||||
<button @click="modalOpen=false"
|
<button @click="modalOpen=false"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
@forelse ($deployments as $deployment)
|
@forelse ($deployments as $deployment)
|
||||||
<div @class([
|
<div @class([
|
||||||
'dark:bg-coolgray-100 p-2 border-l-2 transition-colors hover:no-underline box-without-bg-without-border bg-white flex-col cursor-pointer dark:hover:text-neutral-400 dark:hover:bg-coolgray-200',
|
'dark:bg-coolgray-100 p-2 border-l-2 transition-colors hover:no-underline box-without-bg-without-border bg-white flex-col cursor-pointer dark:hover:text-neutral-400 dark:hover:bg-coolgray-200',
|
||||||
'border-warning border-dashed ' =>
|
'border-white border-dashed ' =>
|
||||||
data_get($deployment, 'status') === 'in_progress' ||
|
data_get($deployment, 'status') === 'in_progress' ||
|
||||||
data_get($deployment, 'status') === 'cancelled-by-user',
|
data_get($deployment, 'status') === 'cancelled-by-user',
|
||||||
'border-error border-dashed ' =>
|
'border-error border-dashed ' =>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
volume
|
volume
|
||||||
name, example: <span class='text-helper'>-pr-1</span>" />
|
name, example: <span class='text-helper'>-pr-1</span>" />
|
||||||
@if ($resource?->build_pack !== 'dockercompose')
|
@if ($resource?->build_pack !== 'dockercompose')
|
||||||
<x-modal-input :closeOutside="false" buttonTitle="+ Add" title="New Persistent Storage">
|
<x-modal-input :closeOutside="false" buttonTitle="+ Add" title="New Persistent Storage" minWidth="64rem">
|
||||||
<livewire:project.shared.storages.add :resource="$resource" />
|
<livewire:project.shared.storages.add :resource="$resource" />
|
||||||
</x-modal-input>
|
</x-modal-input>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<div class="flex flex-col w-full gap-2 rounded max-h-[80vh] overflow-y-auto scrollbar">
|
<div class="flex flex-col w-full gap-2 max-h-[80vh] overflow-y-auto scrollbar">
|
||||||
<div class="p-4">
|
<form class="flex flex-col w-full gap-2 rounded " wire:submit='submitPersistentVolume'>
|
||||||
You can add Volumes, Files and Directories to your resources here.
|
<div class="flex flex-col">
|
||||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitPersistentVolume'>
|
|
||||||
<h3>Volume Mount</h3>
|
<h3>Volume Mount</h3>
|
||||||
@if ($isSwarm)
|
<div>Docker Volumes mounted to the container.</div>
|
||||||
<h5>Swarm Mode detected: You need to set a shared volume (EFS/NFS/etc) on all the worker nodes if you
|
</div>
|
||||||
would
|
@if ($isSwarm)
|
||||||
like to use a persistent volumes.</h5>
|
<h5>Swarm Mode detected: You need to set a shared volume (EFS/NFS/etc) on all the worker nodes if you
|
||||||
@endif
|
would
|
||||||
|
like to use a persistent volumes.</h5>
|
||||||
|
@endif
|
||||||
|
<div class="flex flex-col gap-2 px-2">
|
||||||
<x-forms.input placeholder="pv-name" id="name" label="Name" required helper="Volume name." />
|
<x-forms.input placeholder="pv-name" id="name" label="Name" required helper="Volume name." />
|
||||||
@if ($isSwarm)
|
@if ($isSwarm)
|
||||||
<x-forms.input placeholder="/root" id="host_path" label="Source Path" required
|
<x-forms.input placeholder="/root" id="host_path" label="Source Path" required
|
||||||
@@ -19,29 +21,39 @@
|
|||||||
<x-forms.input placeholder="/tmp/root" id="mount_path" label="Destination Path" required
|
<x-forms.input placeholder="/tmp/root" id="mount_path" label="Destination Path" required
|
||||||
helper="Directory inside the container." />
|
helper="Directory inside the container." />
|
||||||
<x-forms.button type="submit" @click="modalOpen=false">
|
<x-forms.button type="submit" @click="modalOpen=false">
|
||||||
Save
|
Add
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitFileStorage'>
|
<form class="flex flex-col w-full gap-2 rounded py-4" wire:submit='submitFileStorage'>
|
||||||
|
<div class="flex flex-col">
|
||||||
<h3>File Mount</h3>
|
<h3>File Mount</h3>
|
||||||
|
<div>Actual file mounted from the host system to the container.</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 px-2">
|
||||||
<x-forms.input placeholder="/etc/nginx/nginx.conf" id="file_storage_path" label="Destination Path" required
|
<x-forms.input placeholder="/etc/nginx/nginx.conf" id="file_storage_path" label="Destination Path" required
|
||||||
helper="File inside the container" />
|
helper="File location inside the container" />
|
||||||
<x-forms.textarea label="Content" id="file_storage_content"></x-forms.textarea>
|
<x-forms.textarea label="Content" id="file_storage_content"></x-forms.textarea>
|
||||||
<x-forms.button type="submit" @click="modalOpen=false">
|
<x-forms.button type="submit" @click="modalOpen=false">
|
||||||
Save
|
Add
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
</form>
|
</div>
|
||||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitFileStorageDirectory'>
|
</form>
|
||||||
|
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitFileStorageDirectory'>
|
||||||
|
<div class="flex flex-col">
|
||||||
<h3>Directory Mount</h3>
|
<h3>Directory Mount</h3>
|
||||||
|
<div>Directory mounted from the host system to the container.</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 px-2">
|
||||||
<x-forms.input placeholder="{{ application_configuration_dir() }}/{{ $resource->uuid }}/etc/nginx"
|
<x-forms.input placeholder="{{ application_configuration_dir() }}/{{ $resource->uuid }}/etc/nginx"
|
||||||
id="file_storage_directory_source" label="Source Directory" required
|
id="file_storage_directory_source" label="Source Directory" required
|
||||||
helper="Directory on the host system." />
|
helper="Directory on the host system." />
|
||||||
<x-forms.input placeholder="/etc/nginx" id="file_storage_directory_destination"
|
<x-forms.input placeholder="/etc/nginx" id="file_storage_directory_destination"
|
||||||
label="Destination Directory" required helper="Directory inside the container." />
|
label="Destination Directory" required helper="Directory inside the container." />
|
||||||
<x-forms.button type="submit" @click="modalOpen=false">
|
<x-forms.button type="submit" @click="modalOpen=false">
|
||||||
Save
|
Add
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<div class="w-full" x-data="{
|
<div class="w-full" x-data="{
|
||||||
open: false,
|
open: false,
|
||||||
search: '{{ $serverTimezone ?: '' }}',
|
search: '{{ $serverTimezone ?: '' }}',
|
||||||
timezones: @js($timezones),
|
timezones: @js($this->timezones),
|
||||||
placeholder: '{{ $serverTimezone ? 'Search timezone...' : 'Select Server Timezone' }}',
|
placeholder: '{{ $serverTimezone ? 'Search timezone...' : 'Select Server Timezone' }}',
|
||||||
init() {
|
init() {
|
||||||
this.$watch('search', value => {
|
this.$watch('search', value => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="w-full" x-data="{
|
<div class="w-full" x-data="{
|
||||||
open: false,
|
open: false,
|
||||||
search: '{{ $settings->instance_timezone ?: '' }}',
|
search: '{{ $settings->instance_timezone ?: '' }}',
|
||||||
timezones: @js($timezones),
|
timezones: @js($this->timezones),
|
||||||
placeholder: '{{ $settings->instance_timezone ? 'Search timezone...' : 'Select Server Timezone' }}',
|
placeholder: '{{ $settings->instance_timezone ? 'Search timezone...' : 'Select Server Timezone' }}',
|
||||||
init() {
|
init() {
|
||||||
this.$watch('search', value => {
|
this.$watch('search', value => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<x-forms.select wire:model.live="selectedTeamId" label="Current Team">
|
<x-forms.select wire:model.live="selectedTeamId" label="Current Team">
|
||||||
<option value="default" disabled selected>Switch team</option>
|
<option value="default" disabled selected>Switch team</option>
|
||||||
@foreach (auth()->user()->teams as $team)
|
@foreach (auth()->user()->teams as $team)
|
||||||
<option value="{{ $team->id }}">{{ $team->name }}</option>
|
<option value="{{ $team->id }}">{{ $team->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</x-forms.select>
|
</x-forms.select>
|
||||||
|
|||||||
27
templates/compose/plex.yaml
Normal file
27
templates/compose/plex.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# documentation: https://docs.linuxserver.io/images/docker-plex/
|
||||||
|
# slogan: Plex organizes video, music and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices.
|
||||||
|
# tags: media, server, movies, tv, music
|
||||||
|
# logo: svgs/plex.svg
|
||||||
|
# port: 32400
|
||||||
|
|
||||||
|
services:
|
||||||
|
plex:
|
||||||
|
image: lscr.io/linuxserver/plex:latest
|
||||||
|
environment:
|
||||||
|
- SERVICE_FQDN_PLEX_32400
|
||||||
|
- _APP_URL=$SERVICE_FQDN_PLEX
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- TZ=${TZ:-America/Toronto}
|
||||||
|
- PLEX_CLAIM=${PLEX_CLAIM}
|
||||||
|
#devices:
|
||||||
|
# - "/dev/dri:/dev/dri"
|
||||||
|
volumes:
|
||||||
|
- plex-config:/config
|
||||||
|
- plex-tv:/tv
|
||||||
|
- plex-movies:/movies
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:32400/identity"]
|
||||||
|
interval: 2s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 15
|
||||||
@@ -2170,6 +2170,21 @@
|
|||||||
"logo": "svgs/plane.svg",
|
"logo": "svgs/plane.svg",
|
||||||
"minversion": "0.0.0"
|
"minversion": "0.0.0"
|
||||||
},
|
},
|
||||||
|
"plex": {
|
||||||
|
"documentation": "https://docs.linuxserver.io/images/docker-plex/?utm_source=coolify.io",
|
||||||
|
"slogan": "Plex organizes video, music and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices.",
|
||||||
|
"compose": "c2VydmljZXM6CiAgcGxleDoKICAgIGltYWdlOiAnbHNjci5pby9saW51eHNlcnZlci9wbGV4OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QTEVYXzMyNDAwCiAgICAgIC0gX0FQUF9VUkw9JFNFUlZJQ0VfRlFETl9QTEVYCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gJ1RaPSR7VFo6LUFtZXJpY2EvVG9yb250b30nCiAgICAgIC0gJ1BMRVhfQ0xBSU09JHtQTEVYX0NMQUlNfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BsZXgtY29uZmlnOi9jb25maWcnCiAgICAgIC0gJ3BsZXgtdHY6L3R2JwogICAgICAtICdwbGV4LW1vdmllczovbW92aWVzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjMyNDAwL2lkZW50aXR5JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==",
|
||||||
|
"tags": [
|
||||||
|
"media",
|
||||||
|
"server",
|
||||||
|
"movies",
|
||||||
|
"tv",
|
||||||
|
"music"
|
||||||
|
],
|
||||||
|
"logo": "svgs/plex.svg",
|
||||||
|
"minversion": "0.0.0",
|
||||||
|
"port": "32400"
|
||||||
|
},
|
||||||
"plunk": {
|
"plunk": {
|
||||||
"documentation": "https://docs.useplunk.com/getting-started/introduction?utm_source=coolify.io",
|
"documentation": "https://docs.useplunk.com/getting-started/introduction?utm_source=coolify.io",
|
||||||
"slogan": "Plunk, The Open-Source Email Platform for AWS",
|
"slogan": "Plunk, The Open-Source Email Platform for AWS",
|
||||||
|
|||||||
57
tests/Feature/ExecuteContainerCommandTest.php
Normal file
57
tests/Feature/ExecuteContainerCommandTest.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Application;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\User;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Tests\Traits\HandlesTestDatabase;
|
||||||
|
|
||||||
|
class ExecuteContainerCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use HandlesTestDatabase;
|
||||||
|
|
||||||
|
private $user;
|
||||||
|
|
||||||
|
private $team;
|
||||||
|
|
||||||
|
private $server;
|
||||||
|
|
||||||
|
private $application;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Only set up database for tests that need it
|
||||||
|
if ($this->shouldSetUpDatabase()) {
|
||||||
|
$this->setUpTestDatabase();
|
||||||
|
}
|
||||||
|
// Create test data
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
$this->team = $this->user->teams()->first();
|
||||||
|
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
$this->application = Application::factory()->create();
|
||||||
|
|
||||||
|
// Login the user
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
if ($this->shouldSetUpDatabase()) {
|
||||||
|
$this->tearDownTestDatabase();
|
||||||
|
}
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldSetUpDatabase(): bool
|
||||||
|
{
|
||||||
|
return in_array($this->name(), [
|
||||||
|
'it_allows_valid_container_access',
|
||||||
|
'it_prevents_cross_server_container_access',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
tests/Traits/HandlesTestDatabase.php
Normal file
78
tests/Traits/HandlesTestDatabase.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Traits;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
trait HandlesTestDatabase
|
||||||
|
{
|
||||||
|
protected function setUpTestDatabase(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Create test database if it doesn't exist
|
||||||
|
$database = config('database.connections.testing.database');
|
||||||
|
$this->createTestDatabase($database);
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
Artisan::call('migrate:fresh', [
|
||||||
|
'--database' => 'testing',
|
||||||
|
'--seed' => false,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->tearDownTestDatabase();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDownTestDatabase(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Drop test database
|
||||||
|
$database = config('database.connections.testing.database');
|
||||||
|
$this->dropTestDatabase($database);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log error but don't throw
|
||||||
|
error_log('Failed to tear down test database: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createTestDatabase($database)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Connect to postgres database to create/drop test database
|
||||||
|
config(['database.connections.pgsql.database' => 'postgres']);
|
||||||
|
DB::purge('pgsql');
|
||||||
|
DB::reconnect('pgsql');
|
||||||
|
|
||||||
|
// Drop if exists and create new database
|
||||||
|
DB::connection('pgsql')->statement("DROP DATABASE IF EXISTS $database WITH (FORCE);");
|
||||||
|
DB::connection('pgsql')->statement("CREATE DATABASE $database;");
|
||||||
|
|
||||||
|
// Switch back to testing connection
|
||||||
|
DB::disconnect('pgsql');
|
||||||
|
DB::reconnect('testing');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->tearDownTestDatabase();
|
||||||
|
throw new \Exception('Could not create test database: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function dropTestDatabase($database)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Connect to postgres database to drop test database
|
||||||
|
config(['database.connections.pgsql.database' => 'postgres']);
|
||||||
|
DB::purge('pgsql');
|
||||||
|
DB::reconnect('pgsql');
|
||||||
|
|
||||||
|
// Drop the test database
|
||||||
|
DB::connection('pgsql')->statement("DROP DATABASE IF EXISTS $database WITH (FORCE);");
|
||||||
|
|
||||||
|
DB::disconnect('pgsql');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log error but don't throw
|
||||||
|
error_log('Failed to drop test database: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"coolify": {
|
"coolify": {
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.374"
|
"version": "4.0.0-beta.375"
|
||||||
},
|
},
|
||||||
"nightly": {
|
"nightly": {
|
||||||
"version": "4.0.0-beta.375"
|
"version": "4.0.0-beta.376"
|
||||||
},
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"version": "1.0.4"
|
"version": "1.0.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user