feat: public database

This commit is contained in:
Andras Bacsai 2023-09-07 13:23:34 +02:00
parent db5ecf07bd
commit 77e3208f00
9 changed files with 132 additions and 6 deletions

1
.gitignore vendored
View File

@ -31,3 +31,4 @@ _ide_helper.php
_ide_helper_models.php _ide_helper_models.php
.rnd .rnd
/.ssh /.ssh
scripts/load-test/*

View File

@ -42,8 +42,13 @@ public function stop()
["docker rm -f {$this->database->uuid}"], ["docker rm -f {$this->database->uuid}"],
$this->database->destination->server $this->database->destination->server
); );
if ($this->database->is_public) {
stopPostgresProxy($this->database);
$this->database->is_public = false;
}
$this->database->status = 'stopped'; $this->database->status = 'stopped';
$this->database->save(); $this->database->save();
$this->emit('refresh');
// $this->database->environment->project->team->notify(new StatusChanged($this->database)); // $this->database->environment->project->team->notify(new StatusChanged($this->database));
} }

View File

@ -12,6 +12,7 @@ class General extends Component
public StandalonePostgresql $database; public StandalonePostgresql $database;
public string $new_filename; public string $new_filename;
public string $new_content; public string $new_content;
public string $db_url;
protected $listeners = ['refresh', 'save_init_script', 'delete_init_script']; protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
@ -26,6 +27,8 @@ class General extends Component
'database.init_scripts' => 'nullable', 'database.init_scripts' => 'nullable',
'database.image' => 'required', 'database.image' => 'required',
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@ -38,8 +41,43 @@ class General extends Component
'database.init_scripts' => 'Init Scripts', 'database.init_scripts' => 'Init Scripts',
'database.image' => 'Image', 'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping', 'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
]; ];
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->ip}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
startPostgresProxy($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
stopPostgresProxy($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->database->save();
} catch(Exception $e) {
$this->database->is_public = !$this->database->is_public;
return general_error_handler(err: $e, that: $this);
}
}
public function save_init_script($script) public function save_init_script($script)
{ {
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']); $this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']);

View File

@ -20,7 +20,7 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
} }
return StandalonePostgresql::create([ return StandalonePostgresql::create([
'name' => generate_database_name('postgresql'), 'name' => generate_database_name('postgresql'),
'postgres_password' => \Illuminate\Support\Str::password(), 'postgres_password' => \Illuminate\Support\Str::password(symbols: false),
'environment_id' => $environment_id, 'environment_id' => $environment_id,
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(), 'destination_type' => $destination->getMorphClass(),

View File

@ -1,6 +1,7 @@
<?php <?php
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
function get_proxy_path() function get_proxy_path()
@ -166,3 +167,75 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server)
} }
} }
} }
function startPostgresProxy(StandalonePostgresql $database)
{
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:5432;
}
}
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'volumes' => [
"$configuration_dir/nginx.conf:/etc/nginx/nginx.conf:ro",
],
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'stat /etc/nginx/nginx.conf || exit 1',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 3,
'start_period' => '1s'
],
]
],
'networks' => [
$database->destination->network => [
'external' => true,
'name' => $database->destination->network,
'attachable' => true,
]
]
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
instant_remote_process([
"mkdir -p $configuration_dir",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up -d >/dev/null",
], $database->destination->server);
}
function stopPostgresProxy(StandalonePostgresql $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
}

View File

@ -28,6 +28,10 @@ function database_configuration_dir(): string
{ {
return '/data/coolify/databases'; return '/data/coolify/databases';
} }
function database_proxy_dir($uuid): string
{
return "/data/coolify/databases/$uuid/proxy";
}
function backup_dir(): string function backup_dir(): string
{ {

View File

@ -24,7 +24,7 @@ class="w-4 h-4 stroke-current">
@endif @endif
</span> </span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }} <input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}' @if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model.defer={{ $id }} @else wire:model.defer={{ $value ?? $id }} @endif /> wire:model.defer={{ $id }} @else wire:model.defer={{ $value ?? $id }} @endif />
</label> </label>
</div> </div>

View File

@ -51,10 +51,15 @@
<x-forms.input label="Host Auth Method" id="database.postgres_host_auth_method" <x-forms.input label="Host Auth Method" id="database.postgres_host_auth_method"
placeholder="If empty, use default. See in docker docs." /> placeholder="If empty, use default. See in docker docs." />
</div> </div>
<div class=""> <div class="flex flex-col gap-2">
<h3 class="py-2">Network</h3> <h3 class="py-2">Network</h3>
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings" <div class="flex items-end gap-2">
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" /> helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{$database->is_public}}" id="database.public_port" label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Postgres URL" readonly wire:model="db_url" />
</div> </div>
</form> </form>
<div class="pb-16"> <div class="pb-16">

View File

@ -22,7 +22,7 @@
class="relative flex duration-300 transform transition ease-in-out max-w-md w-full pointer-events-auto {{ $position->is('center') ? 'text-center' : 'text-left' }}" class="relative flex duration-300 transform transition ease-in-out max-w-md w-full pointer-events-auto {{ $position->is('center') ? 'text-center' : 'text-left' }}"
:class="toast.select({ error: 'text-white', info: 'text-white', success: 'text-white', warning: 'text-white' })" :class="toast.select({ error: 'text-white', info: 'text-white', success: 'text-white', warning: 'text-white' })"
> >
<i class=" flex items-center gap-2 select-none not-italic pr-6 pl-4 py-3 rounded shadow-lg w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}" <i class=" flex items-center gap-2 select-none not-italic pr-6 pl-4 py-3 rounded shadow-lg w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}"
:class="toast.select({ :class="toast.select({
error: 'bg-coolgray-300', error: 'bg-coolgray-300',
info: 'bg-coolgray-300', info: 'bg-coolgray-300',
@ -71,7 +71,7 @@ class="relative flex duration-300 transform transition ease-in-out max-w-md w-fu
</svg> </svg>
</div> </div>
</template> </template>
<span x-html="toast.message" /> <span x-html="toast.message" class="w-full pr-10 break-words" />
</i> </i>
@if ($closeable) @if ($closeable)