Merge pull request #5027 from peaklabs-dev/feat-db-ssl
feat: Full SSL Support for all Databases
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Actions\Database;
|
namespace App\Actions\Database;
|
||||||
|
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneDragonfly;
|
use App\Models\StandaloneDragonfly;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
@@ -16,24 +18,70 @@ class StartDragonfly
|
|||||||
|
|
||||||
public string $configuration_dir;
|
public string $configuration_dir;
|
||||||
|
|
||||||
|
private ?SslCertificate $ssl_certificate = null;
|
||||||
|
|
||||||
public function handle(StandaloneDragonfly $database)
|
public function handle(StandaloneDragonfly $database)
|
||||||
{
|
{
|
||||||
$this->database = $database;
|
$this->database = $database;
|
||||||
|
|
||||||
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
|
|
||||||
|
|
||||||
$container_name = $this->database->uuid;
|
$container_name = $this->database->uuid;
|
||||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting database.'",
|
"echo 'Starting database.'",
|
||||||
|
"echo 'Creating directories.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
|
"echo 'Directories created successfully.'",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (! $this->database->enable_ssl) {
|
||||||
|
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||||
|
$this->database->sslCertificates()->delete();
|
||||||
|
$this->database->fileStorages()
|
||||||
|
->where('resource_type', $this->database->getMorphClass())
|
||||||
|
->where('resource_id', $this->database->id)
|
||||||
|
->get()
|
||||||
|
->filter(function ($storage) {
|
||||||
|
return in_array($storage->mount_path, [
|
||||||
|
'/etc/dragonfly/certs/server.crt',
|
||||||
|
'/etc/dragonfly/certs/server.key',
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->each(function ($storage) {
|
||||||
|
$storage->delete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||||
|
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$server = $this->database->destination->server;
|
||||||
|
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $this->ssl_certificate) {
|
||||||
|
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||||
|
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||||
|
commonName: $this->database->uuid,
|
||||||
|
resourceType: $this->database->getMorphClass(),
|
||||||
|
resourceId: $this->database->id,
|
||||||
|
serverId: $server->id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $this->configuration_dir,
|
||||||
|
mountPath: '/etc/dragonfly/certs',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$container_name = $this->database->uuid;
|
||||||
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||||
$environment_variables = $this->generate_environment_variables();
|
$environment_variables = $this->generate_environment_variables();
|
||||||
|
$startCommand = $this->buildStartCommand();
|
||||||
|
|
||||||
$docker_compose = [
|
$docker_compose = [
|
||||||
'services' => [
|
'services' => [
|
||||||
@@ -70,27 +118,55 @@ class StartDragonfly
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (! is_null($this->database->limits_cpuset)) {
|
if (! is_null($this->database->limits_cpuset)) {
|
||||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($this->database->ports_mappings_array) > 0) {
|
if (count($this->database->ports_mappings_array) > 0) {
|
||||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||||
|
|
||||||
if (count($persistent_storages) > 0) {
|
if (count($persistent_storages) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
|
$persistent_storages
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($persistent_file_volumes) > 0) {
|
if (count($persistent_file_volumes) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
return "$item->fs_path:$item->mount_path";
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
})->toArray();
|
$persistent_file_volumes->map(function ($item) {
|
||||||
|
return "$item->fs_path:$item->mount_path";
|
||||||
|
})->toArray()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'type' => 'bind',
|
||||||
|
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||||
|
'target' => '/etc/dragonfly/certs/coolify-ca.crt',
|
||||||
|
'read_only' => true,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Add custom docker run options
|
// Add custom docker run options
|
||||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||||
@@ -102,12 +178,32 @@ class StartDragonfly
|
|||||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||||
|
}
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||||
$this->commands[] = "echo 'Database started.'";
|
$this->commands[] = "echo 'Database started.'";
|
||||||
|
|
||||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildStartCommand(): string
|
||||||
|
{
|
||||||
|
$command = "dragonfly --requirepass {$this->database->dragonfly_password}";
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$sslArgs = [
|
||||||
|
'--tls',
|
||||||
|
'--tls_cert_file /etc/dragonfly/certs/server.crt',
|
||||||
|
'--tls_key_file /etc/dragonfly/certs/server.key',
|
||||||
|
'--tls_ca_cert_file /etc/dragonfly/certs/coolify-ca.crt',
|
||||||
|
];
|
||||||
|
$command .= ' '.implode(' ', $sslArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $command;
|
||||||
|
}
|
||||||
|
|
||||||
private function generate_local_persistent_volumes()
|
private function generate_local_persistent_volumes()
|
||||||
{
|
{
|
||||||
$local_persistent_volumes = [];
|
$local_persistent_volumes = [];
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Actions\Database;
|
namespace App\Actions\Database;
|
||||||
|
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneKeydb;
|
use App\Models\StandaloneKeydb;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
@@ -17,26 +19,73 @@ class StartKeydb
|
|||||||
|
|
||||||
public string $configuration_dir;
|
public string $configuration_dir;
|
||||||
|
|
||||||
|
private ?SslCertificate $ssl_certificate = null;
|
||||||
|
|
||||||
public function handle(StandaloneKeydb $database)
|
public function handle(StandaloneKeydb $database)
|
||||||
{
|
{
|
||||||
$this->database = $database;
|
$this->database = $database;
|
||||||
|
|
||||||
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
|
|
||||||
|
|
||||||
$container_name = $this->database->uuid;
|
$container_name = $this->database->uuid;
|
||||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting database.'",
|
"echo 'Starting database.'",
|
||||||
|
"echo 'Creating directories.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
|
"echo 'Directories created successfully.'",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (! $this->database->enable_ssl) {
|
||||||
|
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||||
|
$this->database->sslCertificates()->delete();
|
||||||
|
$this->database->fileStorages()
|
||||||
|
->where('resource_type', $this->database->getMorphClass())
|
||||||
|
->where('resource_id', $this->database->id)
|
||||||
|
->get()
|
||||||
|
->filter(function ($storage) {
|
||||||
|
return in_array($storage->mount_path, [
|
||||||
|
'/etc/keydb/certs/server.crt',
|
||||||
|
'/etc/keydb/certs/server.key',
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->each(function ($storage) {
|
||||||
|
$storage->delete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||||
|
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$server = $this->database->destination->server;
|
||||||
|
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $this->ssl_certificate) {
|
||||||
|
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||||
|
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||||
|
commonName: $this->database->uuid,
|
||||||
|
resourceType: $this->database->getMorphClass(),
|
||||||
|
resourceId: $this->database->id,
|
||||||
|
serverId: $server->id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $this->configuration_dir,
|
||||||
|
mountPath: '/etc/keydb/certs',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$container_name = $this->database->uuid;
|
||||||
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||||
$environment_variables = $this->generate_environment_variables();
|
$environment_variables = $this->generate_environment_variables();
|
||||||
$this->add_custom_keydb();
|
$this->add_custom_keydb();
|
||||||
|
|
||||||
|
$startCommand = $this->buildStartCommand();
|
||||||
|
|
||||||
$docker_compose = [
|
$docker_compose = [
|
||||||
'services' => [
|
'services' => [
|
||||||
$container_name => [
|
$container_name => [
|
||||||
@@ -72,34 +121,67 @@ class StartKeydb
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (! is_null($this->database->limits_cpuset)) {
|
if (! is_null($this->database->limits_cpuset)) {
|
||||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($this->database->ports_mappings_array) > 0) {
|
if (count($this->database->ports_mappings_array) > 0) {
|
||||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||||
|
|
||||||
if (count($persistent_storages) > 0) {
|
if (count($persistent_storages) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
|
$persistent_storages
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($persistent_file_volumes) > 0) {
|
if (count($persistent_file_volumes) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
return "$item->fs_path:$item->mount_path";
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
})->toArray();
|
$persistent_file_volumes->map(function ($item) {
|
||||||
|
return "$item->fs_path:$item->mount_path";
|
||||||
|
})->toArray()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
|
if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
'type' => 'bind',
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
'source' => $this->configuration_dir.'/keydb.conf',
|
[
|
||||||
'target' => '/etc/keydb/keydb.conf',
|
[
|
||||||
'read_only' => true,
|
'type' => 'bind',
|
||||||
];
|
'source' => $this->configuration_dir.'/keydb.conf',
|
||||||
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
|
'target' => '/etc/keydb/keydb.conf',
|
||||||
|
'read_only' => true,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'type' => 'bind',
|
||||||
|
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||||
|
'target' => '/etc/keydb/certs/coolify-ca.crt',
|
||||||
|
'read_only' => true,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add custom docker run options
|
// Add custom docker run options
|
||||||
@@ -112,6 +194,9 @@ class StartKeydb
|
|||||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||||
|
}
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||||
$this->commands[] = "echo 'Database started.'";
|
$this->commands[] = "echo 'Database started.'";
|
||||||
|
|
||||||
@@ -177,4 +262,36 @@ class StartKeydb
|
|||||||
instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
|
instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
|
||||||
Storage::disk('local')->delete("tmp/keydb.conf_{$this->database->uuid}");
|
Storage::disk('local')->delete("tmp/keydb.conf_{$this->database->uuid}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildStartCommand(): string
|
||||||
|
{
|
||||||
|
$hasKeydbConf = ! is_null($this->database->keydb_conf) && ! empty($this->database->keydb_conf);
|
||||||
|
$keydbConfPath = '/etc/keydb/keydb.conf';
|
||||||
|
|
||||||
|
if ($hasKeydbConf) {
|
||||||
|
$confContent = $this->database->keydb_conf;
|
||||||
|
$hasRequirePass = str_contains($confContent, 'requirepass');
|
||||||
|
|
||||||
|
if ($hasRequirePass) {
|
||||||
|
$command = "keydb-server $keydbConfPath";
|
||||||
|
} else {
|
||||||
|
$command = "keydb-server $keydbConfPath --requirepass {$this->database->keydb_password}";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$command = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$sslArgs = [
|
||||||
|
'--tls-port 6380',
|
||||||
|
'--tls-cert-file /etc/keydb/certs/server.crt',
|
||||||
|
'--tls-key-file /etc/keydb/certs/server.key',
|
||||||
|
'--tls-ca-cert-file /etc/keydb/certs/coolify-ca.crt',
|
||||||
|
'--tls-auth-clients optional',
|
||||||
|
];
|
||||||
|
$command .= ' '.implode(' ', $sslArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $command;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Actions\Database;
|
namespace App\Actions\Database;
|
||||||
|
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneMariadb;
|
use App\Models\StandaloneMariadb;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
@@ -16,6 +18,8 @@ class StartMariadb
|
|||||||
|
|
||||||
public string $configuration_dir;
|
public string $configuration_dir;
|
||||||
|
|
||||||
|
private ?SslCertificate $ssl_certificate = null;
|
||||||
|
|
||||||
public function handle(StandaloneMariadb $database)
|
public function handle(StandaloneMariadb $database)
|
||||||
{
|
{
|
||||||
$this->database = $database;
|
$this->database = $database;
|
||||||
@@ -25,9 +29,53 @@ class StartMariadb
|
|||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting database.'",
|
"echo 'Starting database.'",
|
||||||
|
"echo 'Creating directories.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
|
"echo 'Directories created successfully.'",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (! $this->database->enable_ssl) {
|
||||||
|
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$this->database->sslCertificates()->delete();
|
||||||
|
|
||||||
|
$this->database->fileStorages()
|
||||||
|
->where('resource_type', $this->database->getMorphClass())
|
||||||
|
->where('resource_id', $this->database->id)
|
||||||
|
->get()
|
||||||
|
->filter(function ($storage) {
|
||||||
|
return in_array($storage->mount_path, [
|
||||||
|
'/etc/mysql/certs/server.crt',
|
||||||
|
'/etc/mysql/certs/server.key',
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->each(function ($storage) {
|
||||||
|
$storage->delete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||||
|
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$server = $this->database->destination->server;
|
||||||
|
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $this->ssl_certificate) {
|
||||||
|
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||||
|
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||||
|
commonName: $this->database->uuid,
|
||||||
|
resourceType: $this->database->getMorphClass(),
|
||||||
|
resourceId: $this->database->id,
|
||||||
|
serverId: $server->id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $this->configuration_dir,
|
||||||
|
mountPath: '/etc/mysql/certs',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||||
@@ -67,38 +115,81 @@ class StartMariadb
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (! is_null($this->database->limits_cpuset)) {
|
if (! is_null($this->database->limits_cpuset)) {
|
||||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($this->database->ports_mappings_array) > 0) {
|
if (count($this->database->ports_mappings_array) > 0) {
|
||||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||||
}
|
}
|
||||||
if (count($persistent_storages) > 0) {
|
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
|
||||||
}
|
|
||||||
if (count($persistent_file_volumes) > 0) {
|
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
|
||||||
return "$item->fs_path:$item->mount_path";
|
|
||||||
})->toArray();
|
|
||||||
}
|
|
||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||||
|
|
||||||
|
if (count($persistent_storages) > 0) {
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
|
$persistent_storages
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($persistent_file_volumes) > 0) {
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
|
$persistent_file_volumes->map(function ($item) {
|
||||||
|
return "$item->fs_path:$item->mount_path";
|
||||||
|
})->toArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'type' => 'bind',
|
||||||
|
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||||
|
'target' => '/etc/mysql/certs/coolify-ca.crt',
|
||||||
|
'read_only' => true,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
|
if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
'type' => 'bind',
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
[
|
||||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
[
|
||||||
'read_only' => true,
|
'type' => 'bind',
|
||||||
];
|
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||||
|
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||||
|
'read_only' => true,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add custom docker run options
|
// Add custom docker run options
|
||||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$docker_compose['services'][$container_name]['command'] = [
|
||||||
|
'mysqld',
|
||||||
|
'--ssl-cert=/etc/mysql/certs/server.crt',
|
||||||
|
'--ssl-key=/etc/mysql/certs/server.key',
|
||||||
|
'--ssl-ca=/etc/mysql/certs/coolify-ca.crt',
|
||||||
|
'--require-secure-transport=1',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||||
$docker_compose_base64 = base64_encode($docker_compose);
|
$docker_compose_base64 = base64_encode($docker_compose);
|
||||||
@@ -109,6 +200,9 @@ class StartMariadb
|
|||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||||
$this->commands[] = "echo 'Database started.'";
|
$this->commands[] = "echo 'Database started.'";
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$this->commands[] = executeInDocker($this->database->uuid, 'chown mysql:mysql /etc/mysql/certs/server.crt /etc/mysql/certs/server.key');
|
||||||
|
}
|
||||||
|
|
||||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Actions\Database;
|
namespace App\Actions\Database;
|
||||||
|
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneMongodb;
|
use App\Models\StandaloneMongodb;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
@@ -16,6 +18,8 @@ class StartMongodb
|
|||||||
|
|
||||||
public string $configuration_dir;
|
public string $configuration_dir;
|
||||||
|
|
||||||
|
private ?SslCertificate $ssl_certificate = null;
|
||||||
|
|
||||||
public function handle(StandaloneMongodb $database)
|
public function handle(StandaloneMongodb $database)
|
||||||
{
|
{
|
||||||
$this->database = $database;
|
$this->database = $database;
|
||||||
@@ -24,16 +28,59 @@ class StartMongodb
|
|||||||
|
|
||||||
$container_name = $this->database->uuid;
|
$container_name = $this->database->uuid;
|
||||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
$this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
|
$this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting database.'",
|
"echo 'Starting database.'",
|
||||||
|
"echo 'Creating directories.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
|
"echo 'Directories created successfully.'",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (! $this->database->enable_ssl) {
|
||||||
|
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$this->database->sslCertificates()->delete();
|
||||||
|
|
||||||
|
$this->database->fileStorages()
|
||||||
|
->where('resource_type', $this->database->getMorphClass())
|
||||||
|
->where('resource_id', $this->database->id)
|
||||||
|
->get()
|
||||||
|
->filter(function ($storage) {
|
||||||
|
return in_array($storage->mount_path, [
|
||||||
|
'/etc/mongo/certs/server.pem',
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->each(function ($storage) {
|
||||||
|
$storage->delete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||||
|
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$server = $this->database->destination->server;
|
||||||
|
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $this->ssl_certificate) {
|
||||||
|
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||||
|
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||||
|
commonName: $this->database->uuid,
|
||||||
|
resourceType: $this->database->getMorphClass(),
|
||||||
|
resourceId: $this->database->id,
|
||||||
|
serverId: $server->id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $this->configuration_dir,
|
||||||
|
mountPath: '/etc/mongo/certs',
|
||||||
|
isPemKeyFileRequired: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||||
@@ -79,47 +126,118 @@ class StartMongodb
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (! is_null($this->database->limits_cpuset)) {
|
if (! is_null($this->database->limits_cpuset)) {
|
||||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($this->database->ports_mappings_array) > 0) {
|
if (count($this->database->ports_mappings_array) > 0) {
|
||||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||||
|
|
||||||
if (count($persistent_storages) > 0) {
|
if (count($persistent_storages) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
|
$persistent_storages
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($persistent_file_volumes) > 0) {
|
if (count($persistent_file_volumes) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
return "$item->fs_path:$item->mount_path";
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
})->toArray();
|
$persistent_file_volumes->map(function ($item) {
|
||||||
|
return "$item->fs_path:$item->mount_path";
|
||||||
|
})->toArray()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
|
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
if (! empty($this->database->mongo_conf)) {
|
||||||
'type' => 'bind',
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
'source' => $this->configuration_dir.'/mongod.conf',
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
'target' => '/etc/mongo/mongod.conf',
|
[[
|
||||||
'read_only' => true,
|
'type' => 'bind',
|
||||||
];
|
'source' => $this->configuration_dir.'/mongod.conf',
|
||||||
$docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
|
'target' => '/etc/mongo/mongod.conf',
|
||||||
|
'read_only' => true,
|
||||||
|
]]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->add_default_database();
|
$this->add_default_database();
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
|
||||||
'type' => 'bind',
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
'target' => '/docker-entrypoint-initdb.d',
|
[[
|
||||||
'read_only' => true,
|
'type' => 'bind',
|
||||||
];
|
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
|
||||||
|
'target' => '/docker-entrypoint-initdb.d',
|
||||||
|
'read_only' => true,
|
||||||
|
]]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'type' => 'bind',
|
||||||
|
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||||
|
'target' => '/etc/mongo/certs/ca.pem',
|
||||||
|
'read_only' => true,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Add custom docker run options
|
// Add custom docker run options
|
||||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$commandParts = ['mongod'];
|
||||||
|
|
||||||
|
$sslConfig = match ($this->database->ssl_mode) {
|
||||||
|
'allow' => [
|
||||||
|
'--tlsMode=allowTLS',
|
||||||
|
'--tlsAllowConnectionsWithoutCertificates',
|
||||||
|
'--tlsAllowInvalidHostnames',
|
||||||
|
],
|
||||||
|
'prefer' => [
|
||||||
|
'--tlsMode=preferTLS',
|
||||||
|
'--tlsAllowConnectionsWithoutCertificates',
|
||||||
|
'--tlsAllowInvalidHostnames',
|
||||||
|
],
|
||||||
|
'require' => [
|
||||||
|
'--tlsMode=requireTLS',
|
||||||
|
'--tlsAllowConnectionsWithoutCertificates',
|
||||||
|
'--tlsAllowInvalidHostnames',
|
||||||
|
],
|
||||||
|
'verify-full' => [
|
||||||
|
'--tlsMode=requireTLS',
|
||||||
|
'--tlsAllowInvalidHostnames',
|
||||||
|
],
|
||||||
|
default => [],
|
||||||
|
};
|
||||||
|
|
||||||
|
$commandParts = [...$commandParts, ...$sslConfig];
|
||||||
|
$commandParts[] = '--tlsCAFile';
|
||||||
|
$commandParts[] = '/etc/mongo/certs/ca.pem';
|
||||||
|
$commandParts[] = '--tlsCertificateKeyFile';
|
||||||
|
$commandParts[] = '/etc/mongo/certs/server.pem';
|
||||||
|
|
||||||
|
$docker_compose['services'][$container_name]['command'] = $commandParts;
|
||||||
|
}
|
||||||
|
|
||||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||||
$docker_compose_base64 = base64_encode($docker_compose);
|
$docker_compose_base64 = base64_encode($docker_compose);
|
||||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||||
@@ -128,6 +246,9 @@ class StartMongodb
|
|||||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$this->commands[] = executeInDocker($this->database->uuid, 'chown mongodb:mongodb /etc/mongo/certs/server.pem');
|
||||||
|
}
|
||||||
$this->commands[] = "echo 'Database started.'";
|
$this->commands[] = "echo 'Database started.'";
|
||||||
|
|
||||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Actions\Database;
|
namespace App\Actions\Database;
|
||||||
|
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneMysql;
|
use App\Models\StandaloneMysql;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
@@ -16,6 +18,8 @@ class StartMysql
|
|||||||
|
|
||||||
public string $configuration_dir;
|
public string $configuration_dir;
|
||||||
|
|
||||||
|
private ?SslCertificate $ssl_certificate = null;
|
||||||
|
|
||||||
public function handle(StandaloneMysql $database)
|
public function handle(StandaloneMysql $database)
|
||||||
{
|
{
|
||||||
$this->database = $database;
|
$this->database = $database;
|
||||||
@@ -25,9 +29,53 @@ class StartMysql
|
|||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting database.'",
|
"echo 'Starting database.'",
|
||||||
|
"echo 'Creating directories.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
|
"echo 'Directories created successfully.'",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (! $this->database->enable_ssl) {
|
||||||
|
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$this->database->sslCertificates()->delete();
|
||||||
|
|
||||||
|
$this->database->fileStorages()
|
||||||
|
->where('resource_type', $this->database->getMorphClass())
|
||||||
|
->where('resource_id', $this->database->id)
|
||||||
|
->get()
|
||||||
|
->filter(function ($storage) {
|
||||||
|
return in_array($storage->mount_path, [
|
||||||
|
'/etc/mysql/certs/server.crt',
|
||||||
|
'/etc/mysql/certs/server.key',
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->each(function ($storage) {
|
||||||
|
$storage->delete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||||
|
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$server = $this->database->destination->server;
|
||||||
|
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $this->ssl_certificate) {
|
||||||
|
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||||
|
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||||
|
commonName: $this->database->uuid,
|
||||||
|
resourceType: $this->database->getMorphClass(),
|
||||||
|
resourceId: $this->database->id,
|
||||||
|
serverId: $server->id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $this->configuration_dir,
|
||||||
|
mountPath: '/etc/mysql/certs',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||||
@@ -67,39 +115,83 @@ class StartMysql
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (! is_null($this->database->limits_cpuset)) {
|
if (! is_null($this->database->limits_cpuset)) {
|
||||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($this->database->ports_mappings_array) > 0) {
|
if (count($this->database->ports_mappings_array) > 0) {
|
||||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||||
|
|
||||||
if (count($persistent_storages) > 0) {
|
if (count($persistent_storages) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
|
$persistent_storages
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($persistent_file_volumes) > 0) {
|
if (count($persistent_file_volumes) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
return "$item->fs_path:$item->mount_path";
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
})->toArray();
|
$persistent_file_volumes->map(function ($item) {
|
||||||
|
return "$item->fs_path:$item->mount_path";
|
||||||
|
})->toArray()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'type' => 'bind',
|
||||||
|
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||||
|
'target' => '/etc/mysql/certs/coolify-ca.crt',
|
||||||
|
'read_only' => true,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
|
if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
'type' => 'bind',
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
[
|
||||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
[
|
||||||
'read_only' => true,
|
'type' => 'bind',
|
||||||
];
|
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||||
|
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||||
|
'read_only' => true,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add custom docker run options
|
// Add custom docker run options
|
||||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$docker_compose['services'][$container_name]['command'] = [
|
||||||
|
'mysqld',
|
||||||
|
'--ssl-cert=/etc/mysql/certs/server.crt',
|
||||||
|
'--ssl-key=/etc/mysql/certs/server.key',
|
||||||
|
'--ssl-ca=/etc/mysql/certs/coolify-ca.crt',
|
||||||
|
'--require-secure-transport=1',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||||
$docker_compose_base64 = base64_encode($docker_compose);
|
$docker_compose_base64 = base64_encode($docker_compose);
|
||||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||||
@@ -108,6 +200,11 @@ class StartMysql
|
|||||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->mysql_user}:{$this->database->mysql_user} /etc/mysql/certs/server.crt /etc/mysql/certs/server.key");
|
||||||
|
}
|
||||||
|
|
||||||
$this->commands[] = "echo 'Database started.'";
|
$this->commands[] = "echo 'Database started.'";
|
||||||
|
|
||||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Actions\Database;
|
namespace App\Actions\Database;
|
||||||
|
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandalonePostgresql;
|
use App\Models\StandalonePostgresql;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
@@ -18,6 +20,8 @@ class StartPostgresql
|
|||||||
|
|
||||||
public string $configuration_dir;
|
public string $configuration_dir;
|
||||||
|
|
||||||
|
private ?SslCertificate $ssl_certificate = null;
|
||||||
|
|
||||||
public function handle(StandalonePostgresql $database)
|
public function handle(StandalonePostgresql $database)
|
||||||
{
|
{
|
||||||
$this->database = $database;
|
$this->database = $database;
|
||||||
@@ -29,10 +33,54 @@ class StartPostgresql
|
|||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting database.'",
|
"echo 'Starting database.'",
|
||||||
|
"echo 'Creating directories.'",
|
||||||
"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/",
|
||||||
|
"echo 'Directories created successfully.'",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (! $this->database->enable_ssl) {
|
||||||
|
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$this->database->sslCertificates()->delete();
|
||||||
|
|
||||||
|
$this->database->fileStorages()
|
||||||
|
->where('resource_type', $this->database->getMorphClass())
|
||||||
|
->where('resource_id', $this->database->id)
|
||||||
|
->get()
|
||||||
|
->filter(function ($storage) {
|
||||||
|
return in_array($storage->mount_path, [
|
||||||
|
'/var/lib/postgresql/certs/server.crt',
|
||||||
|
'/var/lib/postgresql/certs/server.key',
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->each(function ($storage) {
|
||||||
|
$storage->delete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||||
|
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$server = $this->database->destination->server;
|
||||||
|
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $this->ssl_certificate) {
|
||||||
|
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||||
|
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||||
|
commonName: $this->database->uuid,
|
||||||
|
resourceType: $this->database->getMorphClass(),
|
||||||
|
resourceId: $this->database->id,
|
||||||
|
serverId: $server->id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $this->configuration_dir,
|
||||||
|
mountPath: '/var/lib/postgresql/certs',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||||
@@ -77,49 +125,84 @@ class StartPostgresql
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (filled($this->database->limits_cpuset)) {
|
if (filled($this->database->limits_cpuset)) {
|
||||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($this->database->ports_mappings_array) > 0) {
|
if (count($this->database->ports_mappings_array) > 0) {
|
||||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||||
|
|
||||||
if (count($persistent_storages) > 0) {
|
if (count($persistent_storages) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
|
$persistent_storages
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($persistent_file_volumes) > 0) {
|
if (count($persistent_file_volumes) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
return "$item->fs_path:$item->mount_path";
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
})->toArray();
|
$persistent_file_volumes->map(function ($item) {
|
||||||
|
return "$item->fs_path:$item->mount_path";
|
||||||
|
})->toArray()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($this->init_scripts) > 0) {
|
if (count($this->init_scripts) > 0) {
|
||||||
foreach ($this->init_scripts as $init_script) {
|
foreach ($this->init_scripts as $init_script) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
'type' => 'bind',
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
'source' => $init_script,
|
[[
|
||||||
'target' => '/docker-entrypoint-initdb.d/'.basename($init_script),
|
'type' => 'bind',
|
||||||
'read_only' => true,
|
'source' => $init_script,
|
||||||
];
|
'target' => '/docker-entrypoint-initdb.d/'.basename($init_script),
|
||||||
|
'read_only' => true,
|
||||||
|
]]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filled($this->database->postgres_conf)) {
|
if (filled($this->database->postgres_conf)) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
'type' => 'bind',
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
'source' => $this->configuration_dir.'/custom-postgres.conf',
|
[[
|
||||||
'target' => '/etc/postgresql/postgresql.conf',
|
'type' => 'bind',
|
||||||
'read_only' => true,
|
'source' => $this->configuration_dir.'/custom-postgres.conf',
|
||||||
];
|
'target' => '/etc/postgresql/postgresql.conf',
|
||||||
|
'read_only' => true,
|
||||||
|
]]
|
||||||
|
);
|
||||||
$docker_compose['services'][$container_name]['command'] = [
|
$docker_compose['services'][$container_name]['command'] = [
|
||||||
'postgres',
|
'postgres',
|
||||||
'-c',
|
'-c',
|
||||||
'config_file=/etc/postgresql/postgresql.conf',
|
'config_file=/etc/postgresql/postgresql.conf',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$docker_compose['services'][$container_name]['command'] = [
|
||||||
|
'postgres',
|
||||||
|
'-c',
|
||||||
|
'ssl=on',
|
||||||
|
'-c',
|
||||||
|
'ssl_cert_file=/var/lib/postgresql/certs/server.crt',
|
||||||
|
'-c',
|
||||||
|
'ssl_key_file=/var/lib/postgresql/certs/server.key',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Add custom docker run options
|
// Add custom docker run options
|
||||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||||
@@ -132,6 +215,9 @@ class StartPostgresql
|
|||||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->postgres_user}:{$this->database->postgres_user} /var/lib/postgresql/certs/server.key /var/lib/postgresql/certs/server.crt");
|
||||||
|
}
|
||||||
$this->commands[] = "echo 'Database started.'";
|
$this->commands[] = "echo 'Database started.'";
|
||||||
|
|
||||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Actions\Database;
|
namespace App\Actions\Database;
|
||||||
|
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneRedis;
|
use App\Models\StandaloneRedis;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
@@ -17,6 +19,8 @@ class StartRedis
|
|||||||
|
|
||||||
public string $configuration_dir;
|
public string $configuration_dir;
|
||||||
|
|
||||||
|
private ?SslCertificate $ssl_certificate = null;
|
||||||
|
|
||||||
public function handle(StandaloneRedis $database)
|
public function handle(StandaloneRedis $database)
|
||||||
{
|
{
|
||||||
$this->database = $database;
|
$this->database = $database;
|
||||||
@@ -26,9 +30,51 @@ class StartRedis
|
|||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting database.'",
|
"echo 'Starting database.'",
|
||||||
|
"echo 'Creating directories.'",
|
||||||
"mkdir -p $this->configuration_dir",
|
"mkdir -p $this->configuration_dir",
|
||||||
|
"echo 'Directories created successfully.'",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (! $this->database->enable_ssl) {
|
||||||
|
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
|
||||||
|
$this->database->sslCertificates()->delete();
|
||||||
|
$this->database->fileStorages()
|
||||||
|
->where('resource_type', $this->database->getMorphClass())
|
||||||
|
->where('resource_id', $this->database->id)
|
||||||
|
->get()
|
||||||
|
->filter(function ($storage) {
|
||||||
|
return in_array($storage->mount_path, [
|
||||||
|
'/etc/redis/certs/server.crt',
|
||||||
|
'/etc/redis/certs/server.key',
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->each(function ($storage) {
|
||||||
|
$storage->delete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$this->commands[] = "echo 'Setting up SSL for this database.'";
|
||||||
|
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
|
||||||
|
|
||||||
|
$server = $this->database->destination->server;
|
||||||
|
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
$this->ssl_certificate = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $this->ssl_certificate) {
|
||||||
|
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
|
||||||
|
$this->ssl_certificate = SslHelper::generateSslCertificate(
|
||||||
|
commonName: $this->database->uuid,
|
||||||
|
resourceType: $this->database->getMorphClass(),
|
||||||
|
resourceId: $this->database->id,
|
||||||
|
serverId: $server->id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $this->configuration_dir,
|
||||||
|
mountPath: '/etc/redis/certs',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||||
@@ -76,26 +122,55 @@ class StartRedis
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (! is_null($this->database->limits_cpuset)) {
|
if (! is_null($this->database->limits_cpuset)) {
|
||||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($this->database->ports_mappings_array) > 0) {
|
if (count($this->database->ports_mappings_array) > 0) {
|
||||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||||
|
|
||||||
if (count($persistent_storages) > 0) {
|
if (count($persistent_storages) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
|
$persistent_storages
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($persistent_file_volumes) > 0) {
|
if (count($persistent_file_volumes) > 0) {
|
||||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
return "$item->fs_path:$item->mount_path";
|
$docker_compose['services'][$container_name]['volumes'],
|
||||||
})->toArray();
|
$persistent_file_volumes->map(function ($item) {
|
||||||
|
return "$item->fs_path:$item->mount_path";
|
||||||
|
})->toArray()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||||
|
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'type' => 'bind',
|
||||||
|
'source' => '/data/coolify/ssl/coolify-ca.crt',
|
||||||
|
'target' => '/etc/redis/certs/coolify-ca.crt',
|
||||||
|
'read_only' => true,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
|
if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||||
'type' => 'bind',
|
'type' => 'bind',
|
||||||
@@ -116,6 +191,9 @@ class StartRedis
|
|||||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||||
|
}
|
||||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||||
$this->commands[] = "echo 'Database started.'";
|
$this->commands[] = "echo 'Database started.'";
|
||||||
|
|
||||||
@@ -202,6 +280,20 @@ class StartRedis
|
|||||||
$command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
$command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->database->enable_ssl) {
|
||||||
|
$sslArgs = [
|
||||||
|
'--tls-port 6380',
|
||||||
|
'--tls-cert-file /etc/redis/certs/server.crt',
|
||||||
|
'--tls-key-file /etc/redis/certs/server.key',
|
||||||
|
'--tls-ca-cert-file /etc/redis/certs/coolify-ca.crt',
|
||||||
|
'--tls-auth-clients optional',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($sslArgs)) {
|
||||||
|
$command .= ' '.implode(' ', $sslArgs);
|
||||||
|
}
|
||||||
|
|
||||||
return $command;
|
return $command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Actions\Server;
|
namespace App\Actions\Server;
|
||||||
|
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneDocker;
|
use App\Models\StandaloneDocker;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
@@ -17,6 +19,27 @@ class InstallDocker
|
|||||||
if (! $supported_os_type) {
|
if (! $supported_os_type) {
|
||||||
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');
|
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->exists()) {
|
||||||
|
$serverCert = SslHelper::generateSslCertificate(
|
||||||
|
commonName: 'Coolify CA Certificate',
|
||||||
|
serverId: $server->id,
|
||||||
|
isCaCertificate: true,
|
||||||
|
validityDays: 10 * 365
|
||||||
|
);
|
||||||
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$commands = collect([
|
||||||
|
"mkdir -p $caCertPath",
|
||||||
|
"chown -R 9999:root $caCertPath",
|
||||||
|
"chmod -R 700 $caCertPath",
|
||||||
|
"rm -rf $caCertPath/coolify-ca.crt",
|
||||||
|
"echo '{$serverCert->ssl_certificate}' > $caCertPath/coolify-ca.crt",
|
||||||
|
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||||
|
]);
|
||||||
|
remote_process($commands, $server);
|
||||||
|
}
|
||||||
|
|
||||||
$config = base64_encode('{
|
$config = base64_encode('{
|
||||||
"log-driver": "json-file",
|
"log-driver": "json-file",
|
||||||
"log-opts": {
|
"log-opts": {
|
||||||
|
@@ -9,6 +9,7 @@ use App\Jobs\CleanupInstanceStuffsJob;
|
|||||||
use App\Jobs\DatabaseBackupJob;
|
use App\Jobs\DatabaseBackupJob;
|
||||||
use App\Jobs\DockerCleanupJob;
|
use App\Jobs\DockerCleanupJob;
|
||||||
use App\Jobs\PullTemplatesFromCDN;
|
use App\Jobs\PullTemplatesFromCDN;
|
||||||
|
use App\Jobs\RegenerateSslCertJob;
|
||||||
use App\Jobs\ScheduledTaskJob;
|
use App\Jobs\ScheduledTaskJob;
|
||||||
use App\Jobs\ServerCheckJob;
|
use App\Jobs\ServerCheckJob;
|
||||||
use App\Jobs\ServerStorageCheckJob;
|
use App\Jobs\ServerStorageCheckJob;
|
||||||
@@ -83,6 +84,8 @@ class Kernel extends ConsoleKernel
|
|||||||
$this->checkScheduledBackups();
|
$this->checkScheduledBackups();
|
||||||
$this->checkScheduledTasks();
|
$this->checkScheduledTasks();
|
||||||
|
|
||||||
|
$this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily();
|
||||||
|
|
||||||
$this->scheduleInstance->command('cleanup:database --yes')->daily();
|
$this->scheduleInstance->command('cleanup:database --yes')->daily();
|
||||||
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
|
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
|
||||||
}
|
}
|
||||||
|
233
app/Helpers/SslHelper.php
Normal file
233
app/Helpers/SslHelper.php
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
|
class SslHelper
|
||||||
|
{
|
||||||
|
private const DEFAULT_ORGANIZATION_NAME = 'Coolify';
|
||||||
|
|
||||||
|
private const DEFAULT_COUNTRY_NAME = 'XX';
|
||||||
|
|
||||||
|
private const DEFAULT_STATE_NAME = 'Default';
|
||||||
|
|
||||||
|
public static function generateSslCertificate(
|
||||||
|
string $commonName,
|
||||||
|
array $subjectAlternativeNames = [],
|
||||||
|
?string $resourceType = null,
|
||||||
|
?int $resourceId = null,
|
||||||
|
?int $serverId = null,
|
||||||
|
int $validityDays = 365,
|
||||||
|
?string $caCert = null,
|
||||||
|
?string $caKey = null,
|
||||||
|
bool $isCaCertificate = false,
|
||||||
|
?string $configurationDir = null,
|
||||||
|
?string $mountPath = null,
|
||||||
|
bool $isPemKeyFileRequired = false,
|
||||||
|
): SslCertificate {
|
||||||
|
$organizationName = self::DEFAULT_ORGANIZATION_NAME;
|
||||||
|
$countryName = self::DEFAULT_COUNTRY_NAME;
|
||||||
|
$stateName = self::DEFAULT_STATE_NAME;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$privateKey = openssl_pkey_new([
|
||||||
|
'private_key_type' => OPENSSL_KEYTYPE_EC,
|
||||||
|
'curve_name' => 'secp521r1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($privateKey === false) {
|
||||||
|
throw new \RuntimeException('Failed to generate private key: '.openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! openssl_pkey_export($privateKey, $privateKeyStr)) {
|
||||||
|
throw new \RuntimeException('Failed to export private key: '.openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_null($serverId) && ! $isCaCertificate) {
|
||||||
|
$server = Server::find($serverId);
|
||||||
|
if ($server) {
|
||||||
|
$ip = $server->getIp;
|
||||||
|
if ($ip) {
|
||||||
|
$type = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)
|
||||||
|
? 'IP'
|
||||||
|
: 'DNS';
|
||||||
|
$subjectAlternativeNames = array_unique(
|
||||||
|
array_merge($subjectAlternativeNames, ["$type:$ip"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$basicConstraints = $isCaCertificate ? 'critical, CA:TRUE, pathlen:0' : 'critical, CA:FALSE';
|
||||||
|
$keyUsage = $isCaCertificate ? 'critical, keyCertSign, cRLSign' : 'critical, digitalSignature, keyAgreement';
|
||||||
|
|
||||||
|
$subjectAltNameSection = '';
|
||||||
|
$extendedKeyUsageSection = '';
|
||||||
|
|
||||||
|
if (! $isCaCertificate) {
|
||||||
|
$extendedKeyUsageSection = "\nextendedKeyUsage = serverAuth, clientAuth";
|
||||||
|
|
||||||
|
$subjectAlternativeNames = array_values(
|
||||||
|
array_unique(
|
||||||
|
array_merge(["DNS:$commonName"], $subjectAlternativeNames)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$formattedSubjectAltNames = array_map(
|
||||||
|
function ($index, $san) {
|
||||||
|
[$type, $value] = explode(':', $san, 2);
|
||||||
|
|
||||||
|
return "{$type}.".($index + 1)." = $value";
|
||||||
|
},
|
||||||
|
array_keys($subjectAlternativeNames),
|
||||||
|
$subjectAlternativeNames
|
||||||
|
);
|
||||||
|
|
||||||
|
$subjectAltNameSection = "subjectAltName = @subject_alt_names\n\n[ subject_alt_names ]\n"
|
||||||
|
.implode("\n", $formattedSubjectAltNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = <<<CONF
|
||||||
|
[ req ]
|
||||||
|
prompt = no
|
||||||
|
distinguished_name = distinguished_name
|
||||||
|
req_extensions = req_ext
|
||||||
|
|
||||||
|
[ distinguished_name ]
|
||||||
|
CN = $commonName
|
||||||
|
|
||||||
|
[ req_ext ]
|
||||||
|
basicConstraints = $basicConstraints
|
||||||
|
keyUsage = $keyUsage
|
||||||
|
{$extendedKeyUsageSection}
|
||||||
|
|
||||||
|
[ v3_req ]
|
||||||
|
basicConstraints = $basicConstraints
|
||||||
|
keyUsage = $keyUsage
|
||||||
|
{$extendedKeyUsageSection}
|
||||||
|
subjectKeyIdentifier = hash
|
||||||
|
{$subjectAltNameSection}
|
||||||
|
CONF;
|
||||||
|
|
||||||
|
$tempConfig = tmpfile();
|
||||||
|
fwrite($tempConfig, $config);
|
||||||
|
$tempConfigPath = stream_get_meta_data($tempConfig)['uri'];
|
||||||
|
|
||||||
|
$csr = openssl_csr_new([
|
||||||
|
'commonName' => $commonName,
|
||||||
|
'organizationName' => $organizationName,
|
||||||
|
'countryName' => $countryName,
|
||||||
|
'stateOrProvinceName' => $stateName,
|
||||||
|
], $privateKey, [
|
||||||
|
'digest_alg' => 'sha512',
|
||||||
|
'config' => $tempConfigPath,
|
||||||
|
'req_extensions' => 'req_ext',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($csr === false) {
|
||||||
|
throw new \RuntimeException('Failed to generate CSR: '.openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
$certificate = openssl_csr_sign(
|
||||||
|
$csr,
|
||||||
|
$caCert ?? null,
|
||||||
|
$caKey ?? $privateKey,
|
||||||
|
$validityDays,
|
||||||
|
[
|
||||||
|
'digest_alg' => 'sha512',
|
||||||
|
'config' => $tempConfigPath,
|
||||||
|
'x509_extensions' => 'v3_req',
|
||||||
|
],
|
||||||
|
random_int(1, PHP_INT_MAX)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($certificate === false) {
|
||||||
|
throw new \RuntimeException('Failed to sign certificate: '.openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! openssl_x509_export($certificate, $certificateStr)) {
|
||||||
|
throw new \RuntimeException('Failed to export certificate: '.openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
SslCertificate::query()
|
||||||
|
->where('resource_type', $resourceType)
|
||||||
|
->where('resource_id', $resourceId)
|
||||||
|
->where('server_id', $serverId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$sslCertificate = SslCertificate::create([
|
||||||
|
'ssl_certificate' => $certificateStr,
|
||||||
|
'ssl_private_key' => $privateKeyStr,
|
||||||
|
'resource_type' => $resourceType,
|
||||||
|
'resource_id' => $resourceId,
|
||||||
|
'server_id' => $serverId,
|
||||||
|
'configuration_dir' => $configurationDir,
|
||||||
|
'mount_path' => $mountPath,
|
||||||
|
'valid_until' => CarbonImmutable::now()->addDays($validityDays),
|
||||||
|
'is_ca_certificate' => $isCaCertificate,
|
||||||
|
'common_name' => $commonName,
|
||||||
|
'subject_alternative_names' => $subjectAlternativeNames,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($configurationDir && $mountPath && $resourceType && $resourceId) {
|
||||||
|
$model = app($resourceType)->find($resourceId);
|
||||||
|
|
||||||
|
$model->fileStorages()
|
||||||
|
->where('resource_type', $model->getMorphClass())
|
||||||
|
->where('resource_id', $model->id)
|
||||||
|
->get()
|
||||||
|
->filter(function ($storage) use ($mountPath) {
|
||||||
|
return in_array($storage->mount_path, [
|
||||||
|
$mountPath.'/server.crt',
|
||||||
|
$mountPath.'/server.key',
|
||||||
|
$mountPath.'/server.pem',
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->each(function ($storage) {
|
||||||
|
$storage->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($isPemKeyFileRequired) {
|
||||||
|
$model->fileStorages()->create([
|
||||||
|
'fs_path' => $configurationDir.'/ssl/server.pem',
|
||||||
|
'mount_path' => $mountPath.'/server.pem',
|
||||||
|
'content' => $certificateStr."\n".$privateKeyStr,
|
||||||
|
'is_directory' => false,
|
||||||
|
'chmod' => '600',
|
||||||
|
'resource_type' => $resourceType,
|
||||||
|
'resource_id' => $resourceId,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$model->fileStorages()->create([
|
||||||
|
'fs_path' => $configurationDir.'/ssl/server.crt',
|
||||||
|
'mount_path' => $mountPath.'/server.crt',
|
||||||
|
'content' => $certificateStr,
|
||||||
|
'is_directory' => false,
|
||||||
|
'chmod' => '644',
|
||||||
|
'resource_type' => $resourceType,
|
||||||
|
'resource_id' => $resourceId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$model->fileStorages()->create([
|
||||||
|
'fs_path' => $configurationDir.'/ssl/server.key',
|
||||||
|
'mount_path' => $mountPath.'/server.key',
|
||||||
|
'content' => $privateKeyStr,
|
||||||
|
'is_directory' => false,
|
||||||
|
'chmod' => '600',
|
||||||
|
'resource_type' => $resourceType,
|
||||||
|
'resource_id' => $resourceId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sslCertificate;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
throw new \RuntimeException('SSL Certificate generation failed: '.$e->getMessage(), 0, $e);
|
||||||
|
} finally {
|
||||||
|
fclose($tempConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -66,12 +66,9 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->deleteVolumes && $this->resource->type() !== 'service') {
|
if ($this->deleteVolumes && $this->resource->type() !== 'service') {
|
||||||
$this->resource?->delete_volumes($persistentStorages);
|
$this->resource->delete_volumes($persistentStorages);
|
||||||
|
$this->resource->persistentStorages()->delete();
|
||||||
}
|
}
|
||||||
if ($this->deleteConfigurations) {
|
|
||||||
$this->resource?->delete_configurations();
|
|
||||||
}
|
|
||||||
|
|
||||||
$isDatabase = $this->resource instanceof StandalonePostgresql
|
$isDatabase = $this->resource instanceof StandalonePostgresql
|
||||||
|| $this->resource instanceof StandaloneRedis
|
|| $this->resource instanceof StandaloneRedis
|
||||||
|| $this->resource instanceof StandaloneMongodb
|
|| $this->resource instanceof StandaloneMongodb
|
||||||
@@ -80,6 +77,18 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|| $this->resource instanceof StandaloneKeydb
|
|| $this->resource instanceof StandaloneKeydb
|
||||||
|| $this->resource instanceof StandaloneDragonfly
|
|| $this->resource instanceof StandaloneDragonfly
|
||||||
|| $this->resource instanceof StandaloneClickhouse;
|
|| $this->resource instanceof StandaloneClickhouse;
|
||||||
|
|
||||||
|
if ($this->deleteConfigurations) {
|
||||||
|
$this->resource->delete_configurations(); // rename to FileStorages
|
||||||
|
$this->resource->fileStorages()->delete();
|
||||||
|
}
|
||||||
|
if ($isDatabase) {
|
||||||
|
$this->resource->sslCertificates()->delete();
|
||||||
|
$this->resource->scheduledBackups()->delete();
|
||||||
|
$this->resource->environment_variables()->delete();
|
||||||
|
$this->resource->tags()->detach();
|
||||||
|
}
|
||||||
|
|
||||||
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
|
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
|
||||||
if (($this->dockerCleanup || $isDatabase) && $server) {
|
if (($this->dockerCleanup || $isDatabase) && $server) {
|
||||||
CleanupDocker::dispatch($server, true);
|
CleanupDocker::dispatch($server, true);
|
||||||
|
73
app/Jobs/RegenerateSslCertJob.php
Normal file
73
app/Jobs/RegenerateSslCertJob.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Helpers\SSLHelper;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Notifications\SslExpirationNotification;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class RegenerateSslCertJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $tries = 3;
|
||||||
|
|
||||||
|
public $backoff = 60;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected ?Team $team = null,
|
||||||
|
protected ?int $server_id = null,
|
||||||
|
protected bool $force_regeneration = false,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$query = SslCertificate::query();
|
||||||
|
|
||||||
|
if ($this->server_id) {
|
||||||
|
$query->where('server_id', $this->server_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->force_regeneration) {
|
||||||
|
$query->where('valid_until', '<=', now()->addDays(14));
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->where('is_ca_certificate', false);
|
||||||
|
|
||||||
|
$regenerated = collect();
|
||||||
|
|
||||||
|
$query->cursor()->each(function ($certificate) use ($regenerated) {
|
||||||
|
try {
|
||||||
|
$caCert = SslCertificate::where('server_id', $certificate->server_id)
|
||||||
|
->where('is_ca_certificate', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
SSLHelper::generateSslCertificate(
|
||||||
|
commonName: $certificate->common_name,
|
||||||
|
subjectAlternativeNames: $certificate->subject_alternative_names,
|
||||||
|
resourceType: $certificate->resource_type,
|
||||||
|
resourceId: $certificate->resource_id,
|
||||||
|
serverId: $certificate->server_id,
|
||||||
|
configurationDir: $certificate->configuration_dir,
|
||||||
|
mountPath: $certificate->mount_path,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
);
|
||||||
|
$regenerated->push($certificate);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Failed to regenerate SSL certificate: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($regenerated->isNotEmpty()) {
|
||||||
|
$this->team?->notify(new SslExpirationNotification($regenerated));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Dragonfly;
|
|||||||
|
|
||||||
use App\Actions\Database\StartDatabaseProxy;
|
use App\Actions\Database\StartDatabaseProxy;
|
||||||
use App\Actions\Database\StopDatabaseProxy;
|
use App\Actions\Database\StopDatabaseProxy;
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneDragonfly;
|
use App\Models\StandaloneDragonfly;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
@@ -50,6 +53,11 @@ class General extends Component
|
|||||||
#[Validate(['nullable', 'boolean'])]
|
#[Validate(['nullable', 'boolean'])]
|
||||||
public bool $isLogDrainEnabled = false;
|
public bool $isLogDrainEnabled = false;
|
||||||
|
|
||||||
|
public ?Carbon $certificateValidUntil = null;
|
||||||
|
|
||||||
|
#[Validate(['nullable', 'boolean'])]
|
||||||
|
public bool $enable_ssl = false;
|
||||||
|
|
||||||
public function getListeners()
|
public function getListeners()
|
||||||
{
|
{
|
||||||
$teamId = Auth::user()->currentTeam()->id;
|
$teamId = Auth::user()->currentTeam()->id;
|
||||||
@@ -64,6 +72,12 @@ class General extends Component
|
|||||||
try {
|
try {
|
||||||
$this->syncData();
|
$this->syncData();
|
||||||
$this->server = data_get($this->database, 'destination.server');
|
$this->server = data_get($this->database, 'destination.server');
|
||||||
|
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if ($existingCert) {
|
||||||
|
$this->certificateValidUntil = $existingCert->valid_until;
|
||||||
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
@@ -82,6 +96,7 @@ class General extends Component
|
|||||||
$this->database->public_port = $this->publicPort;
|
$this->database->public_port = $this->publicPort;
|
||||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||||
|
$this->database->enable_ssl = $this->enable_ssl;
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
|
|
||||||
$this->dbUrl = $this->database->internal_db_url;
|
$this->dbUrl = $this->database->internal_db_url;
|
||||||
@@ -96,6 +111,7 @@ class General extends Component
|
|||||||
$this->publicPort = $this->database->public_port;
|
$this->publicPort = $this->database->public_port;
|
||||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||||
|
$this->enable_ssl = $this->database->enable_ssl;
|
||||||
$this->dbUrl = $this->database->internal_db_url;
|
$this->dbUrl = $this->database->internal_db_url;
|
||||||
$this->dbUrlPublic = $this->database->external_db_url;
|
$this->dbUrlPublic = $this->database->external_db_url;
|
||||||
}
|
}
|
||||||
@@ -174,4 +190,47 @@ class General extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function instantSaveSSL()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->syncData(true);
|
||||||
|
$this->dispatch('success', 'SSL configuration updated.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateSslCertificate()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $existingCert) {
|
||||||
|
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caCert = SslCertificate::where('server_id', $existingCert->server_id)
|
||||||
|
->where('is_ca_certificate', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
SslHelper::generateSslCertificate(
|
||||||
|
commonName: $existingCert->commonName,
|
||||||
|
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
|
||||||
|
resourceType: $existingCert->resource_type,
|
||||||
|
resourceId: $existingCert->resource_id,
|
||||||
|
serverId: $existingCert->server_id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $existingCert->configuration_dir,
|
||||||
|
mountPath: $existingCert->mount_path,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Keydb;
|
|||||||
|
|
||||||
use App\Actions\Database\StartDatabaseProxy;
|
use App\Actions\Database\StartDatabaseProxy;
|
||||||
use App\Actions\Database\StopDatabaseProxy;
|
use App\Actions\Database\StopDatabaseProxy;
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneKeydb;
|
use App\Models\StandaloneKeydb;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
@@ -53,6 +56,11 @@ class General extends Component
|
|||||||
#[Validate(['nullable', 'boolean'])]
|
#[Validate(['nullable', 'boolean'])]
|
||||||
public bool $isLogDrainEnabled = false;
|
public bool $isLogDrainEnabled = false;
|
||||||
|
|
||||||
|
public ?Carbon $certificateValidUntil = null;
|
||||||
|
|
||||||
|
#[Validate(['boolean'])]
|
||||||
|
public bool $enable_ssl = false;
|
||||||
|
|
||||||
public function getListeners()
|
public function getListeners()
|
||||||
{
|
{
|
||||||
$teamId = Auth::user()->currentTeam()->id;
|
$teamId = Auth::user()->currentTeam()->id;
|
||||||
@@ -67,6 +75,12 @@ class General extends Component
|
|||||||
try {
|
try {
|
||||||
$this->syncData();
|
$this->syncData();
|
||||||
$this->server = data_get($this->database, 'destination.server');
|
$this->server = data_get($this->database, 'destination.server');
|
||||||
|
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if ($existingCert) {
|
||||||
|
$this->certificateValidUntil = $existingCert->valid_until;
|
||||||
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
@@ -86,6 +100,7 @@ class General extends Component
|
|||||||
$this->database->public_port = $this->publicPort;
|
$this->database->public_port = $this->publicPort;
|
||||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||||
|
$this->database->enable_ssl = $this->enable_ssl;
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
|
|
||||||
$this->dbUrl = $this->database->internal_db_url;
|
$this->dbUrl = $this->database->internal_db_url;
|
||||||
@@ -101,6 +116,7 @@ class General extends Component
|
|||||||
$this->publicPort = $this->database->public_port;
|
$this->publicPort = $this->database->public_port;
|
||||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||||
|
$this->enable_ssl = $this->database->enable_ssl;
|
||||||
$this->dbUrl = $this->database->internal_db_url;
|
$this->dbUrl = $this->database->internal_db_url;
|
||||||
$this->dbUrlPublic = $this->database->external_db_url;
|
$this->dbUrlPublic = $this->database->external_db_url;
|
||||||
}
|
}
|
||||||
@@ -179,4 +195,47 @@ class General extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function instantSaveSSL()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->syncData(true);
|
||||||
|
$this->dispatch('success', 'SSL configuration updated.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateSslCertificate()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $existingCert) {
|
||||||
|
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caCert = SslCertificate::where('server_id', $existingCert->server_id)
|
||||||
|
->where('is_ca_certificate', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
SslHelper::generateSslCertificate(
|
||||||
|
commonName: $existingCert->commonName,
|
||||||
|
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
|
||||||
|
resourceType: $existingCert->resource_type,
|
||||||
|
resourceId: $existingCert->resource_id,
|
||||||
|
serverId: $existingCert->server_id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $existingCert->configuration_dir,
|
||||||
|
mountPath: $existingCert->mount_path,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Mariadb;
|
|||||||
|
|
||||||
use App\Actions\Database\StartDatabaseProxy;
|
use App\Actions\Database\StartDatabaseProxy;
|
||||||
use App\Actions\Database\StopDatabaseProxy;
|
use App\Actions\Database\StopDatabaseProxy;
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneMariadb;
|
use App\Models\StandaloneMariadb;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -21,6 +24,8 @@ class General extends Component
|
|||||||
|
|
||||||
public ?string $db_url_public = null;
|
public ?string $db_url_public = null;
|
||||||
|
|
||||||
|
public ?Carbon $certificateValidUntil = null;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'database.name' => 'required',
|
'database.name' => 'required',
|
||||||
'database.description' => 'nullable',
|
'database.description' => 'nullable',
|
||||||
@@ -35,6 +40,7 @@ class General extends Component
|
|||||||
'database.public_port' => 'nullable|integer',
|
'database.public_port' => 'nullable|integer',
|
||||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||||
'database.custom_docker_run_options' => 'nullable',
|
'database.custom_docker_run_options' => 'nullable',
|
||||||
|
'database.enable_ssl' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
@@ -50,6 +56,7 @@ class General extends Component
|
|||||||
'database.is_public' => 'Is Public',
|
'database.is_public' => 'Is Public',
|
||||||
'database.public_port' => 'Public Port',
|
'database.public_port' => 'Public Port',
|
||||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||||
|
'database.enable_ssl' => 'Enable SSL',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
@@ -57,6 +64,12 @@ class General extends Component
|
|||||||
$this->db_url = $this->database->internal_db_url;
|
$this->db_url = $this->database->internal_db_url;
|
||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->server = data_get($this->database, 'destination.server');
|
$this->server = data_get($this->database, 'destination.server');
|
||||||
|
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if ($existingCert) {
|
||||||
|
$this->certificateValidUntil = $existingCert->valid_until;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
@@ -127,6 +140,47 @@ class General extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function instantSaveSSL()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->database->save();
|
||||||
|
$this->dispatch('success', 'SSL configuration updated.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateSslCertificate()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $existingCert) {
|
||||||
|
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
SslHelper::generateSslCertificate(
|
||||||
|
commonName: $existingCert->common_name,
|
||||||
|
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||||
|
resourceType: $existingCert->resource_type,
|
||||||
|
resourceId: $existingCert->resource_id,
|
||||||
|
serverId: $existingCert->server_id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $existingCert->configuration_dir,
|
||||||
|
mountPath: $existingCert->mount_path,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function refresh(): void
|
public function refresh(): void
|
||||||
{
|
{
|
||||||
$this->database->refresh();
|
$this->database->refresh();
|
||||||
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Mongodb;
|
|||||||
|
|
||||||
use App\Actions\Database\StartDatabaseProxy;
|
use App\Actions\Database\StartDatabaseProxy;
|
||||||
use App\Actions\Database\StopDatabaseProxy;
|
use App\Actions\Database\StopDatabaseProxy;
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneMongodb;
|
use App\Models\StandaloneMongodb;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -21,6 +24,8 @@ class General extends Component
|
|||||||
|
|
||||||
public ?string $db_url_public = null;
|
public ?string $db_url_public = null;
|
||||||
|
|
||||||
|
public ?Carbon $certificateValidUntil = null;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'database.name' => 'required',
|
'database.name' => 'required',
|
||||||
'database.description' => 'nullable',
|
'database.description' => 'nullable',
|
||||||
@@ -34,6 +39,8 @@ class General extends Component
|
|||||||
'database.public_port' => 'nullable|integer',
|
'database.public_port' => 'nullable|integer',
|
||||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||||
'database.custom_docker_run_options' => 'nullable',
|
'database.custom_docker_run_options' => 'nullable',
|
||||||
|
'database.enable_ssl' => 'boolean',
|
||||||
|
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-full',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
@@ -48,6 +55,8 @@ class General extends Component
|
|||||||
'database.is_public' => 'Is Public',
|
'database.is_public' => 'Is Public',
|
||||||
'database.public_port' => 'Public Port',
|
'database.public_port' => 'Public Port',
|
||||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||||
|
'database.enable_ssl' => 'Enable SSL',
|
||||||
|
'database.ssl_mode' => 'SSL Mode',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
@@ -55,6 +64,12 @@ class General extends Component
|
|||||||
$this->db_url = $this->database->internal_db_url;
|
$this->db_url = $this->database->internal_db_url;
|
||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->server = data_get($this->database, 'destination.server');
|
$this->server = data_get($this->database, 'destination.server');
|
||||||
|
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if ($existingCert) {
|
||||||
|
$this->certificateValidUntil = $existingCert->valid_until;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
@@ -128,6 +143,52 @@ class General extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatedDatabaseSslMode()
|
||||||
|
{
|
||||||
|
$this->instantSaveSSL();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function instantSaveSSL()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->database->save();
|
||||||
|
$this->dispatch('success', 'SSL configuration updated.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateSslCertificate()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $existingCert) {
|
||||||
|
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
SslHelper::generateSslCertificate(
|
||||||
|
commonName: $existingCert->common_name,
|
||||||
|
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||||
|
resourceType: $existingCert->resource_type,
|
||||||
|
resourceId: $existingCert->resource_id,
|
||||||
|
serverId: $existingCert->server_id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $existingCert->configuration_dir,
|
||||||
|
mountPath: $existingCert->mount_path,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function refresh(): void
|
public function refresh(): void
|
||||||
{
|
{
|
||||||
$this->database->refresh();
|
$this->database->refresh();
|
||||||
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Mysql;
|
|||||||
|
|
||||||
use App\Actions\Database\StartDatabaseProxy;
|
use App\Actions\Database\StartDatabaseProxy;
|
||||||
use App\Actions\Database\StopDatabaseProxy;
|
use App\Actions\Database\StopDatabaseProxy;
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneMysql;
|
use App\Models\StandaloneMysql;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -21,6 +24,8 @@ class General extends Component
|
|||||||
|
|
||||||
public ?string $db_url_public = null;
|
public ?string $db_url_public = null;
|
||||||
|
|
||||||
|
public ?Carbon $certificateValidUntil = null;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'database.name' => 'required',
|
'database.name' => 'required',
|
||||||
'database.description' => 'nullable',
|
'database.description' => 'nullable',
|
||||||
@@ -35,6 +40,8 @@ class General extends Component
|
|||||||
'database.public_port' => 'nullable|integer',
|
'database.public_port' => 'nullable|integer',
|
||||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||||
'database.custom_docker_run_options' => 'nullable',
|
'database.custom_docker_run_options' => 'nullable',
|
||||||
|
'database.enable_ssl' => 'boolean',
|
||||||
|
'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
@@ -50,6 +57,8 @@ class General extends Component
|
|||||||
'database.is_public' => 'Is Public',
|
'database.is_public' => 'Is Public',
|
||||||
'database.public_port' => 'Public Port',
|
'database.public_port' => 'Public Port',
|
||||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||||
|
'database.enable_ssl' => 'Enable SSL',
|
||||||
|
'database.ssl_mode' => 'SSL Mode',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
@@ -57,6 +66,12 @@ class General extends Component
|
|||||||
$this->db_url = $this->database->internal_db_url;
|
$this->db_url = $this->database->internal_db_url;
|
||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->server = data_get($this->database, 'destination.server');
|
$this->server = data_get($this->database, 'destination.server');
|
||||||
|
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if ($existingCert) {
|
||||||
|
$this->certificateValidUntil = $existingCert->valid_until;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
@@ -127,6 +142,52 @@ class General extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatedDatabaseSslMode()
|
||||||
|
{
|
||||||
|
$this->instantSaveSSL();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function instantSaveSSL()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->database->save();
|
||||||
|
$this->dispatch('success', 'SSL configuration updated.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateSslCertificate()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $existingCert) {
|
||||||
|
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
SslHelper::generateSslCertificate(
|
||||||
|
commonName: $existingCert->common_name,
|
||||||
|
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||||
|
resourceType: $existingCert->resource_type,
|
||||||
|
resourceId: $existingCert->resource_id,
|
||||||
|
serverId: $existingCert->server_id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $existingCert->configuration_dir,
|
||||||
|
mountPath: $existingCert->mount_path,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function refresh(): void
|
public function refresh(): void
|
||||||
{
|
{
|
||||||
$this->database->refresh();
|
$this->database->refresh();
|
||||||
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Postgresql;
|
|||||||
|
|
||||||
use App\Actions\Database\StartDatabaseProxy;
|
use App\Actions\Database\StartDatabaseProxy;
|
||||||
use App\Actions\Database\StopDatabaseProxy;
|
use App\Actions\Database\StopDatabaseProxy;
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandalonePostgresql;
|
use App\Models\StandalonePostgresql;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -23,6 +26,8 @@ class General extends Component
|
|||||||
|
|
||||||
public ?string $db_url_public = null;
|
public ?string $db_url_public = null;
|
||||||
|
|
||||||
|
public ?Carbon $certificateValidUntil = null;
|
||||||
|
|
||||||
public function getListeners()
|
public function getListeners()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -48,6 +53,8 @@ class General extends Component
|
|||||||
'database.public_port' => 'nullable|integer',
|
'database.public_port' => 'nullable|integer',
|
||||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||||
'database.custom_docker_run_options' => 'nullable',
|
'database.custom_docker_run_options' => 'nullable',
|
||||||
|
'database.enable_ssl' => 'boolean',
|
||||||
|
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
@@ -65,6 +72,8 @@ class General extends Component
|
|||||||
'database.is_public' => 'Is Public',
|
'database.is_public' => 'Is Public',
|
||||||
'database.public_port' => 'Public Port',
|
'database.public_port' => 'Public Port',
|
||||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||||
|
'database.enable_ssl' => 'Enable SSL',
|
||||||
|
'database.ssl_mode' => 'SSL Mode',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
@@ -72,6 +81,12 @@ class General extends Component
|
|||||||
$this->db_url = $this->database->internal_db_url;
|
$this->db_url = $this->database->internal_db_url;
|
||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->server = data_get($this->database, 'destination.server');
|
$this->server = data_get($this->database, 'destination.server');
|
||||||
|
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if ($existingCert) {
|
||||||
|
$this->certificateValidUntil = $existingCert->valid_until;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
@@ -91,6 +106,52 @@ class General extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatedDatabaseSslMode()
|
||||||
|
{
|
||||||
|
$this->instantSaveSSL();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function instantSaveSSL()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->database->save();
|
||||||
|
$this->dispatch('success', 'SSL configuration updated.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateSslCertificate()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $existingCert) {
|
||||||
|
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
SslHelper::generateSslCertificate(
|
||||||
|
commonName: $existingCert->common_name,
|
||||||
|
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||||
|
resourceType: $existingCert->resource_type,
|
||||||
|
resourceId: $existingCert->resource_id,
|
||||||
|
serverId: $existingCert->server_id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $existingCert->configuration_dir,
|
||||||
|
mountPath: $existingCert->mount_path,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Redis;
|
|||||||
|
|
||||||
use App\Actions\Database\StartDatabaseProxy;
|
use App\Actions\Database\StartDatabaseProxy;
|
||||||
use App\Actions\Database\StopDatabaseProxy;
|
use App\Actions\Database\StopDatabaseProxy;
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
use App\Models\StandaloneRedis;
|
use App\Models\StandaloneRedis;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -30,6 +33,8 @@ class General extends Component
|
|||||||
|
|
||||||
public ?string $db_url_public = null;
|
public ?string $db_url_public = null;
|
||||||
|
|
||||||
|
public ?Carbon $certificateValidUntil = null;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'database.name' => 'required',
|
'database.name' => 'required',
|
||||||
'database.description' => 'nullable',
|
'database.description' => 'nullable',
|
||||||
@@ -42,6 +47,7 @@ class General extends Component
|
|||||||
'database.custom_docker_run_options' => 'nullable',
|
'database.custom_docker_run_options' => 'nullable',
|
||||||
'redis_username' => 'required',
|
'redis_username' => 'required',
|
||||||
'redis_password' => 'required',
|
'redis_password' => 'required',
|
||||||
|
'database.enable_ssl' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
@@ -55,12 +61,18 @@ class General extends Component
|
|||||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||||
'redis_username' => 'Redis Username',
|
'redis_username' => 'Redis Username',
|
||||||
'redis_password' => 'Redis Password',
|
'redis_password' => 'Redis Password',
|
||||||
|
'database.enable_ssl' => 'Enable SSL',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->server = data_get($this->database, 'destination.server');
|
$this->server = data_get($this->database, 'destination.server');
|
||||||
$this->refreshView();
|
$this->refreshView();
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if ($existingCert) {
|
||||||
|
$this->certificateValidUntil = $existingCert->valid_until;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
@@ -136,6 +148,47 @@ class General extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function instantSaveSSL()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->database->save();
|
||||||
|
$this->dispatch('success', 'SSL configuration updated.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateSslCertificate()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$existingCert = $this->database->sslCertificates()->first();
|
||||||
|
|
||||||
|
if (! $existingCert) {
|
||||||
|
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
SslHelper::generateSslCertificate(
|
||||||
|
commonName: $existingCert->commonName,
|
||||||
|
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
|
||||||
|
resourceType: $existingCert->resource_type,
|
||||||
|
resourceId: $existingCert->resource_id,
|
||||||
|
serverId: $existingCert->server_id,
|
||||||
|
caCert: $caCert->ssl_certificate,
|
||||||
|
caKey: $caCert->ssl_private_key,
|
||||||
|
configurationDir: $existingCert->configuration_dir,
|
||||||
|
mountPath: $existingCert->mount_path,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function refresh(): void
|
public function refresh(): void
|
||||||
{
|
{
|
||||||
$this->database->refresh();
|
$this->database->refresh();
|
||||||
|
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Livewire\Server;
|
namespace App\Livewire\Server;
|
||||||
|
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
|
use App\Jobs\RegenerateSslCertJob;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -10,6 +14,14 @@ class Advanced extends Component
|
|||||||
{
|
{
|
||||||
public Server $server;
|
public Server $server;
|
||||||
|
|
||||||
|
public ?SslCertificate $caCertificate = null;
|
||||||
|
|
||||||
|
public $showCertificate = false;
|
||||||
|
|
||||||
|
public $certificateContent = '';
|
||||||
|
|
||||||
|
public ?Carbon $certificateValidUntil = null;
|
||||||
|
|
||||||
public array $parameters = [];
|
public array $parameters = [];
|
||||||
|
|
||||||
#[Validate(['string'])]
|
#[Validate(['string'])]
|
||||||
@@ -30,11 +42,99 @@ class Advanced extends Component
|
|||||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
||||||
$this->parameters = get_route_parameters();
|
$this->parameters = get_route_parameters();
|
||||||
$this->syncData();
|
$this->syncData();
|
||||||
|
$this->loadCaCertificate();
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
return redirect()->route('server.index');
|
return redirect()->route('server.index');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function loadCaCertificate()
|
||||||
|
{
|
||||||
|
$this->caCertificate = SslCertificate::where('server_id', $this->server->id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
if ($this->caCertificate) {
|
||||||
|
$this->certificateContent = $this->caCertificate->ssl_certificate;
|
||||||
|
$this->certificateValidUntil = $this->caCertificate->valid_until;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggleCertificate()
|
||||||
|
{
|
||||||
|
$this->showCertificate = ! $this->showCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveCaCertificate()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (! $this->certificateContent) {
|
||||||
|
throw new \Exception('Certificate content cannot be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! openssl_x509_read($this->certificateContent)) {
|
||||||
|
throw new \Exception('Invalid certificate format.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->caCertificate) {
|
||||||
|
$this->caCertificate->ssl_certificate = $this->certificateContent;
|
||||||
|
$this->caCertificate->save();
|
||||||
|
|
||||||
|
$this->loadCaCertificate();
|
||||||
|
|
||||||
|
$this->writeCertificateToServer();
|
||||||
|
|
||||||
|
dispatch(new RegenerateSslCertJob(
|
||||||
|
server_id: $this->server->id,
|
||||||
|
force_regeneration: true
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$this->dispatch('success', 'CA Certificate saved successfully.');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateCaCertificate()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
SslHelper::generateSslCertificate(
|
||||||
|
commonName: 'Coolify CA Certificate',
|
||||||
|
serverId: $this->server->id,
|
||||||
|
isCaCertificate: true,
|
||||||
|
validityDays: 10 * 365
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->loadCaCertificate();
|
||||||
|
|
||||||
|
$this->writeCertificateToServer();
|
||||||
|
|
||||||
|
dispatch(new RegenerateSslCertJob(
|
||||||
|
server_id: $this->server->id,
|
||||||
|
force_regeneration: true
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->loadCaCertificate();
|
||||||
|
$this->dispatch('success', 'CA Certificate regenerated successfully.');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeCertificateToServer()
|
||||||
|
{
|
||||||
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$commands = collect([
|
||||||
|
"mkdir -p $caCertPath",
|
||||||
|
"chown -R 9999:root $caCertPath",
|
||||||
|
"chmod -R 700 $caCertPath",
|
||||||
|
"rm -rf $caCertPath/coolify-ca.crt",
|
||||||
|
"echo '{$this->certificateContent}' > $caCertPath/coolify-ca.crt",
|
||||||
|
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||||
|
]);
|
||||||
|
|
||||||
|
remote_process($commands, $this->server);
|
||||||
|
}
|
||||||
|
|
||||||
public function syncData(bool $toModel = false)
|
public function syncData(bool $toModel = false)
|
||||||
{
|
{
|
||||||
if ($toModel) {
|
if ($toModel) {
|
||||||
|
@@ -8,6 +8,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
|
|
||||||
class LocalFileVolume extends BaseModel
|
class LocalFileVolume extends BaseModel
|
||||||
{
|
{
|
||||||
|
protected $casts = [
|
||||||
|
'fs_path' => 'encrypted',
|
||||||
|
'mount_path' => 'encrypted',
|
||||||
|
'content' => 'encrypted',
|
||||||
|
];
|
||||||
|
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
@@ -24,11 +24,6 @@ class LocalPersistentVolume extends Model
|
|||||||
return $this->morphTo('resource');
|
return $this->morphTo('resource');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function standalone_postgresql()
|
|
||||||
{
|
|
||||||
return $this->morphTo('resource');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function name(): Attribute
|
protected function name(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
49
app/Models/SslCertificate.php
Normal file
49
app/Models/SslCertificate.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class SslCertificate extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'ssl_certificate',
|
||||||
|
'ssl_private_key',
|
||||||
|
'configuration_dir',
|
||||||
|
'mount_path',
|
||||||
|
'resource_type',
|
||||||
|
'resource_id',
|
||||||
|
'server_id',
|
||||||
|
'common_name',
|
||||||
|
'subject_alternative_names',
|
||||||
|
'valid_until',
|
||||||
|
'is_ca_certificate',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'ssl_certificate' => 'encrypted',
|
||||||
|
'ssl_private_key' => 'encrypted',
|
||||||
|
'subject_alternative_names' => 'array',
|
||||||
|
'valid_until' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function application()
|
||||||
|
{
|
||||||
|
return $this->morphTo('resource');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function service()
|
||||||
|
{
|
||||||
|
return $this->morphTo('resource');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function database()
|
||||||
|
{
|
||||||
|
return $this->morphTo('resource');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function server()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Server::class);
|
||||||
|
}
|
||||||
|
}
|
@@ -163,6 +163,11 @@ class StandaloneClickhouse extends BaseModel
|
|||||||
return data_get($this, 'environment.project');
|
return data_get($this, 'environment.project');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sslCertificates()
|
||||||
|
{
|
||||||
|
return $this->morphMany(SslCertificate::class, 'resource');
|
||||||
|
}
|
||||||
|
|
||||||
public function link()
|
public function link()
|
||||||
{
|
{
|
||||||
if (data_get($this, 'environment.project.uuid')) {
|
if (data_get($this, 'environment.project.uuid')) {
|
||||||
@@ -218,7 +223,12 @@ class StandaloneClickhouse extends BaseModel
|
|||||||
protected function internalDbUrl(): Attribute
|
protected function internalDbUrl(): Attribute
|
||||||
{
|
{
|
||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: fn () => "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->uuid}:9000/{$this->clickhouse_db}",
|
get: function () {
|
||||||
|
$encodedUser = rawurlencode($this->clickhouse_admin_user);
|
||||||
|
$encodedPass = rawurlencode($this->clickhouse_admin_password);
|
||||||
|
|
||||||
|
return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->uuid}:9000/{$this->clickhouse_db}";
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +237,10 @@ class StandaloneClickhouse extends BaseModel
|
|||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: function () {
|
get: function () {
|
||||||
if ($this->is_public && $this->public_port) {
|
if ($this->is_public && $this->public_port) {
|
||||||
return "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
|
$encodedUser = rawurlencode($this->clickhouse_admin_user);
|
||||||
|
$encodedPass = rawurlencode($this->clickhouse_admin_password);
|
||||||
|
|
||||||
|
return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@@ -168,6 +168,11 @@ class StandaloneDragonfly extends BaseModel
|
|||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'environment.project.team');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sslCertificates()
|
||||||
|
{
|
||||||
|
return $this->morphMany(SslCertificate::class, 'resource');
|
||||||
|
}
|
||||||
|
|
||||||
public function link()
|
public function link()
|
||||||
{
|
{
|
||||||
if (data_get($this, 'environment.project.uuid')) {
|
if (data_get($this, 'environment.project.uuid')) {
|
||||||
@@ -218,7 +223,18 @@ class StandaloneDragonfly extends BaseModel
|
|||||||
protected function internalDbUrl(): Attribute
|
protected function internalDbUrl(): Attribute
|
||||||
{
|
{
|
||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: fn () => "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0",
|
get: function () {
|
||||||
|
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||||
|
$port = $this->enable_ssl ? 6380 : 6379;
|
||||||
|
$encodedPass = rawurlencode($this->dragonfly_password);
|
||||||
|
$url = "{$scheme}://:{$encodedPass}@{$this->uuid}:{$port}/0";
|
||||||
|
|
||||||
|
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||||
|
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +243,15 @@ class StandaloneDragonfly extends BaseModel
|
|||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: function () {
|
get: function () {
|
||||||
if ($this->is_public && $this->public_port) {
|
if ($this->is_public && $this->public_port) {
|
||||||
return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||||
|
$encodedPass = rawurlencode($this->dragonfly_password);
|
||||||
|
$url = "{$scheme}://:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||||
|
|
||||||
|
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||||
|
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@@ -168,6 +168,11 @@ class StandaloneKeydb extends BaseModel
|
|||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'environment.project.team');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sslCertificates()
|
||||||
|
{
|
||||||
|
return $this->morphMany(SslCertificate::class, 'resource');
|
||||||
|
}
|
||||||
|
|
||||||
public function link()
|
public function link()
|
||||||
{
|
{
|
||||||
if (data_get($this, 'environment.project.uuid')) {
|
if (data_get($this, 'environment.project.uuid')) {
|
||||||
@@ -218,7 +223,18 @@ class StandaloneKeydb extends BaseModel
|
|||||||
protected function internalDbUrl(): Attribute
|
protected function internalDbUrl(): Attribute
|
||||||
{
|
{
|
||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: fn () => "redis://:{$this->keydb_password}@{$this->uuid}:6379/0",
|
get: function () {
|
||||||
|
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||||
|
$port = $this->enable_ssl ? 6380 : 6379;
|
||||||
|
$encodedPass = rawurlencode($this->keydb_password);
|
||||||
|
$url = "{$scheme}://:{$encodedPass}@{$this->uuid}:{$port}/0";
|
||||||
|
|
||||||
|
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||||
|
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +243,15 @@ class StandaloneKeydb extends BaseModel
|
|||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: function () {
|
get: function () {
|
||||||
if ($this->is_public && $this->public_port) {
|
if ($this->is_public && $this->public_port) {
|
||||||
return "redis://:{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||||
|
$encodedPass = rawurlencode($this->keydb_password);
|
||||||
|
$url = "{$scheme}://:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||||
|
|
||||||
|
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||||
|
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@@ -218,7 +218,12 @@ class StandaloneMariadb extends BaseModel
|
|||||||
protected function internalDbUrl(): Attribute
|
protected function internalDbUrl(): Attribute
|
||||||
{
|
{
|
||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}",
|
get: function () {
|
||||||
|
$encodedUser = rawurlencode($this->mariadb_user);
|
||||||
|
$encodedPass = rawurlencode($this->mariadb_password);
|
||||||
|
|
||||||
|
return "mysql://{$encodedUser}:{$encodedPass}@{$this->uuid}:3306/{$this->mariadb_database}";
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +232,10 @@ class StandaloneMariadb extends BaseModel
|
|||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: function () {
|
get: function () {
|
||||||
if ($this->is_public && $this->public_port) {
|
if ($this->is_public && $this->public_port) {
|
||||||
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
|
$encodedUser = rawurlencode($this->mariadb_user);
|
||||||
|
$encodedPass = rawurlencode($this->mariadb_password);
|
||||||
|
|
||||||
|
return "mysql://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -271,6 +279,11 @@ class StandaloneMariadb extends BaseModel
|
|||||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sslCertificates()
|
||||||
|
{
|
||||||
|
return $this->morphMany(SslCertificate::class, 'resource');
|
||||||
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
public function getCpuMetrics(int $mins = 5)
|
||||||
{
|
{
|
||||||
$server = $this->destination->server;
|
$server = $this->destination->server;
|
||||||
|
@@ -177,6 +177,11 @@ class StandaloneMongodb extends BaseModel
|
|||||||
return data_get($this, 'is_log_drain_enabled', false);
|
return data_get($this, 'is_log_drain_enabled', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sslCertificates()
|
||||||
|
{
|
||||||
|
return $this->morphMany(SslCertificate::class, 'resource');
|
||||||
|
}
|
||||||
|
|
||||||
public function link()
|
public function link()
|
||||||
{
|
{
|
||||||
if (data_get($this, 'environment.project.uuid')) {
|
if (data_get($this, 'environment.project.uuid')) {
|
||||||
@@ -238,7 +243,19 @@ class StandaloneMongodb extends BaseModel
|
|||||||
protected function internalDbUrl(): Attribute
|
protected function internalDbUrl(): Attribute
|
||||||
{
|
{
|
||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: fn () => "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true",
|
get: function () {
|
||||||
|
$encodedUser = rawurlencode($this->mongo_initdb_root_username);
|
||||||
|
$encodedPass = rawurlencode($this->mongo_initdb_root_password);
|
||||||
|
$url = "mongodb://{$encodedUser}:{$encodedPass}@{$this->uuid}:27017/?directConnection=true";
|
||||||
|
if ($this->enable_ssl) {
|
||||||
|
$url .= '&tls=true';
|
||||||
|
if (in_array($this->ssl_mode, ['verify-full'])) {
|
||||||
|
$url .= '&tlsCAFile=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +264,17 @@ class StandaloneMongodb extends BaseModel
|
|||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: function () {
|
get: function () {
|
||||||
if ($this->is_public && $this->public_port) {
|
if ($this->is_public && $this->public_port) {
|
||||||
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
|
$encodedUser = rawurlencode($this->mongo_initdb_root_username);
|
||||||
|
$encodedPass = rawurlencode($this->mongo_initdb_root_password);
|
||||||
|
$url = "mongodb://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
|
||||||
|
if ($this->enable_ssl) {
|
||||||
|
$url .= '&tls=true';
|
||||||
|
if (in_array($this->ssl_mode, ['verify-full'])) {
|
||||||
|
$url .= '&tlsCAFile=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@@ -169,6 +169,11 @@ class StandaloneMysql extends BaseModel
|
|||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'environment.project.team');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sslCertificates()
|
||||||
|
{
|
||||||
|
return $this->morphMany(SslCertificate::class, 'resource');
|
||||||
|
}
|
||||||
|
|
||||||
public function link()
|
public function link()
|
||||||
{
|
{
|
||||||
if (data_get($this, 'environment.project.uuid')) {
|
if (data_get($this, 'environment.project.uuid')) {
|
||||||
@@ -219,7 +224,19 @@ class StandaloneMysql extends BaseModel
|
|||||||
protected function internalDbUrl(): Attribute
|
protected function internalDbUrl(): Attribute
|
||||||
{
|
{
|
||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: fn () => "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}",
|
get: function () {
|
||||||
|
$encodedUser = rawurlencode($this->mysql_user);
|
||||||
|
$encodedPass = rawurlencode($this->mysql_password);
|
||||||
|
$url = "mysql://{$encodedUser}:{$encodedPass}@{$this->uuid}:3306/{$this->mysql_database}";
|
||||||
|
if ($this->enable_ssl) {
|
||||||
|
$url .= "?ssl-mode={$this->ssl_mode}";
|
||||||
|
if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) {
|
||||||
|
$url .= '&ssl-ca=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +245,17 @@ class StandaloneMysql extends BaseModel
|
|||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: function () {
|
get: function () {
|
||||||
if ($this->is_public && $this->public_port) {
|
if ($this->is_public && $this->public_port) {
|
||||||
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
|
$encodedUser = rawurlencode($this->mysql_user);
|
||||||
|
$encodedPass = rawurlencode($this->mysql_password);
|
||||||
|
$url = "mysql://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
|
||||||
|
if ($this->enable_ssl) {
|
||||||
|
$url .= "?ssl-mode={$this->ssl_mode}";
|
||||||
|
if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) {
|
||||||
|
$url .= '&ssl-ca=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@@ -219,7 +219,19 @@ class StandalonePostgresql extends BaseModel
|
|||||||
protected function internalDbUrl(): Attribute
|
protected function internalDbUrl(): Attribute
|
||||||
{
|
{
|
||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: fn () => "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}",
|
get: function () {
|
||||||
|
$encodedUser = rawurlencode($this->postgres_user);
|
||||||
|
$encodedPass = rawurlencode($this->postgres_password);
|
||||||
|
$url = "postgres://{$encodedUser}:{$encodedPass}@{$this->uuid}:5432/{$this->postgres_db}";
|
||||||
|
if ($this->enable_ssl) {
|
||||||
|
$url .= "?sslmode={$this->ssl_mode}";
|
||||||
|
if (in_array($this->ssl_mode, ['verify-ca', 'verify-full'])) {
|
||||||
|
$url .= '&sslrootcert=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +240,17 @@ class StandalonePostgresql extends BaseModel
|
|||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: function () {
|
get: function () {
|
||||||
if ($this->is_public && $this->public_port) {
|
if ($this->is_public && $this->public_port) {
|
||||||
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
|
$encodedUser = rawurlencode($this->postgres_user);
|
||||||
|
$encodedPass = rawurlencode($this->postgres_password);
|
||||||
|
$url = "postgres://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
|
||||||
|
if ($this->enable_ssl) {
|
||||||
|
$url .= "?sslmode={$this->ssl_mode}";
|
||||||
|
if (in_array($this->ssl_mode, ['verify-ca', 'verify-full'])) {
|
||||||
|
$url .= '&sslrootcert=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -241,11 +263,21 @@ class StandalonePostgresql extends BaseModel
|
|||||||
return $this->belongsTo(Environment::class);
|
return $this->belongsTo(Environment::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function persistentStorages()
|
||||||
|
{
|
||||||
|
return $this->morphMany(LocalPersistentVolume::class, 'resource');
|
||||||
|
}
|
||||||
|
|
||||||
public function fileStorages()
|
public function fileStorages()
|
||||||
{
|
{
|
||||||
return $this->morphMany(LocalFileVolume::class, 'resource');
|
return $this->morphMany(LocalFileVolume::class, 'resource');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sslCertificates()
|
||||||
|
{
|
||||||
|
return $this->morphMany(SslCertificate::class, 'resource');
|
||||||
|
}
|
||||||
|
|
||||||
public function destination()
|
public function destination()
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
@@ -256,16 +288,17 @@ class StandalonePostgresql extends BaseModel
|
|||||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function persistentStorages()
|
|
||||||
{
|
|
||||||
return $this->morphMany(LocalPersistentVolume::class, 'resource');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scheduledBackups()
|
public function scheduledBackups()
|
||||||
{
|
{
|
||||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function environment_variables()
|
||||||
|
{
|
||||||
|
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
|
||||||
|
->orderBy('key', 'asc');
|
||||||
|
}
|
||||||
|
|
||||||
public function isBackupSolutionAvailable()
|
public function isBackupSolutionAvailable()
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@@ -314,10 +347,4 @@ class StandalonePostgresql extends BaseModel
|
|||||||
|
|
||||||
return $parsedCollection->toArray();
|
return $parsedCollection->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function environment_variables()
|
|
||||||
{
|
|
||||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
|
|
||||||
->orderBy('key', 'asc');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -170,6 +170,11 @@ class StandaloneRedis extends BaseModel
|
|||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'environment.project.team');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sslCertificates()
|
||||||
|
{
|
||||||
|
return $this->morphMany(SslCertificate::class, 'resource');
|
||||||
|
}
|
||||||
|
|
||||||
public function link()
|
public function link()
|
||||||
{
|
{
|
||||||
if (data_get($this, 'environment.project.uuid')) {
|
if (data_get($this, 'environment.project.uuid')) {
|
||||||
@@ -222,9 +227,17 @@ class StandaloneRedis extends BaseModel
|
|||||||
return new Attribute(
|
return new Attribute(
|
||||||
get: function () {
|
get: function () {
|
||||||
$redis_version = $this->getRedisVersion();
|
$redis_version = $this->getRedisVersion();
|
||||||
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
|
$username_part = version_compare($redis_version, '6.0', '>=') ? rawurlencode($this->redis_username).':' : '';
|
||||||
|
$encodedPass = rawurlencode($this->redis_password);
|
||||||
|
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||||
|
$port = $this->enable_ssl ? 6380 : 6379;
|
||||||
|
$url = "{$scheme}://{$username_part}{$encodedPass}@{$this->uuid}:{$port}/0";
|
||||||
|
|
||||||
return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
|
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||||
|
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -235,9 +248,16 @@ class StandaloneRedis extends BaseModel
|
|||||||
get: function () {
|
get: function () {
|
||||||
if ($this->is_public && $this->public_port) {
|
if ($this->is_public && $this->public_port) {
|
||||||
$redis_version = $this->getRedisVersion();
|
$redis_version = $this->getRedisVersion();
|
||||||
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
|
$username_part = version_compare($redis_version, '6.0', '>=') ? rawurlencode($this->redis_username).':' : '';
|
||||||
|
$encodedPass = rawurlencode($this->redis_password);
|
||||||
|
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||||
|
$url = "{$scheme}://{$username_part}{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||||
|
|
||||||
return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||||
|
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
151
app/Notifications/SslExpirationNotification.php
Normal file
151
app/Notifications/SslExpirationNotification.php
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use App\Notifications\Dto\DiscordMessage;
|
||||||
|
use App\Notifications\Dto\PushoverMessage;
|
||||||
|
use App\Notifications\Dto\SlackMessage;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Spatie\Url\Url;
|
||||||
|
|
||||||
|
class SslExpirationNotification extends CustomEmailNotification
|
||||||
|
{
|
||||||
|
protected Collection $resources;
|
||||||
|
|
||||||
|
protected array $urls = [];
|
||||||
|
|
||||||
|
public function __construct(array|Collection $resources)
|
||||||
|
{
|
||||||
|
$this->onQueue('high');
|
||||||
|
$this->resources = collect($resources);
|
||||||
|
|
||||||
|
// Collect URLs for each resource
|
||||||
|
$this->resources->each(function ($resource) {
|
||||||
|
if (data_get($resource, 'environment.project.uuid')) {
|
||||||
|
$routeName = match ($resource->type()) {
|
||||||
|
'application' => 'project.application.configuration',
|
||||||
|
'database' => 'project.database.configuration',
|
||||||
|
'service' => 'project.service.configuration',
|
||||||
|
default => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($routeName) {
|
||||||
|
$route = route($routeName, [
|
||||||
|
'project_uuid' => data_get($resource, 'environment.project.uuid'),
|
||||||
|
'environment_uuid' => data_get($resource, 'environment.uuid'),
|
||||||
|
$resource->type().'_uuid' => data_get($resource, 'uuid'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$settings = instanceSettings();
|
||||||
|
if (data_get($settings, 'fqdn')) {
|
||||||
|
$url = Url::fromString($route);
|
||||||
|
$url = $url->withPort(null);
|
||||||
|
$fqdn = data_get($settings, 'fqdn');
|
||||||
|
$fqdn = str_replace(['http://', 'https://'], '', $fqdn);
|
||||||
|
$url = $url->withHost($fqdn);
|
||||||
|
|
||||||
|
$this->urls[$resource->name] = $url->__toString();
|
||||||
|
} else {
|
||||||
|
$this->urls[$resource->name] = $route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return $notifiable->getEnabledChannels('ssl_certificate_renewal');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toMail(): MailMessage
|
||||||
|
{
|
||||||
|
$mail = new MailMessage;
|
||||||
|
$mail->subject('Coolify: [Action Required] SSL Certificates Renewed - Manual Redeployment Needed');
|
||||||
|
$mail->view('emails.ssl-certificate-renewed', [
|
||||||
|
'resources' => $this->resources,
|
||||||
|
'urls' => $this->urls,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $mail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toDiscord(): DiscordMessage
|
||||||
|
{
|
||||||
|
$resourceNames = $this->resources->pluck('name')->join(', ');
|
||||||
|
|
||||||
|
$message = new DiscordMessage(
|
||||||
|
title: '🔒 SSL Certificates Renewed',
|
||||||
|
description: "SSL certificates have been renewed for: {$resourceNames}.\n\n**Action Required:** These resources need to be redeployed manually.",
|
||||||
|
color: DiscordMessage::warningColor(),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($this->urls as $name => $url) {
|
||||||
|
$message->addField($name, "[View Resource]({$url})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toTelegram(): array
|
||||||
|
{
|
||||||
|
$resourceNames = $this->resources->pluck('name')->join(', ');
|
||||||
|
$message = "Coolify: SSL certificates have been renewed for: {$resourceNames}.\n\nAction Required: These resources need to be redeployed manually for the new SSL certificates to take effect.";
|
||||||
|
|
||||||
|
$buttons = [];
|
||||||
|
foreach ($this->urls as $name => $url) {
|
||||||
|
$buttons[] = [
|
||||||
|
'text' => "View {$name}",
|
||||||
|
'url' => $url,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'message' => $message,
|
||||||
|
'buttons' => $buttons,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toPushover(): PushoverMessage
|
||||||
|
{
|
||||||
|
$resourceNames = $this->resources->pluck('name')->join(', ');
|
||||||
|
$message = "SSL certificates have been renewed for: {$resourceNames}<br/><br/>";
|
||||||
|
$message .= '<b>Action Required:</b> These resources need to be redeployed manually for the new SSL certificates to take effect.';
|
||||||
|
|
||||||
|
$buttons = [];
|
||||||
|
foreach ($this->urls as $name => $url) {
|
||||||
|
$buttons[] = [
|
||||||
|
'text' => "View {$name}",
|
||||||
|
'url' => $url,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PushoverMessage(
|
||||||
|
title: 'SSL Certificates Renewed',
|
||||||
|
level: 'warning',
|
||||||
|
message: $message,
|
||||||
|
buttons: $buttons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toSlack(): SlackMessage
|
||||||
|
{
|
||||||
|
$resourceNames = $this->resources->pluck('name')->join(', ');
|
||||||
|
$description = "SSL certificates have been renewed for: {$resourceNames}\n\n";
|
||||||
|
$description .= '**Action Required:** These resources need to be redeployed manually for the new SSL certificates to take effect.';
|
||||||
|
|
||||||
|
if (! empty($this->urls)) {
|
||||||
|
$description .= "\n\n**Resource URLs:**\n";
|
||||||
|
foreach ($this->urls as $name => $url) {
|
||||||
|
$description .= "• {$name}: {$url}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SlackMessage(
|
||||||
|
title: '🔒 SSL Certificates Renewed',
|
||||||
|
description: $description,
|
||||||
|
color: SlackMessage::warningColor()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -16,6 +16,7 @@ trait HasNotificationSettings
|
|||||||
'server_force_disabled',
|
'server_force_disabled',
|
||||||
'general',
|
'general',
|
||||||
'test',
|
'test',
|
||||||
|
'ssl_certificate_renewal',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -4,7 +4,6 @@ namespace App\View\Components\Forms;
|
|||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Illuminate\View\Component;
|
use Illuminate\View\Component;
|
||||||
use Visus\Cuid2\Cuid2;
|
use Visus\Cuid2\Cuid2;
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ class Select extends Component
|
|||||||
public ?string $label = null,
|
public ?string $label = null,
|
||||||
public ?string $helper = null,
|
public ?string $helper = null,
|
||||||
public bool $required = false,
|
public bool $required = false,
|
||||||
public string $defaultClass = 'select'
|
public string $defaultClass = 'select w-full'
|
||||||
) {
|
) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@@ -36,8 +35,6 @@ class Select extends Component
|
|||||||
$this->name = $this->id;
|
$this->name = $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->label = Str::title($this->label);
|
|
||||||
|
|
||||||
return view('components.forms.select');
|
return view('components.forms.select');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,16 +16,12 @@ use Illuminate\Support\Collection;
|
|||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Visus\Cuid2\Cuid2;
|
use Visus\Cuid2\Cuid2;
|
||||||
|
|
||||||
function generate_database_name(string $type): string
|
|
||||||
{
|
|
||||||
return $type.'-database-'.(new Cuid2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null, string $databaseImage = 'postgres:16-alpine'): StandalonePostgresql
|
function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null, string $databaseImage = 'postgres:16-alpine'): StandalonePostgresql
|
||||||
{
|
{
|
||||||
$destination = StandaloneDocker::where('uuid', $destinationUuid)->firstOrFail();
|
$destination = StandaloneDocker::where('uuid', $destinationUuid)->firstOrFail();
|
||||||
$database = new StandalonePostgresql;
|
$database = new StandalonePostgresql;
|
||||||
$database->name = generate_database_name('postgresql');
|
$database->uuid = (new Cuid2);
|
||||||
|
$database->name = 'postgresql-database-'.$database->uuid;
|
||||||
$database->image = $databaseImage;
|
$database->image = $databaseImage;
|
||||||
$database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
$database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||||
$database->environment_id = $environmentId;
|
$database->environment_id = $environmentId;
|
||||||
@@ -43,7 +39,8 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
|
|||||||
{
|
{
|
||||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
||||||
$database = new StandaloneRedis;
|
$database = new StandaloneRedis;
|
||||||
$database->name = generate_database_name('redis');
|
$database->uuid = (new Cuid2);
|
||||||
|
$database->name = 'redis-database-'.$database->uuid;
|
||||||
$redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
$redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||||
$database->environment_id = $environment_id;
|
$database->environment_id = $environment_id;
|
||||||
$database->destination_id = $destination->id;
|
$database->destination_id = $destination->id;
|
||||||
@@ -76,7 +73,8 @@ function create_standalone_mongodb($environment_id, $destination_uuid, ?array $o
|
|||||||
{
|
{
|
||||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
||||||
$database = new StandaloneMongodb;
|
$database = new StandaloneMongodb;
|
||||||
$database->name = generate_database_name('mongodb');
|
$database->uuid = (new Cuid2);
|
||||||
|
$database->name = 'mongodb-database-'.$database->uuid;
|
||||||
$database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
$database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||||
$database->environment_id = $environment_id;
|
$database->environment_id = $environment_id;
|
||||||
$database->destination_id = $destination->id;
|
$database->destination_id = $destination->id;
|
||||||
@@ -93,7 +91,8 @@ function create_standalone_mysql($environment_id, $destination_uuid, ?array $oth
|
|||||||
{
|
{
|
||||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
||||||
$database = new StandaloneMysql;
|
$database = new StandaloneMysql;
|
||||||
$database->name = generate_database_name('mysql');
|
$database->uuid = (new Cuid2);
|
||||||
|
$database->name = 'mysql-database-'.$database->uuid;
|
||||||
$database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
$database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||||
$database->mysql_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
$database->mysql_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||||
$database->environment_id = $environment_id;
|
$database->environment_id = $environment_id;
|
||||||
@@ -111,7 +110,8 @@ function create_standalone_mariadb($environment_id, $destination_uuid, ?array $o
|
|||||||
{
|
{
|
||||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
||||||
$database = new StandaloneMariadb;
|
$database = new StandaloneMariadb;
|
||||||
$database->name = generate_database_name('mariadb');
|
$database->uuid = (new Cuid2);
|
||||||
|
$database->name = 'mariadb-database-'.$database->uuid;
|
||||||
$database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
$database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||||
$database->mariadb_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
$database->mariadb_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||||
$database->environment_id = $environment_id;
|
$database->environment_id = $environment_id;
|
||||||
@@ -129,7 +129,8 @@ function create_standalone_keydb($environment_id, $destination_uuid, ?array $oth
|
|||||||
{
|
{
|
||||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
||||||
$database = new StandaloneKeydb;
|
$database = new StandaloneKeydb;
|
||||||
$database->name = generate_database_name('keydb');
|
$database->uuid = (new Cuid2);
|
||||||
|
$database->name = 'keydb-database-'.$database->uuid;
|
||||||
$database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
$database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||||
$database->environment_id = $environment_id;
|
$database->environment_id = $environment_id;
|
||||||
$database->destination_id = $destination->id;
|
$database->destination_id = $destination->id;
|
||||||
@@ -146,7 +147,8 @@ function create_standalone_dragonfly($environment_id, $destination_uuid, ?array
|
|||||||
{
|
{
|
||||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
||||||
$database = new StandaloneDragonfly;
|
$database = new StandaloneDragonfly;
|
||||||
$database->name = generate_database_name('dragonfly');
|
$database->uuid = (new Cuid2);
|
||||||
|
$database->name = 'dragonfly-database-'.$database->uuid;
|
||||||
$database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
$database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||||
$database->environment_id = $environment_id;
|
$database->environment_id = $environment_id;
|
||||||
$database->destination_id = $destination->id;
|
$database->destination_id = $destination->id;
|
||||||
@@ -163,7 +165,8 @@ function create_standalone_clickhouse($environment_id, $destination_uuid, ?array
|
|||||||
{
|
{
|
||||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
$destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail();
|
||||||
$database = new StandaloneClickhouse;
|
$database = new StandaloneClickhouse;
|
||||||
$database->name = generate_database_name('clickhouse');
|
$database->uuid = (new Cuid2);
|
||||||
|
$database->name = 'clickhouse-database-'.$database->uuid;
|
||||||
$database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
$database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||||
$database->environment_id = $environment_id;
|
$database->environment_id = $environment_id;
|
||||||
$database->destination_id = $destination->id;
|
$database->destination_id = $destination->id;
|
||||||
|
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('standalone_postgresqls', function (Blueprint $table) {
|
||||||
|
$table->boolean('enable_ssl')->default(false);
|
||||||
|
$table->enum('ssl_mode', ['allow', 'prefer', 'require', 'verify-ca', 'verify-full'])->default('require');
|
||||||
|
});
|
||||||
|
Schema::table('standalone_mysqls', function (Blueprint $table) {
|
||||||
|
$table->boolean('enable_ssl')->default(false);
|
||||||
|
$table->enum('ssl_mode', ['PREFERRED', 'REQUIRED', 'VERIFY_CA', 'VERIFY_IDENTITY'])->default('REQUIRED');
|
||||||
|
});
|
||||||
|
Schema::table('standalone_mariadbs', function (Blueprint $table) {
|
||||||
|
$table->boolean('enable_ssl')->default(false);
|
||||||
|
});
|
||||||
|
Schema::table('standalone_redis', function (Blueprint $table) {
|
||||||
|
$table->boolean('enable_ssl')->default(false);
|
||||||
|
});
|
||||||
|
Schema::table('standalone_keydbs', function (Blueprint $table) {
|
||||||
|
$table->boolean('enable_ssl')->default(false);
|
||||||
|
});
|
||||||
|
Schema::table('standalone_dragonflies', function (Blueprint $table) {
|
||||||
|
$table->boolean('enable_ssl')->default(false);
|
||||||
|
});
|
||||||
|
Schema::table('standalone_mongodbs', function (Blueprint $table) {
|
||||||
|
$table->boolean('enable_ssl')->default(true);
|
||||||
|
$table->enum('ssl_mode', ['allow', 'prefer', 'require', 'verify-full'])->default('require');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('standalone_postgresqls', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('enable_ssl');
|
||||||
|
$table->dropColumn('ssl_mode');
|
||||||
|
});
|
||||||
|
Schema::table('standalone_mysqls', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('enable_ssl');
|
||||||
|
$table->dropColumn('ssl_mode');
|
||||||
|
});
|
||||||
|
Schema::table('standalone_mariadbs', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('enable_ssl');
|
||||||
|
});
|
||||||
|
Schema::table('standalone_redis', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('enable_ssl');
|
||||||
|
});
|
||||||
|
Schema::table('standalone_keydbs', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('enable_ssl');
|
||||||
|
});
|
||||||
|
Schema::table('standalone_dragonflies', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('enable_ssl');
|
||||||
|
});
|
||||||
|
Schema::table('standalone_mongodbs', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('enable_ssl');
|
||||||
|
$table->dropColumn('ssl_mode');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('ssl_certificates', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->text('ssl_certificate');
|
||||||
|
$table->text('ssl_private_key');
|
||||||
|
$table->text('configuration_dir')->nullable();
|
||||||
|
$table->text('mount_path')->nullable();
|
||||||
|
$table->string('resource_type')->nullable();
|
||||||
|
$table->unsignedBigInteger('resource_id')->nullable();
|
||||||
|
$table->unsignedBigInteger('server_id');
|
||||||
|
$table->text('common_name');
|
||||||
|
$table->json('subject_alternative_names')->nullable();
|
||||||
|
$table->timestamp('valid_until');
|
||||||
|
$table->boolean('is_ca_certificate')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('server_id')->references('id')->on('servers');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('ssl_certificates');
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('local_file_volumes', function (Blueprint $table) {
|
||||||
|
$table->text('mount_path')->nullable()->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (DB::table('local_file_volumes')->exists()) {
|
||||||
|
DB::table('local_file_volumes')
|
||||||
|
->orderBy('id')
|
||||||
|
->chunk(100, function ($volumes) {
|
||||||
|
foreach ($volumes as $volume) {
|
||||||
|
try {
|
||||||
|
DB::table('local_file_volumes')->where('id', $volume->id)->update([
|
||||||
|
'fs_path' => $volume->fs_path ? Crypt::encryptString($volume->fs_path) : null,
|
||||||
|
'mount_path' => $volume->mount_path ? Crypt::encryptString($volume->mount_path) : null,
|
||||||
|
'content' => $volume->content ? Crypt::encryptString($volume->content) : null,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error encrypting local file volume fields: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('local_file_volumes', function (Blueprint $table) {
|
||||||
|
$table->string('fs_path')->change();
|
||||||
|
$table->string('mount_path')->nullable()->change();
|
||||||
|
$table->longText('content')->nullable()->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (DB::table('local_file_volumes')->exists()) {
|
||||||
|
DB::table('local_file_volumes')
|
||||||
|
->orderBy('id')
|
||||||
|
->chunk(100, function ($volumes) {
|
||||||
|
foreach ($volumes as $volume) {
|
||||||
|
try {
|
||||||
|
DB::table('local_file_volumes')->where('id', $volume->id)->update([
|
||||||
|
'fs_path' => $volume->fs_path ? Crypt::decryptString($volume->fs_path) : null,
|
||||||
|
'mount_path' => $volume->mount_path ? Crypt::decryptString($volume->mount_path) : null,
|
||||||
|
'content' => $volume->content ? Crypt::decryptString($volume->content) : null,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error decrypting local file volume fields: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
43
database/seeders/CaSslCertSeeder.php
Normal file
43
database/seeders/CaSslCertSeeder.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Helpers\SslHelper;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\SslCertificate;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class CaSslCertSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
Server::chunk(200, function ($servers) {
|
||||||
|
foreach ($servers as $server) {
|
||||||
|
$existingCaCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||||
|
|
||||||
|
if (! $existingCaCert) {
|
||||||
|
$caCert = SslHelper::generateSslCertificate(
|
||||||
|
commonName: 'Coolify CA Certificate',
|
||||||
|
serverId: $server->id,
|
||||||
|
isCaCertificate: true,
|
||||||
|
validityDays: 10 * 365
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$caCert = $existingCaCert;
|
||||||
|
}
|
||||||
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$commands = collect([
|
||||||
|
"mkdir -p $caCertPath",
|
||||||
|
"chown -R 9999:root $caCertPath",
|
||||||
|
"chmod -R 700 $caCertPath",
|
||||||
|
"rm -rf $caCertPath/coolify-ca.crt",
|
||||||
|
"echo '{$caCert->ssl_certificate}' > $caCertPath/coolify-ca.crt",
|
||||||
|
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||||
|
]);
|
||||||
|
|
||||||
|
remote_process($commands, $server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -28,6 +28,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
OauthSettingSeeder::class,
|
OauthSettingSeeder::class,
|
||||||
DisableTwoStepConfirmationSeeder::class,
|
DisableTwoStepConfirmationSeeder::class,
|
||||||
SentinelSeeder::class,
|
SentinelSeeder::class,
|
||||||
|
CaSslCertSeeder::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -193,5 +193,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
|||||||
$this->call(PopulateSshKeysDirectorySeeder::class);
|
$this->call(PopulateSshKeysDirectorySeeder::class);
|
||||||
$this->call(SentinelSeeder::class);
|
$this->call(SentinelSeeder::class);
|
||||||
$this->call(RootUserSeeder::class);
|
$this->call(RootUserSeeder::class);
|
||||||
|
$this->call(CaSslCertSeeder::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,3 @@
|
|||||||
// import { createApp } from "vue";
|
|
||||||
// import MagicBar from "./components/MagicBar.vue";
|
|
||||||
|
|
||||||
// const app = createApp({});
|
|
||||||
// app.component("magic-bar", MagicBar);
|
|
||||||
// app.mount("#vue");
|
|
||||||
|
|
||||||
import { initializeTerminalComponent } from './terminal.js';
|
import { initializeTerminalComponent } from './terminal.js';
|
||||||
|
|
||||||
['livewire:navigated', 'alpine:init'].forEach((event) => {
|
['livewire:navigated', 'alpine:init'].forEach((event) => {
|
||||||
|
@@ -1,682 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Transition name="fade">
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center p-1 px-2 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-100"
|
|
||||||
@click="showCommandPalette = true">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" 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 d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
|
|
||||||
<path d="M21 21l-6 -6" />
|
|
||||||
</svg>
|
|
||||||
<span class="flex-1"></span>
|
|
||||||
<span class="ml-2 kbd-custom">/</span>
|
|
||||||
</div>
|
|
||||||
<div class="relative" role="dialog" aria-modal="true" v-if="showCommandPalette" @keyup.esc="resetState">
|
|
||||||
<div class="fixed inset-0 transition-opacity bg-opacity-90 bg-coolgray-100" @click.self="resetState">
|
|
||||||
</div>
|
|
||||||
<div class="fixed inset-0 p-4 mx-auto overflow-y-auto lg:w-[70rem] sm:p-10 md:px-20"
|
|
||||||
@click.self="resetState">
|
|
||||||
<div class="overflow-hidden transition-all transform bg-coolgray-200 ring-1 ring-black ring-opacity-5">
|
|
||||||
<div class="relative">
|
|
||||||
<svg class="absolute w-5 h-5 text-gray-400 pointer-events-none left-3 top-2.5"
|
|
||||||
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
<input type="text" v-model="search" ref="searchInput" @keydown.down="focusNext(magic.length)"
|
|
||||||
@keydown.up="focusPrev(magic.length)" @keyup.enter="callAction"
|
|
||||||
class="w-full h-10 pr-4 rounded outline-none dark:text-white bg-coolgray-400 pl-11 placeholder:text-neutral-700 sm:text-sm focus:outline-none"
|
|
||||||
placeholder="Search, jump or create... magically... 🪄" role="combobox"
|
|
||||||
aria-expanded="false" aria-controls="options">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="px-4 pb-2 overflow-y-auto max-h-96 scroll-py-10 scroll-pb-2 scrollbar" id="options"
|
|
||||||
role="listbox">
|
|
||||||
<li v-if="sequenceState.sequence.length !== 0">
|
|
||||||
<h2 v-if="sequenceState.sequence[sequenceState.currentActionIndex] && possibleSequences[sequenceState.sequence[sequenceState.currentActionIndex]]"
|
|
||||||
class="mt-4 mb-2 text-xs font-semibold text-neutral-500">{{
|
|
||||||
possibleSequences[sequenceState.sequence[sequenceState.currentActionIndex]].newTitle }}
|
|
||||||
</h2>
|
|
||||||
<ul class="mt-2 -mx-4 dark:text-white">
|
|
||||||
<li class="flex items-center px-4 py-2 cursor-pointer select-none group hover:bg-coolgray-400"
|
|
||||||
id="option-1" role="option" tabindex="-1"
|
|
||||||
@click="addNew(sequenceState.sequence[sequenceState.currentActionIndex])">
|
|
||||||
<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" />
|
|
||||||
<path d="M12 5l0 14" />
|
|
||||||
<path d="M5 12l14 0" />
|
|
||||||
</svg>
|
|
||||||
<span class="flex-auto ml-3 truncate">
|
|
||||||
<span v-if="search"><span class="capitalize ">{{
|
|
||||||
sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name
|
|
||||||
will be:
|
|
||||||
<span class="inline-block dark:text-warning">{{ search }}</span>
|
|
||||||
</span>
|
|
||||||
<span v-else><span class="capitalize ">{{
|
|
||||||
sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name
|
|
||||||
will be:
|
|
||||||
<span class="inline-block dark:text-warning">randomly generated (type to
|
|
||||||
change)</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<ul v-if="magic.length == 0" class="mt-2 -mx-4 dark:text-white">
|
|
||||||
<li class="flex items-center px-4 py-2 select-none group">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" 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="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
|
||||||
<path d="M9 10l.01 0" />
|
|
||||||
<path d="M15 10l.01 0" />
|
|
||||||
<path d="M9 15l6 0" />
|
|
||||||
</svg>
|
|
||||||
<span class="flex-auto ml-3 truncate">Nothing found. Ooops.</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h2 v-if="magic.length !== 0 && sequenceState.sequence[sequenceState.currentActionIndex] && possibleSequences[sequenceState.sequence[sequenceState.currentActionIndex]]"
|
|
||||||
class="mt-4 mb-2 text-xs font-semibold text-neutral-500">{{
|
|
||||||
possibleSequences[sequenceState.sequence[sequenceState.currentActionIndex]].title }}
|
|
||||||
</h2>
|
|
||||||
<ul v-if="magic.length != 0" class="mt-2 -mx-4 dark:text-white">
|
|
||||||
<li class="flex items-center px-4 py-2 transition-all cursor-pointer select-none group hover:bg-coolgray-400"
|
|
||||||
:class="{ 'bg-coollabs': currentFocus === index }" id="option-1" role="option"
|
|
||||||
tabindex="-1" v-for="action, index in magic" @click="goThroughSequence(index)"
|
|
||||||
ref="magicItems">
|
|
||||||
<div class="relative">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round">
|
|
||||||
<template
|
|
||||||
v-if="action.icon === 'git' || sequenceState.sequence[sequenceState.currentActionIndex] === 'git'">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M16 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M12 8m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M12 16m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M12 15v-6" />
|
|
||||||
<path d="M15 11l-2 -2" />
|
|
||||||
<path d="M11 7l-1.9 -1.9" />
|
|
||||||
<path
|
|
||||||
d="M13.446 2.6l7.955 7.954a2.045 2.045 0 0 1 0 2.892l-7.955 7.955a2.045 2.045 0 0 1 -2.892 0l-7.955 -7.955a2.045 2.045 0 0 1 0 -2.892l7.955 -7.955a2.045 2.045 0 0 1 2.892 0z" />
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="action.icon === 'server' || sequenceState.sequence[sequenceState.currentActionIndex] === 'server'">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path
|
|
||||||
d="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z" />
|
|
||||||
<path d="M15 20h-9a3 3 0 0 1 -3 -3v-2a3 3 0 0 1 3 -3h12" />
|
|
||||||
<path d="M7 8v.01" />
|
|
||||||
<path d="M7 16v.01" />
|
|
||||||
<path d="M20 15l-2 3h3l-2 3" />
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="action.icon === 'destination' || sequenceState.sequence[sequenceState.currentActionIndex] === 'destination'">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path
|
|
||||||
d="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z" />
|
|
||||||
<path d="M5 10h3v3h-3z" />
|
|
||||||
<path d="M8 10h3v3h-3z" />
|
|
||||||
<path d="M11 10h3v3h-3z" />
|
|
||||||
<path d="M8 7h3v3h-3z" />
|
|
||||||
<path d="M11 7h3v3h-3z" />
|
|
||||||
<path d="M11 4h3v3h-3z" />
|
|
||||||
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
|
|
||||||
<path d="M10 16l0 .01" />
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="action.icon === 'storage' || sequenceState.sequence[sequenceState.currentActionIndex] === 'storage'">
|
|
||||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round" stroke-width="2">
|
|
||||||
<path d="M4 6a8 3 0 1 0 16 0A8 3 0 1 0 4 6" />
|
|
||||||
<path d="M4 6v6a8 3 0 0 0 16 0V6" />
|
|
||||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
|
||||||
</g>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="action.icon === 'project' || sequenceState.sequence[sequenceState.currentActionIndex] === 'project'">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path
|
|
||||||
d="M9 4h3l2 2h5a2 2 0 0 1 2 2v7a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-9a2 2 0 0 1 2 -2" />
|
|
||||||
<path
|
|
||||||
d="M17 17v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-9a2 2 0 0 1 2 -2h2" />
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="action.icon === 'environment' || sequenceState.sequence[sequenceState.currentActionIndex] === 'environment'">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M16 5l3 3l-2 1l4 4l-3 1l4 4h-9" />
|
|
||||||
<path d="M15 21l0 -3" />
|
|
||||||
<path d="M8 13l-2 -2" />
|
|
||||||
<path d="M8 12l2 -2" />
|
|
||||||
<path d="M8 21v-13" />
|
|
||||||
<path
|
|
||||||
d="M5.824 16a3 3 0 0 1 -2.743 -3.69a3 3 0 0 1 .304 -4.833a3 3 0 0 1 4.615 -3.707a3 3 0 0 1 4.614 3.707a3 3 0 0 1 .305 4.833a3 3 0 0 1 -2.919 3.695h-4z" />
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="action.icon === 'key' || sequenceState.sequence[sequenceState.currentActionIndex] === 'key'">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M14 10m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
|
||||||
<path d="M21 12a9 9 0 1 1 -18 0a9 9 0 0 1 18 0z" />
|
|
||||||
<path d="M12.5 11.5l-4 4l1.5 1.5" />
|
|
||||||
<path d="M12 15l-1.5 -1.5" />
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="action.icon === 'goto' || sequenceState.sequence[sequenceState.currentActionIndex] === 'goto'">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M10 18h4" />
|
|
||||||
<path
|
|
||||||
d="M3 8a9 9 0 0 1 9 9v1l1.428 -4.285a12 12 0 0 1 6.018 -6.938l.554 -.277" />
|
|
||||||
<path d="M15 6h5v5" />
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="action.icon === 'team' || sequenceState.sequence[sequenceState.currentActionIndex] === 'team'">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
|
||||||
<path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1" />
|
|
||||||
<path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
|
||||||
<path d="M17 10h2a2 2 0 0 1 2 2v1" />
|
|
||||||
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
|
||||||
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
|
|
||||||
</template>
|
|
||||||
</svg>
|
|
||||||
<div v-if="action.new"
|
|
||||||
class="absolute top-0 right-0 -mt-2 -mr-2 font-bold dark:text-warning">+
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="flex-auto ml-3 truncate">{{ action.name }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
||||||
import axios from "axios";
|
|
||||||
const currentFocus = ref(0)
|
|
||||||
const magicItems = ref()
|
|
||||||
function focusNext(length) {
|
|
||||||
if (currentFocus.value === undefined) {
|
|
||||||
currentFocus.value = 0
|
|
||||||
} else {
|
|
||||||
if (length > currentFocus.value + 1) {
|
|
||||||
currentFocus.value = currentFocus.value + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentFocus.value > 4) {
|
|
||||||
magicItems.value[currentFocus.value].scrollIntoView({ block: "center", inline: "center", behavior: 'auto' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function focusPrev(length) {
|
|
||||||
if (currentFocus.value === undefined) {
|
|
||||||
currentFocus.value = length - 1
|
|
||||||
} else {
|
|
||||||
if (currentFocus.value > 0) {
|
|
||||||
currentFocus.value = currentFocus.value - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentFocus.value < length - 4) {
|
|
||||||
magicItems.value[currentFocus.value].scrollIntoView({ block: "center", inline: "center", behavior: 'auto' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function callAction() {
|
|
||||||
await goThroughSequence(currentFocus.value)
|
|
||||||
}
|
|
||||||
const showCommandPalette = ref(false)
|
|
||||||
const search = ref()
|
|
||||||
const searchInput = ref()
|
|
||||||
|
|
||||||
const baseUrl = '/magic'
|
|
||||||
|
|
||||||
const uuidSelector = ['project', 'destination']
|
|
||||||
const nameSelector = ['environment']
|
|
||||||
const possibleSequences = {
|
|
||||||
server: {
|
|
||||||
newTitle: 'Create a new Server',
|
|
||||||
title: 'Select a server'
|
|
||||||
},
|
|
||||||
destination: {
|
|
||||||
newTitle: 'Create a new Destination',
|
|
||||||
title: 'Select a destination'
|
|
||||||
},
|
|
||||||
project: {
|
|
||||||
newTitle: 'Create a new Project',
|
|
||||||
title: 'Select a project'
|
|
||||||
},
|
|
||||||
environment: {
|
|
||||||
newTitle: 'Create a new Environment',
|
|
||||||
title: 'Select an environment'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const magicActions = [{
|
|
||||||
id: 1,
|
|
||||||
name: 'Deploy: Public Repository',
|
|
||||||
tags: 'git,github,public',
|
|
||||||
icon: 'git',
|
|
||||||
new: true,
|
|
||||||
sequence: ['main', 'server', 'destination', 'project', 'environment', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Deploy: Private Repository (with GitHub Apps)',
|
|
||||||
tags: 'git,github,private',
|
|
||||||
icon: 'git',
|
|
||||||
new: true,
|
|
||||||
sequence: ['main', 'server', 'destination', 'project', 'environment', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Deploy: Private Repository (with Deploy Key)',
|
|
||||||
tags: 'git,github,private,deploy,key',
|
|
||||||
icon: 'git',
|
|
||||||
new: true,
|
|
||||||
sequence: ['main', 'server', 'destination', 'project', 'environment', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Deploy: Dockerfile',
|
|
||||||
tags: 'dockerfile,deploy',
|
|
||||||
icon: 'destination',
|
|
||||||
new: true,
|
|
||||||
sequence: ['main', 'server', 'destination', 'project', 'environment', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: 'Create: Server',
|
|
||||||
tags: 'server,ssh,new,create',
|
|
||||||
icon: 'server',
|
|
||||||
new: true,
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: 'Create: Source',
|
|
||||||
tags: 'source,git,gitlab,github,bitbucket,gitea,new,create',
|
|
||||||
icon: 'git',
|
|
||||||
new: true,
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
name: 'Create: Private Key',
|
|
||||||
tags: 'private,key,ssh,new,create',
|
|
||||||
icon: 'key',
|
|
||||||
new: true,
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: 'Create: Destination',
|
|
||||||
tags: 'destination,docker,network,new,create',
|
|
||||||
icon: 'destination',
|
|
||||||
new: true,
|
|
||||||
sequence: ['main', 'server', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
name: 'Create: Team',
|
|
||||||
tags: 'team,member,new,create',
|
|
||||||
icon: 'team',
|
|
||||||
new: true,
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
name: 'Create: S3 Storage',
|
|
||||||
tags: 's3,storage,new,create',
|
|
||||||
icon: 'storage',
|
|
||||||
new: true,
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
name: 'Goto: S3 Storage',
|
|
||||||
tags: 's3,storage',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
name: 'Goto: Dashboard',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
name: 'Goto: Servers',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
name: 'Goto: Private Keys',
|
|
||||||
tags: 'destination,docker,network,new,create,ssh,private,key',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
name: 'Goto: Projects',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
name: 'Goto: Sources',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 17,
|
|
||||||
name: 'Goto: Destinations',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
name: 'Goto: Settings',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 19,
|
|
||||||
name: 'Goto: Terminal',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20,
|
|
||||||
name: 'Goto: Notifications',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 21,
|
|
||||||
name: 'Goto: Profile',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 22,
|
|
||||||
name: 'Goto: Teams',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 23,
|
|
||||||
name: 'Goto: Switch Teams',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 24,
|
|
||||||
name: 'Goto: Onboarding process',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 25,
|
|
||||||
name: 'Goto: API Tokens',
|
|
||||||
tags: 'api,tokens,rest',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 26,
|
|
||||||
name: 'Goto: Team Shared Variables',
|
|
||||||
tags: 'team,shared,variables',
|
|
||||||
icon: 'goto',
|
|
||||||
sequence: ['main', 'redirect']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
const initialState = {
|
|
||||||
sequence: [],
|
|
||||||
currentActionIndex: 0,
|
|
||||||
magicActions,
|
|
||||||
selected: {}
|
|
||||||
}
|
|
||||||
const sequenceState = ref({ ...initialState })
|
|
||||||
|
|
||||||
function focusSearch(event) {
|
|
||||||
if (event.target.nodeName === 'BODY') {
|
|
||||||
if (event.key === '/') {
|
|
||||||
event.preventDefault();
|
|
||||||
showCommandPalette.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener("keydown", focusSearch);
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener("keydown", focusSearch);
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(showCommandPalette, async (value) => {
|
|
||||||
if (value) {
|
|
||||||
await nextTick();
|
|
||||||
searchInput.value.focus();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
watch(search, async () => {
|
|
||||||
currentFocus.value = 0
|
|
||||||
})
|
|
||||||
const magic = computed(() => {
|
|
||||||
if (search.value) {
|
|
||||||
return sequenceState.value.magicActions.filter(action => {
|
|
||||||
return action.name.toLowerCase().includes(search.value.toLowerCase()) || action.tags?.toLowerCase().includes(search.value.toLowerCase())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return sequenceState.value.magicActions
|
|
||||||
})
|
|
||||||
async function addNew(name) {
|
|
||||||
let targetUrl = new URL(window.location.origin)
|
|
||||||
let newUrl = new URL(`${window.location.origin}${baseUrl}/${name}/new`);
|
|
||||||
if (search.value) {
|
|
||||||
targetUrl.searchParams.append('name', search.value)
|
|
||||||
newUrl.searchParams.append('name', search.value)
|
|
||||||
}
|
|
||||||
switch (name) {
|
|
||||||
case 'server':
|
|
||||||
targetUrl.pathname = '/server/new'
|
|
||||||
window.location.href = targetUrl.href
|
|
||||||
break;
|
|
||||||
case 'destination':
|
|
||||||
targetUrl.pathname = '/destination/new'
|
|
||||||
window.location.href = targetUrl.href
|
|
||||||
break;
|
|
||||||
case 'project':
|
|
||||||
const { data: { project_uuid } } = await axios(newUrl.href)
|
|
||||||
search.value = ''
|
|
||||||
sequenceState.value.selected['project'] = project_uuid
|
|
||||||
sequenceState.value.magicActions = await getEnvironments(project_uuid)
|
|
||||||
sequenceState.value.currentActionIndex += 1
|
|
||||||
break;
|
|
||||||
case 'environment':
|
|
||||||
newUrl.searchParams.append('project_uuid', sequenceState.value.selected.project)
|
|
||||||
const { data: { environment_name } } = await axios(newUrl.href)
|
|
||||||
search.value = ''
|
|
||||||
sequenceState.value.selected['environment'] = environment_name
|
|
||||||
redirect()
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function resetState() {
|
|
||||||
showCommandPalette.value = false
|
|
||||||
sequenceState.value = { ...initialState }
|
|
||||||
search.value = ''
|
|
||||||
}
|
|
||||||
async function goThroughSequence(actionId) {
|
|
||||||
let currentSequence = null;
|
|
||||||
let nextSequence = null;
|
|
||||||
if (sequenceState.value.selected.main === undefined) {
|
|
||||||
const { sequence, id } = magic.value[actionId];
|
|
||||||
currentSequence = sequence[sequenceState.value.currentActionIndex]
|
|
||||||
nextSequence = sequence[sequenceState.value.currentActionIndex + 1]
|
|
||||||
sequenceState.value.sequence = sequence
|
|
||||||
sequenceState.value.selected = {
|
|
||||||
main: id
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentSequence = sequenceState.value.sequence[sequenceState.value.currentActionIndex]
|
|
||||||
nextSequence = sequenceState.value.sequence[sequenceState.value.currentActionIndex + 1]
|
|
||||||
let selectedId = sequenceState.value.magicActions[actionId].id
|
|
||||||
if (uuidSelector.includes(currentSequence)) {
|
|
||||||
selectedId = sequenceState.value.magicActions[actionId].uuid
|
|
||||||
}
|
|
||||||
if (nameSelector.includes(currentSequence)) {
|
|
||||||
selectedId = sequenceState.value.magicActions[actionId].name
|
|
||||||
}
|
|
||||||
sequenceState.value.selected = {
|
|
||||||
...sequenceState.value.selected,
|
|
||||||
[currentSequence]: selectedId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (nextSequence) {
|
|
||||||
case 'server':
|
|
||||||
sequenceState.value.magicActions = await getServers();
|
|
||||||
break;
|
|
||||||
case 'destination':
|
|
||||||
sequenceState.value.magicActions = await getDestinations(sequenceState.value.selected[currentSequence]);
|
|
||||||
break;
|
|
||||||
case 'project':
|
|
||||||
sequenceState.value.magicActions = await getProjects()
|
|
||||||
break;
|
|
||||||
case 'environment':
|
|
||||||
sequenceState.value.magicActions = await getEnvironments(sequenceState.value.selected[currentSequence])
|
|
||||||
break;
|
|
||||||
case 'redirect':
|
|
||||||
redirect()
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sequenceState.value.currentActionIndex += 1
|
|
||||||
search.value = ''
|
|
||||||
searchInput.value.focus()
|
|
||||||
currentFocus.value = 0
|
|
||||||
}
|
|
||||||
async function getServers() {
|
|
||||||
const { data: { servers } } = await axios.get(`${baseUrl}/servers`);
|
|
||||||
return servers;
|
|
||||||
}
|
|
||||||
async function getDestinations(serverId) {
|
|
||||||
const { data: { destinations } } = await axios.get(`${baseUrl}/destinations?server_id=${serverId}`);
|
|
||||||
return destinations;
|
|
||||||
}
|
|
||||||
async function getProjects() {
|
|
||||||
const { data: { projects } } = await axios.get(`${baseUrl}/projects`);
|
|
||||||
return projects;
|
|
||||||
}
|
|
||||||
async function getEnvironments(project_uuid) {
|
|
||||||
const { data: { environments } } = await axios.get(`${baseUrl}/environments?project_uuid=${project_uuid}`);
|
|
||||||
return environments;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function redirect() {
|
|
||||||
let targetUrl = new URL(window.location.origin)
|
|
||||||
const selected = sequenceState.value.selected
|
|
||||||
const { main, destination = null, project = null, environment = null, server = null } = selected
|
|
||||||
switch (main) {
|
|
||||||
case 1:
|
|
||||||
targetUrl.pathname = `/project/${project}/${environment}/new`
|
|
||||||
targetUrl.searchParams.append('type', 'public')
|
|
||||||
targetUrl.searchParams.append('destination', destination)
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
targetUrl.pathname = `/project/${project}/${environment}/new`
|
|
||||||
targetUrl.searchParams.append('type', 'private-gh-app')
|
|
||||||
targetUrl.searchParams.append('destination', destination)
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
targetUrl.pathname = `/project/${project}/${environment}/new`
|
|
||||||
targetUrl.searchParams.append('type', 'private-deploy-key')
|
|
||||||
targetUrl.searchParams.append('destination', destination)
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
targetUrl.pathname = `/project/${project}/${environment}/new`
|
|
||||||
targetUrl.searchParams.append('type', 'dockerfile')
|
|
||||||
targetUrl.searchParams.append('destination', destination)
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
targetUrl.pathname = `/server/new`
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
targetUrl.pathname = `/source/new`
|
|
||||||
break;
|
|
||||||
case 7:
|
|
||||||
targetUrl.pathname = `/security/private-key/new`
|
|
||||||
break;
|
|
||||||
case 8:
|
|
||||||
targetUrl.pathname = `/destination/new`
|
|
||||||
targetUrl.searchParams.append('server', server)
|
|
||||||
break;
|
|
||||||
case 9:
|
|
||||||
targetUrl.pathname = `/team/new`
|
|
||||||
break;
|
|
||||||
case 10:
|
|
||||||
targetUrl.pathname = `/team/storages/new`
|
|
||||||
break;
|
|
||||||
case 11:
|
|
||||||
targetUrl.pathname = `/team/storages/`
|
|
||||||
break;
|
|
||||||
case 12:
|
|
||||||
targetUrl.pathname = `/`
|
|
||||||
break;
|
|
||||||
case 13:
|
|
||||||
targetUrl.pathname = `/servers`
|
|
||||||
break;
|
|
||||||
case 14:
|
|
||||||
targetUrl.pathname = `/security/private-key`
|
|
||||||
break;
|
|
||||||
case 15:
|
|
||||||
targetUrl.pathname = `/projects`
|
|
||||||
break;
|
|
||||||
case 16:
|
|
||||||
targetUrl.pathname = `/sources`
|
|
||||||
break;
|
|
||||||
case 17:
|
|
||||||
targetUrl.pathname = `/destinations`
|
|
||||||
break;
|
|
||||||
case 18:
|
|
||||||
targetUrl.pathname = `/settings`
|
|
||||||
break;
|
|
||||||
case 19:
|
|
||||||
targetUrl.pathname = `/terminal`
|
|
||||||
break;
|
|
||||||
case 20:
|
|
||||||
targetUrl.pathname = `/team/notifications`
|
|
||||||
break;
|
|
||||||
case 21:
|
|
||||||
targetUrl.pathname = `/profile`
|
|
||||||
break;
|
|
||||||
case 22:
|
|
||||||
targetUrl.pathname = `/team`
|
|
||||||
break;
|
|
||||||
case 23:
|
|
||||||
targetUrl.pathname = `/team`
|
|
||||||
break;
|
|
||||||
case 24:
|
|
||||||
targetUrl.pathname = `/onboarding`
|
|
||||||
break;
|
|
||||||
case 25:
|
|
||||||
targetUrl.pathname = `/security/api-tokens`
|
|
||||||
break;
|
|
||||||
case 26:
|
|
||||||
targetUrl.pathname = `/team/shared-variables`
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
window.location.href = targetUrl;
|
|
||||||
}
|
|
||||||
</script>
|
|
17
resources/views/components/forms/copy-button.blade.php
Normal file
17
resources/views/components/forms/copy-button.blade.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@props(['text'])
|
||||||
|
|
||||||
|
<div class="relative" x-data="{ copied: false }">
|
||||||
|
<input type="text" value="{{ $text }}" readonly class="input">
|
||||||
|
<button
|
||||||
|
@click.prevent="copied = true; navigator.clipboard.writeText('{{ $text }}'); setTimeout(() => copied = false, 1000)"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-300 transition-colors"
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<svg x-show="!copied" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
<svg x-show="copied" class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
@@ -40,7 +40,6 @@
|
|||||||
userConfirmationText: '',
|
userConfirmationText: '',
|
||||||
confirmWithText: @js($confirmWithText && !$disableTwoStepConfirmation),
|
confirmWithText: @js($confirmWithText && !$disableTwoStepConfirmation),
|
||||||
confirmWithPassword: @js($confirmWithPassword && !$disableTwoStepConfirmation),
|
confirmWithPassword: @js($confirmWithPassword && !$disableTwoStepConfirmation),
|
||||||
copied: false,
|
|
||||||
submitAction: @js($submitAction),
|
submitAction: @js($submitAction),
|
||||||
passwordError: '',
|
passwordError: '',
|
||||||
selectedActions: @js(collect($checkboxes)->pluck('id')->filter(fn($id) => $this->$id)->values()->all()),
|
selectedActions: @js(collect($checkboxes)->pluck('id')->filter(fn($id) => $this->$id)->values()->all()),
|
||||||
@@ -91,13 +90,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
copyConfirmationText() {
|
|
||||||
navigator.clipboard.writeText(this.confirmationText);
|
|
||||||
this.copied = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.copied = false;
|
|
||||||
}, 2000);
|
|
||||||
},
|
|
||||||
toggleAction(id) {
|
toggleAction(id) {
|
||||||
const index = this.selectedActions.indexOf(id);
|
const index = this.selectedActions.indexOf(id);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
@@ -255,29 +247,9 @@
|
|||||||
<h4 class="mb-2 text-lg font-semibold">Confirm Actions</h4>
|
<h4 class="mb-2 text-lg font-semibold">Confirm Actions</h4>
|
||||||
<p class="mb-2 text-sm">{{ $confirmationLabel }}</p>
|
<p class="mb-2 text-sm">{{ $confirmationLabel }}</p>
|
||||||
<div class="relative mb-2">
|
<div class="relative mb-2">
|
||||||
<input type="text" x-model="confirmationText"
|
<x-forms.copy-button
|
||||||
class="p-2 pr-10 w-full text-black rounded cursor-text input" readonly>
|
text="{{ $confirmationText }}"
|
||||||
<button @click="copyConfirmationText()"
|
/>
|
||||||
x-show="window.isSecureContext"
|
|
||||||
class="absolute right-2 top-1/2 text-gray-500 transform -translate-y-1/2 hover:text-gray-700"
|
|
||||||
title="Copy confirmation text" x-ref="copyButton">
|
|
||||||
<template x-if="!copied">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5"
|
|
||||||
viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
||||||
<path
|
|
||||||
d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
<template x-if="copied">
|
|
||||||
<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"
|
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label for="userConfirmationText"
|
<label for="userConfirmationText"
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||||
<a wire:navigate class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}"
|
<a wire:navigate class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}"
|
||||||
href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}">General</a>
|
href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}">General</a>
|
||||||
|
@if ($server->isFunctional())
|
||||||
|
<a wire:navigate class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}"
|
||||||
|
href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}">Advanced
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
<a wire:navigate class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}"
|
<a wire:navigate class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}"
|
||||||
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key
|
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key
|
||||||
</a>
|
</a>
|
||||||
@@ -16,9 +21,6 @@
|
|||||||
<a wire:navigate class="menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}"
|
<a wire:navigate class="menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}"
|
||||||
href="{{ route('server.destinations', ['server_uuid' => $server->uuid]) }}">Destinations
|
href="{{ route('server.destinations', ['server_uuid' => $server->uuid]) }}">Destinations
|
||||||
</a>
|
</a>
|
||||||
<a wire:navigate class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}"
|
|
||||||
href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}">Advanced
|
|
||||||
</a>
|
|
||||||
<a wire:navigate class="menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}"
|
<a wire:navigate class="menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}"
|
||||||
href="{{ route('server.log-drains', ['server_uuid' => $server->uuid]) }}">Log
|
href="{{ route('server.log-drains', ['server_uuid' => $server->uuid]) }}">Log
|
||||||
Drains</a>
|
Drains</a>
|
||||||
|
28
resources/views/emails/ssl-certificate-renewed.blade.php
Normal file
28
resources/views/emails/ssl-certificate-renewed.blade.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<x-emails.layout>
|
||||||
|
<h2>SSL Certificates Renewed</h2>
|
||||||
|
|
||||||
|
<p>SSL certificates have been renewed for the following resources:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
@foreach($resources as $resource)
|
||||||
|
<li>{{ $resource->name }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0; padding: 15px; background-color: #fff3cd; border: 1px solid #ffeeba; border-radius: 4px;">
|
||||||
|
<strong>⚠️ Action Required:</strong> These resources need to be redeployed manually for the new SSL certificates to take effect. Please do this in the next few days to ensure your database connections remain accessible.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>The old SSL certificates will remain valid for approximately 14 more days, as we renew certificates 14 days before their expiration.</p>
|
||||||
|
|
||||||
|
@if(isset($urls) && count($urls) > 0)
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<p>You can redeploy these resources here:</p>
|
||||||
|
<ul>
|
||||||
|
@foreach($urls as $name => $url)
|
||||||
|
<li><a href="{{ $url }}">{{ $name }}</a></li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-emails.layout>
|
@@ -45,52 +45,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div x-data="{
|
<div x-data="{
|
||||||
showCode: false,
|
showCode: false,
|
||||||
secretKey: '{{ decrypt(request()->user()->two_factor_secret) }}',
|
|
||||||
otpUrl: '{{ request()->user()->twoFactorQrCodeUrl() }}',
|
|
||||||
copiedSecretKey: false,
|
|
||||||
copiedOtpUrl: false
|
|
||||||
}" class="py-4 w-full">
|
}" class="py-4 w-full">
|
||||||
<div class="flex flex-col gap-2" x-show="showCode">
|
<div class="flex flex-col gap-2 pb-2" x-show="showCode">
|
||||||
<div class="relative">
|
<x-forms.copy-button
|
||||||
<x-forms.input
|
text="{{ decrypt(request()->user()->two_factor_secret) }}"
|
||||||
x-model="secretKey"
|
/>
|
||||||
label="Secret Key"
|
<x-forms.copy-button
|
||||||
readonly
|
text="{{ request()->user()->twoFactorQrCodeUrl() }}"
|
||||||
class="font-mono pr-10"
|
/>
|
||||||
/>
|
|
||||||
<button
|
|
||||||
x-show="window.isSecureContext"
|
|
||||||
@click="navigator.clipboard.writeText(secretKey); copiedSecretKey = true; setTimeout(() => copiedSecretKey = false, 2000)"
|
|
||||||
class="absolute right-2 bottom-1 p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
<svg x-show="!copiedSecretKey" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
|
||||||
</svg>
|
|
||||||
<svg x-show="copiedSecretKey" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-green-500">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="relative" >
|
|
||||||
<x-forms.input
|
|
||||||
x-model="otpUrl"
|
|
||||||
label="OTP URL"
|
|
||||||
readonly
|
|
||||||
class="font-mono pr-10"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
x-show="window.isSecureContext"
|
|
||||||
@click="navigator.clipboard.writeText(otpUrl); copiedOtpUrl = true; setTimeout(() => copiedOtpUrl = false, 2000)"
|
|
||||||
class="absolute right-2 bottom-1 p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
<svg x-show="!copiedOtpUrl" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
|
||||||
</svg>
|
|
||||||
<svg x-show="copiedOtpUrl" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-green-500">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<x-forms.button x-on:click="showCode = !showCode" class="mt-2">
|
<x-forms.button x-on:click="showCode = !showCode" class="mt-2">
|
||||||
<span x-text="showCode ? 'Hide Secret Key and OTP URL' : 'Show Secret Key and OTP URL'"></span>
|
<span x-text="showCode ? 'Hide Secret Key and OTP URL' : 'Show Secret Key and OTP URL'"></span>
|
||||||
|
@@ -49,6 +49,37 @@
|
|||||||
readonly value="Starting the database will generate this." />
|
readonly value="Starting the database will generate this." />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between py-2">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<h3>SSL Configuration</h3>
|
||||||
|
@if ($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||||
|
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||||
|
'The SSL certificate of this database will be regenerated.',
|
||||||
|
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||||
|
]"
|
||||||
|
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if ($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<span class="text-sm">Valid until:
|
||||||
|
@if (now()->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||||
|
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||||
|
soon</span>
|
||||||
|
@else
|
||||||
|
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<x-forms.checkbox id="enable_ssl" label="Enable SSL" wire:model.live="enable_ssl"
|
||||||
|
instantSave="instantSaveSSL" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col py-2 w-64">
|
<div class="flex flex-col py-2 w-64">
|
||||||
<div class="flex items-center gap-2 pb-2">
|
<div class="flex items-center gap-2 pb-2">
|
||||||
|
@@ -49,6 +49,40 @@
|
|||||||
readonly value="Starting the database will generate this." />
|
readonly value="Starting the database will generate this." />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between py-2">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<h3>SSL Configuration</h3>
|
||||||
|
@if($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<x-modal-confirmation
|
||||||
|
title="Regenerate SSL Certificates"
|
||||||
|
buttonTitle="Regenerate SSL Certificates"
|
||||||
|
:actions="[
|
||||||
|
'The SSL certificate of this database will be regenerated.',
|
||||||
|
'You must restart the database after regenerating the certificate to start using the new certificate.'
|
||||||
|
]"
|
||||||
|
submitAction="regenerateSslCertificate"
|
||||||
|
:confirmWithText="false"
|
||||||
|
:confirmWithPassword="false"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<span class="text-sm">Valid until:
|
||||||
|
@if(now()->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||||
|
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon</span>
|
||||||
|
@else
|
||||||
|
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<x-forms.checkbox id="enable_ssl" label="Enable SSL" wire:model.live="enable_ssl" instantSave="instantSaveSSL" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col py-2 w-64">
|
<div class="flex flex-col py-2 w-64">
|
||||||
<div class="flex items-center gap-2 pb-2">
|
<div class="flex items-center gap-2 pb-2">
|
||||||
|
@@ -65,6 +65,41 @@
|
|||||||
type="password" readonly wire:model="db_url_public" />
|
type="password" readonly wire:model="db_url_public" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between py-2">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<h3>SSL Configuration</h3>
|
||||||
|
@if($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<x-modal-confirmation
|
||||||
|
title="Regenerate SSL Certificates"
|
||||||
|
buttonTitle="Regenerate SSL Certificates"
|
||||||
|
:actions="['The SSL certificate of this database will be regenerated.','You must restart the database after regenerating the certificate to start using the new certificate.']"
|
||||||
|
submitAction="regenerateSslCertificate"
|
||||||
|
:confirmWithText="false"
|
||||||
|
:confirmWithPassword="false"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<span class="text-sm">Valid until:
|
||||||
|
@if(now()->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||||
|
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon</span>
|
||||||
|
@else
|
||||||
|
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<x-forms.checkbox id="database.enable_ssl" label="Enable SSL" wire:model.live="database.enable_ssl" instantSave="instantSaveSSL" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col py-2 w-64">
|
<div class="flex flex-col py-2 w-64">
|
||||||
<div class="flex items-center gap-2 pb-2">
|
<div class="flex items-center gap-2 pb-2">
|
||||||
|
@@ -55,6 +55,53 @@
|
|||||||
type="password" readonly wire:model="db_url_public" />
|
type="password" readonly wire:model="db_url_public" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between py-2">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<h3>SSL Configuration</h3>
|
||||||
|
@if ($database->enable_ssl)
|
||||||
|
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||||
|
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||||
|
'The SSL certificate of this database will be regenerated.',
|
||||||
|
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||||
|
]"
|
||||||
|
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if ($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<span class="text-sm">Valid until:
|
||||||
|
@if (now()->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||||
|
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||||
|
soon</span>
|
||||||
|
@else
|
||||||
|
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<x-forms.checkbox id="database.enable_ssl" label="Enable SSL" wire:model.live="database.enable_ssl"
|
||||||
|
instantSave="instantSaveSSL" />
|
||||||
|
@if ($database->enable_ssl)
|
||||||
|
<div class="mx-2">
|
||||||
|
<x-forms.select id="database.ssl_mode" label="SSL Mode" wire:model.live="database.ssl_mode"
|
||||||
|
instantSave="instantSaveSSL"
|
||||||
|
helper="Choose the SSL verification mode for MongoDB connections">
|
||||||
|
<option value="allow" title="Allow insecure connections">allow (insecure)</option>
|
||||||
|
<option value="prefer" title="Prefer secure connections">prefer (secure)</option>
|
||||||
|
<option value="require" title="Require secure connections">require (secure)</option>
|
||||||
|
<option value="verify-full" title="Verify full certificate">verify-full (secure)</option>
|
||||||
|
</x-forms.select>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col py-2 w-64">
|
<div class="flex flex-col py-2 w-64">
|
||||||
<div class="flex items-center gap-2 pb-2">
|
<div class="flex items-center gap-2 pb-2">
|
||||||
|
@@ -65,6 +65,54 @@
|
|||||||
type="password" readonly wire:model="db_url_public" />
|
type="password" readonly wire:model="db_url_public" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between py-2">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<h3>SSL Configuration</h3>
|
||||||
|
@if ($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||||
|
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||||
|
'The SSL certificate of this database will be regenerated.',
|
||||||
|
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||||
|
]"
|
||||||
|
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if ($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<span class="text-sm">Valid until:
|
||||||
|
@if (now()->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||||
|
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||||
|
soon</span>
|
||||||
|
@else
|
||||||
|
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<x-forms.checkbox id="database.enable_ssl" label="Enable SSL" wire:model.live="database.enable_ssl"
|
||||||
|
instantSave="instantSaveSSL" />
|
||||||
|
@if ($database->enable_ssl)
|
||||||
|
<div class="mx-2">
|
||||||
|
<x-forms.select id="database.ssl_mode" label="SSL Mode" wire:model.live="database.ssl_mode"
|
||||||
|
instantSave="instantSaveSSL"
|
||||||
|
helper="Choose the SSL verification mode for MySQL connections">
|
||||||
|
<option value="PREFERRED" title="Prefer secure connections">Prefer (secure)</option>
|
||||||
|
<option value="REQUIRED" title="Require secure connections">Require (secure)</option>
|
||||||
|
<option value="VERIFY_CA" title="Verify CA certificate">Verify CA (secure)</option>
|
||||||
|
<option value="VERIFY_IDENTITY" title="Verify full certificate">Verify Full (secure)
|
||||||
|
</option>
|
||||||
|
</x-forms.select>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col py-2 w-64">
|
<div class="flex flex-col py-2 w-64">
|
||||||
<div class="flex items-center gap-2 pb-2">
|
<div class="flex items-center gap-2 pb-2">
|
||||||
|
@@ -73,58 +73,112 @@
|
|||||||
type="password" readonly wire:model="db_url_public" />
|
type="password" readonly wire:model="db_url_public" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-col py-2 w-64">
|
<div class="flex items-center justify-between py-2">
|
||||||
<div class="flex items-center gap-2 pb-2">
|
<div class="flex items-center justify-between w-full">
|
||||||
<div class="flex items-center">
|
<h3>SSL Configuration</h3>
|
||||||
<h3>Proxy</h3>
|
@if ($database->enable_ssl && $certificateValidUntil)
|
||||||
<x-loading wire:loading wire:target="instantSave" />
|
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||||
</div>
|
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||||
@if (data_get($database, 'is_public'))
|
'The SSL certificate of this database will be regenerated.',
|
||||||
<x-slide-over fullScreen>
|
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||||
<x-slot:title>Proxy Logs</x-slot:title>
|
]"
|
||||||
<x-slot:content>
|
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
|
||||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
|
||||||
</x-slot:content>
|
|
||||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}"
|
|
||||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
|
||||||
</x-slide-over>
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<x-forms.checkbox instantSave id="database.is_public" label="Make it publicly available" />
|
|
||||||
</div>
|
</div>
|
||||||
<x-forms.input placeholder="5432" disabled="{{ data_get($database, 'is_public') }}"
|
@if ($database->enable_ssl && $certificateValidUntil)
|
||||||
id="database.public_port" label="Public Port" />
|
<span class="text-sm">Valid until:
|
||||||
</div>
|
@if (now()->gt($certificateValidUntil))
|
||||||
<x-forms.textarea label="Custom PostgreSQL Configuration" rows="10" id="database.postgres_conf" />
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||||
</form>
|
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||||
<h3 class="pt-4">Advanced</h3>
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||||
<div class="flex flex-col">
|
soon</span>
|
||||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
@else
|
||||||
instantSave="instantSaveAdvanced" id="database.is_log_drain_enabled" label="Drain Logs" />
|
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||||
</div>
|
@endif
|
||||||
<div class="pb-16">
|
</span>
|
||||||
<div class="flex gap-2 pt-4 pb-2">
|
@endif
|
||||||
<h3>Initialization scripts</h3>
|
|
||||||
<x-modal-input buttonTitle="+ Add" title="New Init Script">
|
|
||||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='save_new_init_script'>
|
|
||||||
<x-forms.input placeholder="create_test_db.sql" id="new_filename" label="Filename" required />
|
|
||||||
<x-forms.textarea rows="20" placeholder="CREATE DATABASE test;" id="new_content"
|
|
||||||
label="Content" required />
|
|
||||||
<x-forms.button type="submit">
|
|
||||||
Save
|
|
||||||
</x-forms.button>
|
|
||||||
</form>
|
|
||||||
</x-modal-input>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@forelse(data_get($database,'init_scripts', []) as $script)
|
<div class="flex flex-col gap-2">
|
||||||
<livewire:project.database.init-script :script="$script" :wire:key="$script['index']" />
|
<x-forms.checkbox id="database.enable_ssl" label="Enable SSL" wire:model.live="database.enable_ssl"
|
||||||
@empty
|
instantSave="instantSaveSSL" />
|
||||||
<div>No initialization scripts found.</div>
|
@if ($database->enable_ssl)
|
||||||
@endforelse
|
<div class="mx-2">
|
||||||
|
<x-forms.select id="database.ssl_mode" label="SSL Mode" wire:model.live="database.ssl_mode"
|
||||||
|
instantSave="instantSaveSSL"
|
||||||
|
helper="Choose the SSL verification mode for PostgreSQL connections">
|
||||||
|
<option value="allow" title="Allow insecure connections">allow (insecure)</option>
|
||||||
|
<option value="prefer" title="Prefer secure connections">prefer (secure)</option>
|
||||||
|
<option value="require" title="Require secure connections">require (secure)</option>
|
||||||
|
<option value="verify-ca" title="Verify CA certificate">verify-ca (secure)</option>
|
||||||
|
<option value="verify-full" title="Verify full certificate">verify-full (secure)</option>
|
||||||
|
</x-forms.select>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between py-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h3>Proxy</h3>
|
||||||
|
<x-loading wire:loading wire:target="instantSave" />
|
||||||
|
</div>
|
||||||
|
@if (data_get($database, 'is_public'))
|
||||||
|
<x-slide-over fullScreen>
|
||||||
|
<x-slot:title>Proxy Logs</x-slot:title>
|
||||||
|
<x-slot:content>
|
||||||
|
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||||
|
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||||
|
</x-slot:content>
|
||||||
|
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}"
|
||||||
|
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||||
|
</x-slide-over>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<x-forms.checkbox instantSave id="database.is_public" label="Make it publicly available" />
|
||||||
|
<x-forms.input placeholder="5432" disabled="{{ data_get($database, 'is_public') }}"
|
||||||
|
id="database.public_port" label="Public Port" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<x-forms.textarea label="Custom PostgreSQL Configuration" rows="10" id="database.postgres_conf" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 pt-4">
|
||||||
|
<h3>Advanced</h3>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||||
|
instantSave="instantSaveAdvanced" id="database.is_log_drain_enabled" label="Drain Logs" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pb-16">
|
||||||
|
<div class="flex items-center gap-2 pb-2">
|
||||||
|
<h3>Initialization scripts</h3>
|
||||||
|
<x-modal-input buttonTitle="+ Add" title="New Init Script">
|
||||||
|
<form class="flex flex-col w-full gap-2 rounded" wire:submit='save_new_init_script'>
|
||||||
|
<x-forms.input placeholder="create_test_db.sql" id="new_filename" label="Filename"
|
||||||
|
required />
|
||||||
|
<x-forms.textarea rows="20" placeholder="CREATE DATABASE test;" id="new_content"
|
||||||
|
label="Content" required />
|
||||||
|
<x-forms.button type="submit">
|
||||||
|
Save
|
||||||
|
</x-forms.button>
|
||||||
|
</form>
|
||||||
|
</x-modal-input>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
@forelse(data_get($database,'init_scripts', []) as $script)
|
||||||
|
<livewire:project.database.init-script :script="$script" :wire:key="$script['index']" />
|
||||||
|
@empty
|
||||||
|
<div>No initialization scripts found.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -49,6 +49,40 @@
|
|||||||
type="password" readonly wire:model="db_url_public" />
|
type="password" readonly wire:model="db_url_public" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between py-2">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<h3>SSL Configuration</h3>
|
||||||
|
@if($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<x-modal-confirmation
|
||||||
|
title="Regenerate SSL Certificates"
|
||||||
|
buttonTitle="Regenerate SSL Certificates"
|
||||||
|
:actions="[
|
||||||
|
'The SSL certificate of this database will be regenerated.',
|
||||||
|
'You must restart the database after regenerating the certificate to start using the new certificate.'
|
||||||
|
]"
|
||||||
|
submitAction="regenerateSslCertificate"
|
||||||
|
:confirmWithText="false"
|
||||||
|
:confirmWithPassword="false"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if($database->enable_ssl && $certificateValidUntil)
|
||||||
|
<span class="text-sm">Valid until:
|
||||||
|
@if(now()->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||||
|
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon</span>
|
||||||
|
@else
|
||||||
|
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<x-forms.checkbox id="database.enable_ssl" label="Enable SSL" wire:model.live="database.enable_ssl" instantSave="instantSaveSSL" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col py-2 w-64">
|
<div class="flex flex-col py-2 w-64">
|
||||||
<div class="flex items-center gap-2 pb-2">
|
<div class="flex items-center gap-2 pb-2">
|
||||||
|
@@ -38,6 +38,95 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 pt-8">
|
||||||
|
<h3>CA SSL Certificate</h3>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<x-modal-confirmation
|
||||||
|
title="Confirm changing of CA Certificate?"
|
||||||
|
buttonTitle="Save Certificate"
|
||||||
|
submitAction="saveCaCertificate"
|
||||||
|
:actions="[
|
||||||
|
'This will overwrite the existing CA certificate at /data/coolify/ssl/coolify-ca.crt with your custom CA certificate.',
|
||||||
|
'This will regenerate all SSL certificates for databases on this server and it will sign them with your custom CA.',
|
||||||
|
'You must manually redeploy all your databases on this server so that they use the new SSL certificates singned with your new CA certificate.',
|
||||||
|
'Because of caching, you probably also need to redeploy all your resources on this server that are using this CA certificate.'
|
||||||
|
]"
|
||||||
|
confirmationText="/data/coolify/ssl/coolify-ca.crt"
|
||||||
|
shortConfirmationLabel="CA Certificate Path"
|
||||||
|
step3ButtonText="Save Certificate">
|
||||||
|
</x-modal-confirmation>
|
||||||
|
<x-modal-confirmation
|
||||||
|
title="Confirm Regenerate Certificate?"
|
||||||
|
buttonTitle="Regenerate Certificate"
|
||||||
|
submitAction="regenerateCaCertificate"
|
||||||
|
:actions="[
|
||||||
|
'This will generate a new CA certificate at /data/coolify/ssl/coolify-ca.crt and replace the existing one.',
|
||||||
|
'This will regenerate all SSL certificates for databases on this server and it will sign them with the new CA certificate.',
|
||||||
|
'You must manually redeploy all your databases on this server so that they use the new SSL certificates singned with the new CA certificate.',
|
||||||
|
'Because of caching, you probably also need to redeploy all your resources on this server that are using this CA certificate.'
|
||||||
|
]"
|
||||||
|
confirmationText="/data/coolify/ssl/coolify-ca.crt"
|
||||||
|
shortConfirmationLabel="CA Certificate Path"
|
||||||
|
step3ButtonText="Regenerate Certificate">
|
||||||
|
</x-modal-confirmation>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p class="font-medium mb-2">Recommended Configuration:</p>
|
||||||
|
<ul class="list-disc pl-5 space-y-1">
|
||||||
|
<li>Mount this CA certificate of Coolify into all containers that need to connect to one of your databases over SSL. You can see and copy the bind mount below.</li>
|
||||||
|
<li>Read more when and why this is needed <a class="underline" href="https://coolify.io/docs/databases/ssl" target="_blank">here</a>.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<x-forms.copy-button
|
||||||
|
text="- /data/coolify/ssl/coolify-ca.crt:/etc/ssl/certs/coolify-ca.crt:ro"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm">CA Certificate</span>
|
||||||
|
@if($certificateValidUntil)
|
||||||
|
<span class="text-sm">(Valid until:
|
||||||
|
@if(now()->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired)</span>
|
||||||
|
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon)</span>
|
||||||
|
@else
|
||||||
|
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }})</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<x-forms.button
|
||||||
|
wire:click="toggleCertificate"
|
||||||
|
type="button"
|
||||||
|
class="!py-1 !px-2 text-sm">
|
||||||
|
{{ $showCertificate ? 'Hide' : 'Show' }}
|
||||||
|
</x-forms.button>
|
||||||
|
</div>
|
||||||
|
@if($showCertificate)
|
||||||
|
<textarea
|
||||||
|
class="w-full h-[370px] input"
|
||||||
|
wire:model="certificateContent"
|
||||||
|
placeholder="Paste or edit CA certificate content here..."></textarea>
|
||||||
|
@else
|
||||||
|
<div class="w-full h-[370px] input">
|
||||||
|
<div class="h-full flex flex-col items-center justify-center text-gray-300">
|
||||||
|
<div class="mb-2">
|
||||||
|
━━━━━━━━ CERTIFICATE CONTENT ━━━━━━━━
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
Click "Show" to view or edit
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -209,7 +209,7 @@ if [ "$WARNING_SPACE" = true ]; then
|
|||||||
sleep 5
|
sleep 5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
|
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,ssl,webhooks-during-maintenance,sentinel}
|
||||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||||
mkdir -p /data/coolify/proxy/dynamic
|
mkdir -p /data/coolify/proxy/dynamic
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user