Merge branch 'next' into docker-network-aliases
This commit is contained in:
@@ -14,3 +14,5 @@ PUSHER_APP_SECRET=
|
||||
ROOT_USERNAME=
|
||||
ROOT_USER_EMAIL=
|
||||
ROOT_USER_PASSWORD=
|
||||
|
||||
REGISTRY_URL=ghcr.io
|
||||
|
5235
CHANGELOG.md
5235
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -136,6 +136,7 @@ After installing Docker (or Orbstack) and Spin, verify the installation:
|
||||
- Password: `password`
|
||||
|
||||
2. Additional development tools:
|
||||
|
||||
| Tool | URL | Note |
|
||||
|------|-----|------|
|
||||
| Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user |
|
||||
@@ -237,9 +238,9 @@ After completing these steps, you'll have a fresh development setup.
|
||||
### Contributing a New Service
|
||||
|
||||
To add a new service to Coolify, please refer to our documentation:
|
||||
[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service)
|
||||
[Adding a New Service](https://coolify.io/docs/get-started/contribute/service)
|
||||
|
||||
### Contributing to Documentation
|
||||
|
||||
To contribute to the Coolify documentation, please refer to this guide:
|
||||
[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md)
|
||||
[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/readme.md)
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -16,24 +18,81 @@ class StartDragonfly
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneDragonfly $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"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();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$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_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$startCommand = $this->buildStartCommand();
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
@@ -70,27 +129,55 @@ class StartDragonfly
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($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()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
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) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$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 (count($volume_names) > 0) {
|
||||
$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
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
@@ -102,12 +189,32 @@ class StartDragonfly
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$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[] = "echo 'Database started.'";
|
||||
|
||||
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()
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -17,26 +19,84 @@ class StartKeydb
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneKeydb $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"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();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$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_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->add_custom_keydb();
|
||||
|
||||
$startCommand = $this->buildStartCommand();
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
$container_name => [
|
||||
@@ -72,34 +132,67 @@ class StartKeydb
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($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()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
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) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$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 (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
|
||||
if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/keydb.conf',
|
||||
'target' => '/etc/keydb/keydb.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[
|
||||
[
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/keydb.conf',
|
||||
'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
|
||||
@@ -112,6 +205,9 @@ class StartKeydb
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$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[] = "echo 'Database started.'";
|
||||
|
||||
@@ -177,4 +273,36 @@ class StartKeydb
|
||||
instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
|
||||
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;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -16,6 +18,8 @@ class StartMariadb
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneMariadb $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
@@ -25,9 +29,64 @@ class StartMariadb
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"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();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$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_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
@@ -67,38 +126,81 @@ class StartMariadb
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($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()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$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) {
|
||||
$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)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
[
|
||||
[
|
||||
'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
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$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'] = [
|
||||
'mariadbd',
|
||||
'--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_base64 = base64_encode($docker_compose);
|
||||
@@ -109,6 +211,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 up -d";
|
||||
$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');
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -16,6 +18,8 @@ class StartMongodb
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneMongodb $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
@@ -24,16 +28,69 @@ class StartMongodb
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
if (isDev()) {
|
||||
$this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
|
||||
}
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"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();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
$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_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
@@ -79,47 +136,119 @@ class StartMongodb
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($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()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
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) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$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 (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/mongod.conf',
|
||||
'target' => '/etc/mongo/mongod.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
|
||||
|
||||
if (! empty($this->database->mongo_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[[
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/mongod.conf',
|
||||
'target' => '/etc/mongo/mongod.conf',
|
||||
'read_only' => true,
|
||||
]]
|
||||
);
|
||||
$docker_compose['services'][$container_name]['command'] = ['mongod', '--config', '/etc/mongo/mongod.conf'];
|
||||
}
|
||||
|
||||
$this->add_default_database();
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
|
||||
'target' => '/docker-entrypoint-initdb.d',
|
||||
'read_only' => true,
|
||||
];
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[[
|
||||
'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
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$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_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
@@ -128,6 +257,9 @@ class StartMongodb
|
||||
$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 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.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMysql;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -16,6 +18,8 @@ class StartMysql
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneMysql $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
@@ -25,9 +29,64 @@ class StartMysql
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"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();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$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_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
@@ -67,39 +126,83 @@ class StartMysql
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($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()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
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) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$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 (count($volume_names) > 0) {
|
||||
$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)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'] ?? [],
|
||||
[
|
||||
[
|
||||
'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
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$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_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
@@ -108,6 +211,11 @@ class StartMysql
|
||||
$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 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.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -18,6 +20,8 @@ class StartPostgresql
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandalonePostgresql $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
@@ -29,10 +33,65 @@ class StartPostgresql
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
"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();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$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_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
@@ -77,49 +136,84 @@ class StartPostgresql
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (filled($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()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
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) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$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 (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
|
||||
if (count($this->init_scripts) > 0) {
|
||||
foreach ($this->init_scripts as $init_script) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $init_script,
|
||||
'target' => '/docker-entrypoint-initdb.d/'.basename($init_script),
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
[[
|
||||
'type' => 'bind',
|
||||
'source' => $init_script,
|
||||
'target' => '/docker-entrypoint-initdb.d/'.basename($init_script),
|
||||
'read_only' => true,
|
||||
]]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (filled($this->database->postgres_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/custom-postgres.conf',
|
||||
'target' => '/etc/postgresql/postgresql.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['volumes'] = array_merge(
|
||||
$docker_compose['services'][$container_name]['volumes'],
|
||||
[[
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/custom-postgres.conf',
|
||||
'target' => '/etc/postgresql/postgresql.conf',
|
||||
'read_only' => true,
|
||||
]]
|
||||
);
|
||||
$docker_compose['services'][$container_name]['command'] = [
|
||||
'postgres',
|
||||
'-c',
|
||||
'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
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
@@ -132,6 +226,9 @@ class StartPostgresql
|
||||
$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 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.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -17,6 +19,8 @@ class StartRedis
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
private ?SslCertificate $ssl_certificate = null;
|
||||
|
||||
public function handle(StandaloneRedis $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
@@ -26,9 +30,62 @@ class StartRedis
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting database.'",
|
||||
"echo 'Creating directories.'",
|
||||
"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();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$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_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
@@ -76,26 +133,55 @@ class StartRedis
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! is_null($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()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
||||
}
|
||||
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$container_name]['volumes'] ??= [];
|
||||
|
||||
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) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
$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 (count($volume_names) > 0) {
|
||||
$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)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
@@ -116,6 +202,9 @@ class StartRedis
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$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[] = "echo 'Database started.'";
|
||||
|
||||
@@ -202,6 +291,20 @@ class StartRedis
|
||||
$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;
|
||||
}
|
||||
|
||||
|
@@ -26,7 +26,7 @@ class StopDatabase
|
||||
}
|
||||
|
||||
$this->stopContainer($database, $database->uuid, 300);
|
||||
if (! $isDeleteOperation) {
|
||||
if ($isDeleteOperation) {
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
|
@@ -24,5 +24,6 @@ class ResetUserPassword implements ResetsUserPasswords
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
$user->deleteAllSessions();
|
||||
}
|
||||
}
|
||||
|
@@ -27,13 +27,9 @@ class CheckProxy
|
||||
return false;
|
||||
}
|
||||
$proxyType = $server->proxyType();
|
||||
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
|
||||
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) {
|
||||
return false;
|
||||
}
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
|
||||
if (! $uptime) {
|
||||
throw new \Exception($error);
|
||||
}
|
||||
if (! $server->isProxyShouldRun()) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception('Proxy should not run. You selected the Custom Proxy.');
|
||||
@@ -41,8 +37,12 @@ class CheckProxy
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine proxy container name based on environment
|
||||
$proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
|
||||
if ($server->isSwarm()) {
|
||||
$status = getContainerStatus($server, 'coolify-proxy_traefik');
|
||||
$status = getContainerStatus($server, $proxyContainerName);
|
||||
$server->proxy->set('status', $status);
|
||||
$server->save();
|
||||
if ($status === 'running') {
|
||||
@@ -51,7 +51,7 @@ class CheckProxy
|
||||
|
||||
return true;
|
||||
} else {
|
||||
$status = getContainerStatus($server, 'coolify-proxy');
|
||||
$status = getContainerStatus($server, $proxyContainerName);
|
||||
if ($status === 'running') {
|
||||
$server->proxy->set('status', 'running');
|
||||
$server->save();
|
||||
@@ -65,9 +65,18 @@ class CheckProxy
|
||||
if ($server->id === 0) {
|
||||
$ip = 'host.docker.internal';
|
||||
}
|
||||
|
||||
$portsToCheck = ['80', '443'];
|
||||
|
||||
foreach ($portsToCheck as $port) {
|
||||
// Use the smart port checker that handles dual-stack properly
|
||||
if ($this->isPortConflict($server, $port, $proxyContainerName)) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br><br>Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a>");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
if ($server->proxyType() !== ProxyTypes::NONE->value) {
|
||||
$proxyCompose = CheckConfiguration::run($server);
|
||||
@@ -94,18 +103,148 @@ class CheckProxy
|
||||
if (count($portsToCheck) === 0) {
|
||||
return false;
|
||||
}
|
||||
foreach ($portsToCheck as $port) {
|
||||
$connection = @fsockopen($ip, $port);
|
||||
if (is_resource($connection) && fclose($connection)) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart port checker that handles dual-stack configurations
|
||||
* Returns true only if there's a real port conflict (not just dual-stack)
|
||||
*/
|
||||
private function isPortConflict(Server $server, string $port, string $proxyContainerName): bool
|
||||
{
|
||||
// First check if our own proxy is using this port (which is fine)
|
||||
try {
|
||||
$getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
|
||||
$containerId = trim(instant_remote_process([$getProxyContainerId], $server));
|
||||
|
||||
if (! empty($containerId)) {
|
||||
$checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'";
|
||||
try {
|
||||
instant_remote_process([$checkProxyPort], $server);
|
||||
|
||||
// Our proxy is using the port, which is fine
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
// Our container exists but not using this port
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Container not found or error checking, continue with regular checks
|
||||
}
|
||||
|
||||
// Command sets for different ways to check ports, ordered by preference
|
||||
$commandSets = [
|
||||
// Set 1: Use ss to check listener counts by protocol stack
|
||||
[
|
||||
'available' => 'command -v ss >/dev/null 2>&1',
|
||||
'check' => [
|
||||
// Get listening process details
|
||||
"ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"",
|
||||
// Count IPv4 listeners
|
||||
"echo \"\$ss_output\" | grep -c ':$port '",
|
||||
],
|
||||
],
|
||||
// Set 2: Use netstat as alternative to ss
|
||||
[
|
||||
'available' => 'command -v netstat >/dev/null 2>&1',
|
||||
'check' => [
|
||||
// Get listening process details
|
||||
"netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '",
|
||||
// Count listeners
|
||||
"echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'",
|
||||
],
|
||||
],
|
||||
// Set 3: Use lsof as last resort
|
||||
[
|
||||
'available' => 'command -v lsof >/dev/null 2>&1',
|
||||
'check' => [
|
||||
// Get process using the port
|
||||
"lsof -i :$port -P -n | grep 'LISTEN'",
|
||||
// Count listeners
|
||||
"lsof -i :$port -P -n | grep 'LISTEN' | wc -l",
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Try each command set until we find one available
|
||||
foreach ($commandSets as $set) {
|
||||
try {
|
||||
// Check if the command is available
|
||||
instant_remote_process([$set['available']], $server);
|
||||
|
||||
// Run the actual check commands
|
||||
$output = instant_remote_process($set['check'], $server, true);
|
||||
|
||||
// Parse the output lines
|
||||
$lines = explode("\n", trim($output));
|
||||
|
||||
// Get the detailed output and listener count
|
||||
$details = trim($lines[0] ?? '');
|
||||
$count = intval(trim($lines[1] ?? '0'));
|
||||
|
||||
// If no listeners or empty result, port is free
|
||||
if ($count == 0 || empty($details)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to detect if this is our coolify-proxy
|
||||
if (strpos($details, 'docker') !== false || strpos($details, $proxyContainerName) !== false) {
|
||||
// It's likely our docker or proxy, which is fine
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6)
|
||||
// If exactly 2 listeners and both have same port, likely dual-stack
|
||||
if ($count <= 2) {
|
||||
// Check if it looks like a standard dual-stack setup
|
||||
$isDualStack = false;
|
||||
|
||||
// Look for IPv4 and IPv6 in the listing (ss output format)
|
||||
if (preg_match('/LISTEN.*:'.$port.'\s/', $details) &&
|
||||
(preg_match('/\*:'.$port.'\s/', $details) ||
|
||||
preg_match('/:::'.$port.'\s/', $details))) {
|
||||
$isDualStack = true;
|
||||
}
|
||||
|
||||
// For netstat format
|
||||
if (strpos($details, '0.0.0.0:'.$port) !== false &&
|
||||
strpos($details, ':::'.$port) !== false) {
|
||||
$isDualStack = true;
|
||||
}
|
||||
|
||||
// For lsof format (IPv4 and IPv6)
|
||||
if (strpos($details, '*:'.$port) !== false &&
|
||||
preg_match('/\*:'.$port.'.*IPv4/', $details) &&
|
||||
preg_match('/\*:'.$port.'.*IPv6/', $details)) {
|
||||
$isDualStack = true;
|
||||
}
|
||||
|
||||
if ($isDualStack) {
|
||||
return false; // This is just a normal dual-stack setup
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, it's likely a real port conflict
|
||||
return true;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// This command set failed, try the next one
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to simpler check if all above methods fail
|
||||
try {
|
||||
// Just try to bind to the port directly to see if it's available
|
||||
$checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'";
|
||||
$result = instant_remote_process([$checkCommand], $server, true);
|
||||
|
||||
return trim($result) === 'in-use';
|
||||
} catch (\Throwable $e) {
|
||||
// If everything fails, assume the port is free to avoid false positives
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
56
app/Actions/Proxy/StopProxy.php
Normal file
56
app/Actions/Proxy/StopProxy.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, bool $forceStop = true)
|
||||
{
|
||||
try {
|
||||
$containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
$timeout = 30;
|
||||
|
||||
$process = $this->stopContainer($containerName, $timeout);
|
||||
|
||||
$startTime = Carbon::now()->getTimestamp();
|
||||
while ($process->running()) {
|
||||
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
|
||||
$this->forceStopContainer($containerName, $server);
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
$this->removeContainer($containerName, $server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
} finally {
|
||||
$server->proxy->force_stop = $forceStop;
|
||||
$server->proxy->status = 'exited';
|
||||
$server->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||
{
|
||||
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
}
|
||||
|
||||
private function forceStopContainer(string $containerName, Server $server)
|
||||
{
|
||||
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
|
||||
}
|
||||
|
||||
private function removeContainer(string $containerName, Server $server)
|
||||
{
|
||||
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
|
||||
}
|
||||
}
|
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -17,6 +19,27 @@ class InstallDocker
|
||||
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>.');
|
||||
}
|
||||
|
||||
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('{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
|
@@ -25,7 +25,7 @@ class StartSentinel
|
||||
$endpoint = data_get($server, 'settings.sentinel_custom_url');
|
||||
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
|
||||
$mountDir = '/data/coolify/sentinel';
|
||||
$image = "ghcr.io/coollabsio/sentinel:$version";
|
||||
$image = config('constants.coolify.registry_url').'/coollabsio/sentinel:'.$version;
|
||||
if (! $endpoint) {
|
||||
throw new \Exception('You should set FQDN in Instance Settings.');
|
||||
}
|
||||
|
@@ -52,7 +52,8 @@ class UpdateCoolify
|
||||
{
|
||||
PullHelperImageJob::dispatch($this->server);
|
||||
|
||||
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
|
||||
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
|
||||
instant_remote_process(["docker pull -q $image"], $this->server, false);
|
||||
|
||||
remote_process([
|
||||
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
|
||||
|
@@ -23,7 +23,7 @@ class StopService
|
||||
$containersToStop = $service->getContainersToStop();
|
||||
$service->stopContainers($containersToStop, $server);
|
||||
|
||||
if (! $isDeleteOperation) {
|
||||
if ($isDeleteOperation) {
|
||||
$service->delete_connected_networks($service->uuid);
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
|
@@ -5,12 +5,10 @@ namespace App\Console\Commands;
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Dev extends Command
|
||||
{
|
||||
protected $signature = 'dev {--init} {--generate-openapi}';
|
||||
protected $signature = 'dev {--init}';
|
||||
|
||||
protected $description = 'Helper commands for development.';
|
||||
|
||||
@@ -21,36 +19,6 @@ class Dev extends Command
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->option('generate-openapi')) {
|
||||
$this->generateOpenApi();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function generateOpenApi()
|
||||
{
|
||||
// Generate OpenAPI documentation
|
||||
echo "Generating OpenAPI documentation.\n";
|
||||
// https://github.com/OAI/OpenAPI-Specification/releases
|
||||
$process = Process::run([
|
||||
'/var/www/html/vendor/bin/openapi',
|
||||
'app',
|
||||
'-o',
|
||||
'openapi.yaml',
|
||||
'--version',
|
||||
'3.1.0',
|
||||
]);
|
||||
$error = $process->errorOutput();
|
||||
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
|
||||
$error = preg_replace('/^\h*\v+/m', '', $error);
|
||||
echo $error;
|
||||
echo $process->output();
|
||||
// Convert YAML to JSON
|
||||
$yaml = file_get_contents('openapi.yaml');
|
||||
$json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
|
||||
file_put_contents('openapi.json', $json);
|
||||
echo "Converted OpenAPI YAML to JSON.\n";
|
||||
}
|
||||
|
||||
public function init()
|
||||
|
@@ -4,6 +4,7 @@ namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class OpenApi extends Command
|
||||
{
|
||||
@@ -29,5 +30,10 @@ class OpenApi extends Command
|
||||
$error = preg_replace('/^\h*\v+/m', '', $error);
|
||||
echo $error;
|
||||
echo $process->output();
|
||||
|
||||
$yaml = file_get_contents('openapi.yaml');
|
||||
$json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
|
||||
file_put_contents('openapi.json', $json);
|
||||
echo "Converted OpenAPI YAML to JSON.\n";
|
||||
}
|
||||
}
|
||||
|
@@ -39,7 +39,13 @@ class RootResetPassword extends Command
|
||||
}
|
||||
$this->info('Updating root password...');
|
||||
try {
|
||||
User::find(0)->update(['password' => Hash::make($password)]);
|
||||
$user = User::find(0);
|
||||
if (! $user) {
|
||||
$this->error('Root user not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
$user->update(['password' => Hash::make($password)]);
|
||||
$this->info('Root password updated successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Failed to update root password.');
|
||||
|
@@ -9,6 +9,7 @@ use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\RegenerateSslCertJob;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\ServerCheckJob;
|
||||
use App\Jobs\ServerStorageCheckJob;
|
||||
@@ -83,6 +84,8 @@ class Kernel extends ConsoleKernel
|
||||
$this->checkScheduledBackups();
|
||||
$this->checkScheduledTasks();
|
||||
|
||||
$this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily();
|
||||
|
||||
$this->scheduleInstance->command('cleanup:database --yes')->daily();
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -932,10 +932,31 @@ class ApplicationsController extends Controller
|
||||
if (! $githubApp) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
}
|
||||
$token = generateGithubInstallationToken($githubApp);
|
||||
if (! $token) {
|
||||
return response()->json(['message' => 'Failed to generate Github App token.'], 400);
|
||||
}
|
||||
|
||||
$repositories = collect();
|
||||
$page = 1;
|
||||
$repositories = loadRepositoryByPage($githubApp, $token, $page);
|
||||
if ($repositories['total_count'] > 0) {
|
||||
while (count($repositories['repositories']) < $repositories['total_count']) {
|
||||
$page++;
|
||||
$repositories = loadRepositoryByPage($githubApp, $token, $page);
|
||||
}
|
||||
}
|
||||
|
||||
$gitRepository = $request->git_repository;
|
||||
if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) {
|
||||
$gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', '');
|
||||
}
|
||||
$gitRepositoryFound = collect($repositories['repositories'])->firstWhere('full_name', $gitRepository);
|
||||
if (! $gitRepositoryFound) {
|
||||
return response()->json(['message' => 'Repository not found.'], 404);
|
||||
}
|
||||
$repository_project_id = data_get($gitRepositoryFound, 'id');
|
||||
|
||||
$application = new Application;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
|
||||
@@ -966,6 +987,8 @@ class ApplicationsController extends Controller
|
||||
$application->environment_id = $environment->id;
|
||||
$application->source_type = $githubApp->getMorphClass();
|
||||
$application->source_id = $githubApp->id;
|
||||
$application->repository_project_id = $repository_project_id;
|
||||
|
||||
$application->save();
|
||||
$application->refresh();
|
||||
if (isset($useBuildServer)) {
|
||||
@@ -1310,7 +1333,6 @@ class ApplicationsController extends Controller
|
||||
$service->destination_type = $destination->getMorphClass();
|
||||
$service->save();
|
||||
|
||||
$service->name = "service-$service->uuid";
|
||||
$service->parse(isNew: true);
|
||||
if ($instantDeploy) {
|
||||
StartService::dispatch($service);
|
||||
@@ -2859,198 +2881,198 @@ class ApplicationsController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Execute Command',
|
||||
description: "Execute a command on the application's current container.",
|
||||
path: '/applications/{uuid}/execute',
|
||||
operationId: 'execute-command-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
description: 'Command to execute.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: "Execute a command on the application's current container.",
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Command executed.'],
|
||||
'response' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function execute_command_by_uuid(Request $request)
|
||||
{
|
||||
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
|
||||
$allowedFields = ['command'];
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$uuid = $request->route('uuid');
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 400);
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'command' => 'string|required',
|
||||
]);
|
||||
// #[OA\Post(
|
||||
// summary: 'Execute Command',
|
||||
// description: "Execute a command on the application's current container.",
|
||||
// path: '/applications/{uuid}/execute',
|
||||
// operationId: 'execute-command-application',
|
||||
// security: [
|
||||
// ['bearerAuth' => []],
|
||||
// ],
|
||||
// tags: ['Applications'],
|
||||
// parameters: [
|
||||
// new OA\Parameter(
|
||||
// name: 'uuid',
|
||||
// in: 'path',
|
||||
// description: 'UUID of the application.',
|
||||
// required: true,
|
||||
// schema: new OA\Schema(
|
||||
// type: 'string',
|
||||
// format: 'uuid',
|
||||
// )
|
||||
// ),
|
||||
// ],
|
||||
// requestBody: new OA\RequestBody(
|
||||
// required: true,
|
||||
// description: 'Command to execute.',
|
||||
// content: new OA\MediaType(
|
||||
// mediaType: 'application/json',
|
||||
// schema: new OA\Schema(
|
||||
// type: 'object',
|
||||
// properties: [
|
||||
// 'command' => ['type' => 'string', 'description' => 'Command to execute.'],
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// responses: [
|
||||
// new OA\Response(
|
||||
// response: 200,
|
||||
// description: "Execute a command on the application's current container.",
|
||||
// content: [
|
||||
// new OA\MediaType(
|
||||
// mediaType: 'application/json',
|
||||
// schema: new OA\Schema(
|
||||
// type: 'object',
|
||||
// properties: [
|
||||
// 'message' => ['type' => 'string', 'example' => 'Command executed.'],
|
||||
// 'response' => ['type' => 'string'],
|
||||
// ]
|
||||
// )
|
||||
// ),
|
||||
// ]
|
||||
// ),
|
||||
// new OA\Response(
|
||||
// response: 401,
|
||||
// ref: '#/components/responses/401',
|
||||
// ),
|
||||
// new OA\Response(
|
||||
// response: 400,
|
||||
// ref: '#/components/responses/400',
|
||||
// ),
|
||||
// new OA\Response(
|
||||
// response: 404,
|
||||
// ref: '#/components/responses/404',
|
||||
// ),
|
||||
// ]
|
||||
// )]
|
||||
// public function execute_command_by_uuid(Request $request)
|
||||
// {
|
||||
// // TODO: Need to review this from security perspective, to not allow arbitrary command execution
|
||||
// $allowedFields = ['command'];
|
||||
// $teamId = getTeamIdFromToken();
|
||||
// if (is_null($teamId)) {
|
||||
// return invalidTokenResponse();
|
||||
// }
|
||||
// $uuid = $request->route('uuid');
|
||||
// if (! $uuid) {
|
||||
// return response()->json(['message' => 'UUID is required.'], 400);
|
||||
// }
|
||||
// $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
// if (! $application) {
|
||||
// return response()->json(['message' => 'Application not found.'], 404);
|
||||
// }
|
||||
// $return = validateIncomingRequest($request);
|
||||
// if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
// return $return;
|
||||
// }
|
||||
// $validator = customApiValidator($request->all(), [
|
||||
// 'command' => 'string|required',
|
||||
// ]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
// $extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
// if ($validator->fails() || ! empty($extraFields)) {
|
||||
// $errors = $validator->errors();
|
||||
// if (! empty($extraFields)) {
|
||||
// foreach ($extraFields as $field) {
|
||||
// $errors->add($field, 'This field is not allowed.');
|
||||
// }
|
||||
// }
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
// return response()->json([
|
||||
// 'message' => 'Validation failed.',
|
||||
// 'errors' => $errors,
|
||||
// ], 422);
|
||||
// }
|
||||
|
||||
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
|
||||
$status = getContainerStatus($application->destination->server, $container['Names']);
|
||||
// $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
|
||||
// $status = getContainerStatus($application->destination->server, $container['Names']);
|
||||
|
||||
if ($status !== 'running') {
|
||||
return response()->json([
|
||||
'message' => 'Application is not running.',
|
||||
], 400);
|
||||
}
|
||||
// if ($status !== 'running') {
|
||||
// return response()->json([
|
||||
// 'message' => 'Application is not running.',
|
||||
// ], 400);
|
||||
// }
|
||||
|
||||
$commands = collect([
|
||||
executeInDocker($container['Names'], $request->command),
|
||||
]);
|
||||
// $commands = collect([
|
||||
// executeInDocker($container['Names'], $request->command),
|
||||
// ]);
|
||||
|
||||
$res = instant_remote_process(command: $commands, server: $application->destination->server);
|
||||
// $res = instant_remote_process(command: $commands, server: $application->destination->server);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Command executed.',
|
||||
'response' => $res,
|
||||
]);
|
||||
}
|
||||
// return response()->json([
|
||||
// 'message' => 'Command executed.',
|
||||
// 'response' => $res,
|
||||
// ]);
|
||||
// }
|
||||
|
||||
private function validateDataApplications(Request $request, Server $server)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
private function validateDataApplications(Request $request, Server $server)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
|
||||
// Validate ports_mappings
|
||||
if ($request->has('ports_mappings')) {
|
||||
$ports = [];
|
||||
foreach (explode(',', $request->ports_mappings) as $portMapping) {
|
||||
$port = explode(':', $portMapping);
|
||||
if (in_array($port[0], $ports)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'ports_mappings' => 'The first number before : should be unique between mappings.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$ports[] = $port[0];
|
||||
}
|
||||
}
|
||||
// Validate custom_labels
|
||||
if ($request->has('custom_labels')) {
|
||||
if (! isBase64Encoded($request->custom_labels)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'custom_labels' => 'The custom_labels should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$customLabels = base64_decode($request->custom_labels);
|
||||
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'custom_labels' => 'The custom_labels should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
if ($request->has('domains') && $server->isProxyShouldRun()) {
|
||||
$uuid = $request->uuid;
|
||||
$fqdn = $request->domains;
|
||||
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
|
||||
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
|
||||
$errors = [];
|
||||
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
|
||||
if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
|
||||
$errors[] = 'Invalid domain: '.$domain;
|
||||
}
|
||||
// Validate ports_mappings
|
||||
if ($request->has('ports_mappings')) {
|
||||
$ports = [];
|
||||
foreach (explode(',', $request->ports_mappings) as $portMapping) {
|
||||
$port = explode(':', $portMapping);
|
||||
if (in_array($port[0], $ports)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'ports_mappings' => 'The first number before : should be unique between mappings.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$ports[] = $port[0];
|
||||
}
|
||||
}
|
||||
// Validate custom_labels
|
||||
if ($request->has('custom_labels')) {
|
||||
if (! isBase64Encoded($request->custom_labels)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'custom_labels' => 'The custom_labels should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$customLabels = base64_decode($request->custom_labels);
|
||||
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'custom_labels' => 'The custom_labels should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
if ($request->has('domains') && $server->isProxyShouldRun()) {
|
||||
$uuid = $request->uuid;
|
||||
$fqdn = $request->domains;
|
||||
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
|
||||
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
|
||||
$errors = [];
|
||||
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
|
||||
if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
|
||||
$errors[] = 'Invalid domain: '.$domain;
|
||||
}
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
if (count($errors) > 0) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'domains' => 'One of the domain is already used.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
if (count($errors) > 0) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'domains' => 'One of the domain is already used.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Actions\Database\StartDatabase;
|
||||
use App\Actions\Service\StartService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use App\Models\Tag;
|
||||
@@ -142,6 +143,7 @@ class DeployController extends Controller
|
||||
new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')),
|
||||
new OA\Parameter(name: 'pr', in: 'query', description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
|
||||
responses: [
|
||||
@@ -184,26 +186,32 @@ class DeployController extends Controller
|
||||
public function deploy(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$uuids = $request->query->get('uuid');
|
||||
$tags = $request->query->get('tag');
|
||||
$force = $request->query->get('force') ?? false;
|
||||
$pr = $request->query->get('pr') ? max((int) $request->query->get('pr'), 0) : 0;
|
||||
|
||||
if ($uuids && $tags) {
|
||||
return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400);
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
if ($tags && $pr) {
|
||||
return response()->json(['message' => 'You can only use tag or pr, not both.'], 400);
|
||||
}
|
||||
if ($tags) {
|
||||
return $this->by_tags($tags, $teamId, $force);
|
||||
} elseif ($uuids) {
|
||||
return $this->by_uuids($uuids, $teamId, $force);
|
||||
return $this->by_uuids($uuids, $teamId, $force, $pr);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'You must provide uuid or tag.'], 400);
|
||||
}
|
||||
|
||||
private function by_uuids(string $uuid, int $teamId, bool $force = false)
|
||||
private function by_uuids(string $uuid, int $teamId, bool $force = false, int $pr = 0)
|
||||
{
|
||||
$uuids = explode(',', $uuid);
|
||||
$uuids = collect(array_filter($uuids));
|
||||
@@ -216,7 +224,7 @@ class DeployController extends Controller
|
||||
foreach ($uuids as $uuid) {
|
||||
$resource = getResourceByUuid($uuid, $teamId);
|
||||
if ($resource) {
|
||||
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
|
||||
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force, $pr);
|
||||
if ($deployment_uuid) {
|
||||
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
|
||||
} else {
|
||||
@@ -281,7 +289,7 @@ class DeployController extends Controller
|
||||
return response()->json(['message' => 'No resources found with this tag.'], 404);
|
||||
}
|
||||
|
||||
public function deploy_resource($resource, bool $force = false): array
|
||||
public function deploy_resource($resource, bool $force = false, int $pr = 0): array
|
||||
{
|
||||
$message = null;
|
||||
$deployment_uuid = null;
|
||||
@@ -295,6 +303,7 @@ class DeployController extends Controller
|
||||
application: $resource,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: $force,
|
||||
pull_request_id: $pr,
|
||||
);
|
||||
$message = "Application {$resource->name} deployment queued.";
|
||||
break;
|
||||
@@ -314,4 +323,68 @@ class DeployController extends Controller
|
||||
|
||||
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List application deployments',
|
||||
description: 'List application deployments by using the app uuid',
|
||||
path: '/deployments/applications/{uuid}',
|
||||
operationId: 'list-deployments-by-app-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Deployments'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'List application deployments by using the app uuid.',
|
||||
content: [
|
||||
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/Application'),
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function get_application_deployments(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'skip' => ['nullable', 'integer', 'min:0'],
|
||||
'take' => ['nullable', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
$app_uuid = $request->route('uuid', null);
|
||||
$skip = $request->get('skip', 0);
|
||||
$take = $request->get('take', 10);
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$servers = Server::whereTeamId($teamId)->get();
|
||||
|
||||
if (is_null($app_uuid)) {
|
||||
return response()->json(['message' => 'Application uuid is required'], 400);
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $app_uuid)->first();
|
||||
|
||||
if (is_null($application)) {
|
||||
return response()->json(['message' => 'Application not found'], 404);
|
||||
}
|
||||
$deployments = $application->deployments($skip, $take);
|
||||
|
||||
return response()->json($deployments);
|
||||
}
|
||||
}
|
||||
|
@@ -368,6 +368,20 @@ class SecurityController extends Controller
|
||||
response: 404,
|
||||
description: 'Private Key not found.',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
description: 'Private Key is in use and cannot be deleted.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Private Key is in use and cannot be deleted.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
)]
|
||||
public function delete_key(Request $request)
|
||||
@@ -384,6 +398,14 @@ class SecurityController extends Controller
|
||||
if (is_null($key)) {
|
||||
return response()->json(['message' => 'Private Key not found.'], 404);
|
||||
}
|
||||
|
||||
if ($key->isInUse()) {
|
||||
return response()->json([
|
||||
'message' => 'Private Key is in use and cannot be deleted.',
|
||||
'details' => 'This private key is currently being used by servers, applications, or Git integrations.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$key->forceDelete();
|
||||
|
||||
return response()->json([
|
||||
|
@@ -13,6 +13,7 @@ use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ServicesController extends Controller
|
||||
{
|
||||
@@ -88,8 +89,8 @@ class ServicesController extends Controller
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create',
|
||||
description: 'Create a one-click service',
|
||||
summary: 'Create service',
|
||||
description: 'Create a one-click / custom service',
|
||||
path: '/services',
|
||||
operationId: 'create-service',
|
||||
security: [
|
||||
@@ -102,7 +103,7 @@ class ServicesController extends Controller
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'type'],
|
||||
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
|
||||
properties: [
|
||||
'type' => [
|
||||
'description' => 'The one-click service type',
|
||||
@@ -204,6 +205,7 @@ class ServicesController extends Controller
|
||||
'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
|
||||
'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -211,7 +213,7 @@ class ServicesController extends Controller
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Create a service.',
|
||||
description: 'Service created successfully.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
@@ -237,7 +239,7 @@ class ServicesController extends Controller
|
||||
)]
|
||||
public function create_service(Request $request)
|
||||
{
|
||||
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy'];
|
||||
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -249,12 +251,13 @@ class ServicesController extends Controller
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'type' => 'string|required',
|
||||
'type' => 'string|required_without:docker_compose_raw',
|
||||
'docker_compose_raw' => 'string|required_without:type',
|
||||
'project_uuid' => 'string|required',
|
||||
'environment_name' => 'string|nullable',
|
||||
'environment_uuid' => 'string|nullable',
|
||||
'server_uuid' => 'string|required',
|
||||
'destination_uuid' => 'string',
|
||||
'destination_uuid' => 'string|nullable',
|
||||
'name' => 'string|max:255',
|
||||
'description' => 'string|nullable',
|
||||
'instant_deploy' => 'boolean',
|
||||
@@ -372,12 +375,16 @@ class ServicesController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
} else {
|
||||
return response()->json(['message' => 'Invalid service type.', 'valid_service_types' => $serviceKeys], 400);
|
||||
}
|
||||
return response()->json(['message' => 'Service not found.', 'valid_service_types' => $serviceKeys], 404);
|
||||
} elseif (filled($request->docker_compose_raw)) {
|
||||
|
||||
return response()->json(['message' => 'Invalid service type.'], 400);
|
||||
$service = new Service;
|
||||
$result = $this->upsert_service($request, $service, $teamId);
|
||||
|
||||
return response()->json(serializeApiResponse($result))->setStatusCode(201);
|
||||
} else {
|
||||
return response()->json(['message' => 'No service type or docker_compose_raw provided.'], 400);
|
||||
}
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
@@ -511,6 +518,206 @@ class ServicesController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update',
|
||||
description: 'Update service by UUID.',
|
||||
path: '/services/{uuid}',
|
||||
operationId: 'update-service-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Services'],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Service updated.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'],
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The service name.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The service description.'],
|
||||
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
|
||||
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
|
||||
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID.'],
|
||||
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the service should be deployed instantly.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'Connect the service to the predefined docker network.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Service updated.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'description' => 'Service UUID.'],
|
||||
'domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Service domains.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
|
||||
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
$result = $this->upsert_service($request, $service, $teamId);
|
||||
|
||||
return response()->json(serializeApiResponse($result))->setStatusCode(200);
|
||||
}
|
||||
|
||||
private function upsert_service(Request $request, Service $service, string $teamId)
|
||||
{
|
||||
$allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'project_uuid' => 'string|required',
|
||||
'environment_name' => 'string|nullable',
|
||||
'environment_uuid' => 'string|nullable',
|
||||
'server_uuid' => 'string|required',
|
||||
'destination_uuid' => 'string',
|
||||
'name' => 'string|max:255',
|
||||
'description' => 'string|nullable',
|
||||
'instant_deploy' => 'boolean',
|
||||
'connect_to_docker_network' => 'boolean',
|
||||
'docker_compose_raw' => 'string|required',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
$environmentUuid = $request->environment_uuid;
|
||||
$environmentName = $request->environment_name;
|
||||
if (blank($environmentUuid) && blank($environmentName)) {
|
||||
return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
|
||||
}
|
||||
$serverUuid = $request->server_uuid;
|
||||
$instantDeploy = $request->instant_deploy ?? false;
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
|
||||
if (! $project) {
|
||||
return response()->json(['message' => 'Project not found.'], 404);
|
||||
}
|
||||
$environment = $project->environments()->where('name', $environmentName)->first();
|
||||
if (! $environment) {
|
||||
$environment = $project->environments()->where('uuid', $environmentUuid)->first();
|
||||
}
|
||||
if (! $environment) {
|
||||
return response()->json(['message' => 'Environment not found.'], 404);
|
||||
}
|
||||
$server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
|
||||
if (! $server) {
|
||||
return response()->json(['message' => 'Server not found.'], 404);
|
||||
}
|
||||
$destinations = $server->destinations();
|
||||
if ($destinations->count() == 0) {
|
||||
return response()->json(['message' => 'Server has no destinations.'], 400);
|
||||
}
|
||||
if ($destinations->count() > 1 && ! $request->has('destination_uuid')) {
|
||||
return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
|
||||
}
|
||||
$destination = $destinations->first();
|
||||
if (! isBase64Encoded($request->docker_compose_raw)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
|
||||
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerCompose = base64_decode($request->docker_compose_raw);
|
||||
$dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
|
||||
$connectToDockerNetwork = $request->connect_to_docker_network ?? false;
|
||||
|
||||
$service->name = $request->name ?? null;
|
||||
$service->description = $request->description ?? null;
|
||||
$service->docker_compose_raw = $dockerComposeRaw;
|
||||
$service->environment_id = $environment->id;
|
||||
$service->server_id = $server->id;
|
||||
$service->destination_id = $destination->id;
|
||||
$service->destination_type = $destination->getMorphClass();
|
||||
$service->connect_to_docker_network = $connectToDockerNetwork;
|
||||
$service->save();
|
||||
|
||||
$service->parse();
|
||||
if ($instantDeploy) {
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
|
||||
$domains = $service->applications()->get()->pluck('fqdn')->sort();
|
||||
$domains = $domains->map(function ($domain) {
|
||||
if (count(explode(':', $domain)) > 2) {
|
||||
return str($domain)->beforeLast(':')->value();
|
||||
}
|
||||
|
||||
return $domain;
|
||||
})->values();
|
||||
|
||||
return [
|
||||
'uuid' => $service->uuid,
|
||||
'domains' => $domains,
|
||||
];
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Envs',
|
||||
description: 'List all envs by service UUID.',
|
||||
|
@@ -329,13 +329,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
$this->write_deployment_configurations();
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
|
||||
'hidden' => true,
|
||||
'ignore_errors' => true,
|
||||
]
|
||||
);
|
||||
$this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
|
||||
$this->graceful_shutdown_container($this->deployment_uuid);
|
||||
|
||||
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
|
||||
}
|
||||
@@ -1211,7 +1206,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->container_name) {
|
||||
$counter = 1;
|
||||
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
|
||||
if ($this->full_healthcheck_url) {
|
||||
if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
|
||||
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
|
||||
@@ -1366,13 +1361,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
'command' => "docker rm -f {$this->deployment_uuid}",
|
||||
'ignore_errors' => true,
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
$this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
|
||||
$this->graceful_shutdown_container($this->deployment_uuid);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
$runCommand,
|
||||
@@ -1718,8 +1708,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'save' => 'dockerfile_from_repo',
|
||||
'ignore_errors' => true,
|
||||
]);
|
||||
$dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
|
||||
$this->application->parseHealthcheckFromDockerfile($dockerfile);
|
||||
$this->application->parseHealthcheckFromDockerfile($this->saved_outputs->get('dockerfile_from_repo'));
|
||||
}
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
@@ -2029,7 +2018,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
|
||||
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
|
||||
} else {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
if ($this->application->settings->is_spa) {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration('spa'));
|
||||
} else {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($this->application->build_pack === 'nixpacks') {
|
||||
@@ -2096,7 +2089,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
|
||||
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
|
||||
} else {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
if ($this->application->settings->is_spa) {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration('spa'));
|
||||
} else {
|
||||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
}
|
||||
}
|
||||
}
|
||||
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
|
@@ -20,7 +20,7 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
|
||||
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("'.config('constants.coolify.registry_url').'/coollabsio/coolify-helper")))\''], $this->server, false);
|
||||
$containerIds = collect(json_decode($containers))->pluck('ID');
|
||||
if ($containerIds->count() > 0) {
|
||||
foreach ($containerIds as $containerId) {
|
||||
|
@@ -484,6 +484,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
$fullImageName = $this->getFullImageName();
|
||||
|
||||
$containerExists = instant_remote_process(["docker ps -a -q -f name=backup-of-{$this->backup->uuid}"], $this->server, false);
|
||||
if (filled($containerExists)) {
|
||||
instant_remote_process(["docker rm -f backup-of-{$this->backup->uuid}"], $this->server, false);
|
||||
}
|
||||
|
||||
if (isDev()) {
|
||||
if ($this->database->name === 'coolify-db') {
|
||||
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
|
||||
|
@@ -66,12 +66,9 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
|
||||
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
|
||||
|| $this->resource instanceof StandaloneRedis
|
||||
|| $this->resource instanceof StandaloneMongodb
|
||||
@@ -80,6 +77,18 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|| $this->resource instanceof StandaloneKeydb
|
||||
|| $this->resource instanceof StandaloneDragonfly
|
||||
|| $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');
|
||||
if (($this->dockerCleanup || $isDatabase) && $server) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
|
78
app/Jobs/RegenerateSslCertJob.php
Normal file
78
app/Jobs/RegenerateSslCertJob.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?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();
|
||||
|
||||
if (! $caCert) {
|
||||
Log::error("No CA certificate found for server_id: {$certificate->server_id}");
|
||||
|
||||
return;
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
46
app/Jobs/RestartProxyJob.php
Normal file
46
app/Jobs/RestartProxyJob.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 60;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
StopProxy::run($this->server);
|
||||
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
StartProxy::run($this->server, force: true);
|
||||
|
||||
CheckProxy::run($this->server, true);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,6 +7,7 @@ use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Services\ConfigurationRepository;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
@@ -266,7 +267,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
public function validateServer()
|
||||
{
|
||||
try {
|
||||
config()->set('constants.ssh.mux_enabled', false);
|
||||
$this->disableSshMux();
|
||||
|
||||
// EC2 does not have `uptime` command, lol
|
||||
instant_remote_process(['ls /'], $this->createdServer, true);
|
||||
@@ -376,6 +377,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey();
|
||||
}
|
||||
|
||||
private function disableSshMux(): void
|
||||
{
|
||||
$configRepository = app(ConfigurationRepository::class);
|
||||
$configRepository->disableSshMux();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.boarding.index')->layout('layouts.boarding');
|
||||
|
@@ -56,6 +56,9 @@ class Discord extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachableDiscordNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $discordPingEnabled = true;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -87,6 +90,8 @@ class Discord extends Component
|
||||
$this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications;
|
||||
$this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications;
|
||||
|
||||
$this->settings->discord_ping_enabled = $this->discordPingEnabled;
|
||||
|
||||
$this->settings->save();
|
||||
refreshSession();
|
||||
} else {
|
||||
@@ -105,12 +110,30 @@ class Discord extends Component
|
||||
$this->serverDiskUsageDiscordNotifications = $this->settings->server_disk_usage_discord_notifications;
|
||||
$this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications;
|
||||
$this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications;
|
||||
|
||||
$this->discordPingEnabled = $this->settings->discord_ping_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveDiscordPingEnabled()
|
||||
{
|
||||
try {
|
||||
$original = $this->discordPingEnabled;
|
||||
$this->validate([
|
||||
'discordPingEnabled' => 'required',
|
||||
]);
|
||||
$this->saveModel();
|
||||
} catch (\Throwable $e) {
|
||||
$this->discordPingEnabled = $original;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveDiscordEnabled()
|
||||
{
|
||||
try {
|
||||
$original = $this->discordEnabled;
|
||||
$this->validate([
|
||||
'discordWebhookUrl' => 'required',
|
||||
], [
|
||||
@@ -118,7 +141,7 @@ class Discord extends Component
|
||||
]);
|
||||
$this->saveModel();
|
||||
} catch (\Throwable $e) {
|
||||
$this->discordEnabled = false;
|
||||
$this->discordEnabled = $original;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
@@ -70,6 +70,7 @@ class Index extends Component
|
||||
$this->current_password = '';
|
||||
$this->new_password = '';
|
||||
$this->new_password_confirmation = '';
|
||||
$this->dispatch('reloadWindow');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ class Configuration extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->currentRoute = request()->route()->getName();
|
||||
|
||||
$project = currentTeam()
|
||||
->projects()
|
||||
->select('id', 'uuid', 'team_id')
|
||||
@@ -39,6 +40,9 @@ class Configuration extends Component
|
||||
$this->project = $project;
|
||||
$this->environment = $environment;
|
||||
$this->application = $application;
|
||||
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
|
||||
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
@@ -87,6 +87,7 @@ class General extends Component
|
||||
'application.post_deployment_command_container' => 'nullable',
|
||||
'application.custom_nginx_configuration' => 'nullable',
|
||||
'application.settings.is_static' => 'boolean|required',
|
||||
'application.settings.is_spa' => 'boolean|required',
|
||||
'application.settings.is_build_server_enabled' => 'boolean|required',
|
||||
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
|
||||
'application.settings.is_container_label_readonly_enabled' => 'boolean|required',
|
||||
@@ -126,6 +127,7 @@ class General extends Component
|
||||
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
|
||||
'application.custom_nginx_configuration' => 'Custom Nginx configuration',
|
||||
'application.settings.is_static' => 'Is static',
|
||||
'application.settings.is_spa' => 'Is SPA',
|
||||
'application.settings.is_build_server_enabled' => 'Is build server enabled',
|
||||
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
|
||||
'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly',
|
||||
@@ -173,6 +175,9 @@ class General extends Component
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
if ($this->application->settings->isDirty('is_spa')) {
|
||||
$this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
|
||||
}
|
||||
$this->application->settings->save();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
$this->application->refresh();
|
||||
@@ -192,6 +197,7 @@ class General extends Component
|
||||
if ($this->application->settings->is_container_label_readonly_enabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function loadComposeFile($isInit = false)
|
||||
@@ -289,9 +295,9 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function generateNginxConfiguration()
|
||||
public function generateNginxConfiguration($type = 'static')
|
||||
{
|
||||
$this->application->custom_nginx_configuration = defaultNginxConfiguration();
|
||||
$this->application->custom_nginx_configuration = defaultNginxConfiguration($type);
|
||||
$this->application->save();
|
||||
$this->dispatch('success', 'Nginx configuration generated.');
|
||||
}
|
||||
@@ -371,6 +377,9 @@ class General extends Component
|
||||
if ($this->application->isDirty('redirect')) {
|
||||
$this->setRedirect();
|
||||
}
|
||||
if ($this->application->isDirty('dockerfile')) {
|
||||
$this->application->parseHealthcheckFromDockerfile($this->application->dockerfile);
|
||||
}
|
||||
|
||||
$this->checkFqdns();
|
||||
|
||||
@@ -448,7 +457,6 @@ class General extends Component
|
||||
{
|
||||
$config = GenerateConfig::run($this->application, true);
|
||||
$fileName = str($this->application->name)->slug()->append('_config.json');
|
||||
dd($config);
|
||||
|
||||
return response()->streamDownload(function () use ($config) {
|
||||
echo $config;
|
||||
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Dragonfly;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\Validate;
|
||||
@@ -50,12 +53,19 @@ class General extends Component
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $enable_ssl = false;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -64,6 +74,12 @@ class General extends Component
|
||||
try {
|
||||
$this->syncData();
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -82,6 +98,7 @@ class General extends Component
|
||||
$this->database->public_port = $this->publicPort;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->enable_ssl = $this->enable_ssl;
|
||||
$this->database->save();
|
||||
|
||||
$this->dbUrl = $this->database->internal_db_url;
|
||||
@@ -96,6 +113,7 @@ class General extends Component
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->enable_ssl = $this->database->enable_ssl;
|
||||
$this->dbUrl = $this->database->internal_db_url;
|
||||
$this->dbUrlPublic = $this->database->external_db_url;
|
||||
}
|
||||
@@ -174,4 +192,61 @@ 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;
|
||||
}
|
||||
|
||||
$server = $this->database->destination->server;
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $server->id)
|
||||
->where('is_ca_certificate', true)
|
||||
->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
isPemKeyFileRequired: true,
|
||||
);
|
||||
|
||||
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
|
||||
} catch (Exception $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -31,8 +31,8 @@ class Heading extends Component
|
||||
$this->database->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$this->dispatch('refresh');
|
||||
$this->check_status();
|
||||
|
||||
if (is_null($this->database->config_hash) || $this->database->isConfigurationChanged()) {
|
||||
$this->database->isConfigurationChanged(true);
|
||||
$this->dispatch('configurationChanged');
|
||||
@@ -44,7 +44,7 @@ class Heading extends Component
|
||||
public function check_status($showNotification = false)
|
||||
{
|
||||
if ($this->database->destination->server->isFunctional()) {
|
||||
GetContainersStatus::dispatch($this->database->destination->server);
|
||||
GetContainersStatus::run($this->database->destination->server);
|
||||
}
|
||||
|
||||
if ($showNotification) {
|
||||
@@ -63,6 +63,7 @@ class Heading extends Component
|
||||
$this->database->status = 'exited';
|
||||
$this->database->save();
|
||||
$this->check_status();
|
||||
$this->dispatch('refresh');
|
||||
}
|
||||
|
||||
public function restart()
|
||||
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Keydb;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\Validate;
|
||||
@@ -53,12 +56,20 @@ class General extends Component
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $enable_ssl = false;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -67,6 +78,12 @@ class General extends Component
|
||||
try {
|
||||
$this->syncData();
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -86,6 +103,7 @@ class General extends Component
|
||||
$this->database->public_port = $this->publicPort;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->enable_ssl = $this->enable_ssl;
|
||||
$this->database->save();
|
||||
|
||||
$this->dbUrl = $this->database->internal_db_url;
|
||||
@@ -101,6 +119,7 @@ class General extends Component
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->enable_ssl = $this->database->enable_ssl;
|
||||
$this->dbUrl = $this->database->internal_db_url;
|
||||
$this->dbUrlPublic = $this->database->external_db_url;
|
||||
}
|
||||
@@ -179,4 +198,48 @@ 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,
|
||||
isPemKeyFileRequired: true,
|
||||
);
|
||||
|
||||
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
|
||||
} catch (Exception $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Mariadb;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class General extends Component
|
||||
@@ -21,6 +25,18 @@ class General extends Component
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
'database.description' => 'nullable',
|
||||
@@ -35,6 +51,7 @@ class General extends Component
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'database.enable_ssl' => 'boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -50,6 +67,7 @@ class General extends Component
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||
'database.enable_ssl' => 'Enable SSL',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -57,6 +75,12 @@ class General extends Component
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveAdvanced()
|
||||
@@ -127,6 +151,48 @@ 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,
|
||||
isPemKeyFileRequired: true,
|
||||
);
|
||||
|
||||
$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
|
||||
{
|
||||
$this->database->refresh();
|
||||
|
@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Mongodb;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class General extends Component
|
||||
@@ -21,6 +25,18 @@ class General extends Component
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
'database.description' => 'nullable',
|
||||
@@ -34,6 +50,8 @@ class General extends Component
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'database.enable_ssl' => 'boolean',
|
||||
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-full',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -48,6 +66,8 @@ class General extends Component
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
'database.enable_ssl' => 'Enable SSL',
|
||||
'database.ssl_mode' => 'SSL Mode',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -55,6 +75,12 @@ class General extends Component
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveAdvanced()
|
||||
@@ -128,6 +154,53 @@ 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,
|
||||
isPemKeyFileRequired: true,
|
||||
);
|
||||
|
||||
$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
|
||||
{
|
||||
$this->database->refresh();
|
||||
|
@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Mysql;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMysql;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class General extends Component
|
||||
@@ -21,6 +25,18 @@ class General extends Component
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
'database.description' => 'nullable',
|
||||
@@ -35,6 +51,8 @@ class General extends Component
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'database.enable_ssl' => 'boolean',
|
||||
'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -50,6 +68,8 @@ class General extends Component
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
'database.enable_ssl' => 'Enable SSL',
|
||||
'database.ssl_mode' => 'SSL Mode',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -57,6 +77,12 @@ class General extends Component
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveAdvanced()
|
||||
@@ -127,6 +153,53 @@ 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,
|
||||
isPemKeyFileRequired: true,
|
||||
);
|
||||
|
||||
$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
|
||||
{
|
||||
$this->database->refresh();
|
||||
|
@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Postgresql;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class General extends Component
|
||||
@@ -23,10 +27,15 @@ class General extends Component
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
|
||||
return [
|
||||
'refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
'save_init_script',
|
||||
'delete_init_script',
|
||||
];
|
||||
@@ -48,6 +57,8 @@ class General extends Component
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'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 = [
|
||||
@@ -65,6 +76,8 @@ class General extends Component
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
'database.enable_ssl' => 'Enable SSL',
|
||||
'database.ssl_mode' => 'SSL Mode',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -72,6 +85,12 @@ class General extends Component
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveAdvanced()
|
||||
@@ -91,6 +110,53 @@ 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,
|
||||
isPemKeyFileRequired: true,
|
||||
);
|
||||
|
||||
$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()
|
||||
{
|
||||
try {
|
||||
@@ -143,7 +209,7 @@ class General extends Component
|
||||
$delete_command = "rm -f $old_file_path";
|
||||
try {
|
||||
instant_remote_process([$delete_command], $this->server);
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
$this->dispatch('error', 'Failed to remove old init script from server: '.$e->getMessage());
|
||||
|
||||
return;
|
||||
@@ -184,7 +250,7 @@ class General extends Component
|
||||
$command = "rm -f $file_path";
|
||||
try {
|
||||
instant_remote_process([$command], $this->server);
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
$this->dispatch('error', 'Failed to remove init script from server: '.$e->getMessage());
|
||||
|
||||
return;
|
||||
@@ -201,16 +267,11 @@ class General extends Component
|
||||
|
||||
$this->database->init_scripts = $updatedScripts;
|
||||
$this->database->save();
|
||||
$this->refresh();
|
||||
$this->dispatch('refresh')->self();
|
||||
$this->dispatch('success', 'Init script deleted from the database and the server.');
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
}
|
||||
|
||||
public function save_new_init_script()
|
||||
{
|
||||
$this->validate([
|
||||
|
@@ -4,25 +4,24 @@ namespace App\Livewire\Project\Database\Redis;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class General extends Component
|
||||
{
|
||||
protected $listeners = [
|
||||
'envsUpdated' => 'refresh',
|
||||
'refresh',
|
||||
];
|
||||
|
||||
public Server $server;
|
||||
|
||||
public StandaloneRedis $database;
|
||||
|
||||
public string $redis_username;
|
||||
|
||||
public string $redis_password;
|
||||
public ?string $redis_password;
|
||||
|
||||
public string $redis_version;
|
||||
|
||||
@@ -30,6 +29,19 @@ class General extends Component
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'envsUpdated' => 'refresh',
|
||||
'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
'database.description' => 'nullable',
|
||||
@@ -42,6 +54,7 @@ class General extends Component
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'redis_username' => 'required',
|
||||
'redis_password' => 'required',
|
||||
'database.enable_ssl' => 'boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -55,12 +68,18 @@ class General extends Component
|
||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||
'redis_username' => 'Redis Username',
|
||||
'redis_password' => 'Redis Password',
|
||||
'database.enable_ssl' => 'Enable SSL',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
$this->refreshView();
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveAdvanced()
|
||||
@@ -136,6 +155,48 @@ 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,
|
||||
isPemKeyFileRequired: true,
|
||||
);
|
||||
|
||||
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
|
||||
} catch (Exception $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
|
@@ -7,7 +7,6 @@ use App\Models\Project;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
@@ -66,7 +65,6 @@ class DockerCompose extends Component
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
$service = Service::create([
|
||||
'name' => 'service'.Str::random(10),
|
||||
'docker_compose_raw' => $this->dockerComposeRaw,
|
||||
'environment_id' => $environment->id,
|
||||
'server_id' => (int) $server_id,
|
||||
@@ -85,8 +83,6 @@ class DockerCompose extends Component
|
||||
'resourceable_type' => $service->getMorphClass(),
|
||||
]);
|
||||
}
|
||||
$service->name = "service-$service->uuid";
|
||||
|
||||
$service->parse(isNew: true);
|
||||
|
||||
return redirect()->route('project.service.configuration', [
|
||||
|
@@ -106,11 +106,15 @@ class GithubPrivateRepository extends Component
|
||||
$this->selected_github_app_id = $github_app_id;
|
||||
$this->github_app = GithubApp::where('id', $github_app_id)->first();
|
||||
$this->token = generateGithubInstallationToken($this->github_app);
|
||||
$this->loadRepositoryByPage();
|
||||
$repositories = loadRepositoryByPage($this->github_app, $this->token, $this->page);
|
||||
$this->total_repositories_count = $repositories['total_count'];
|
||||
$this->repositories = $this->repositories->concat(collect($repositories['repositories']));
|
||||
if ($this->repositories->count() < $this->total_repositories_count) {
|
||||
while ($this->repositories->count() < $this->total_repositories_count) {
|
||||
$this->page++;
|
||||
$this->loadRepositoryByPage();
|
||||
$repositories = loadRepositoryByPage($this->github_app, $this->token, $this->page);
|
||||
$this->total_repositories_count = $repositories['total_count'];
|
||||
$this->repositories = $this->repositories->concat(collect($repositories['repositories']));
|
||||
}
|
||||
}
|
||||
$this->repositories = $this->repositories->sortBy('name');
|
||||
@@ -120,21 +124,6 @@ class GithubPrivateRepository extends Component
|
||||
$this->current_step = 'repository';
|
||||
}
|
||||
|
||||
protected function loadRepositoryByPage()
|
||||
{
|
||||
$response = Http::withToken($this->token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100&page={$this->page}");
|
||||
$json = $response->json();
|
||||
if ($response->status() !== 200) {
|
||||
return $this->dispatch('error', $json['message']);
|
||||
}
|
||||
|
||||
if ($json['total_count'] === 0) {
|
||||
return;
|
||||
}
|
||||
$this->total_repositories_count = $json['total_count'];
|
||||
$this->repositories = $this->repositories->concat(collect($json['repositories']));
|
||||
}
|
||||
|
||||
public function loadBranches()
|
||||
{
|
||||
$this->selected_repository_owner = $this->repositories->where('id', $this->selected_repository_id)->first()['owner']['login'];
|
||||
|
@@ -74,7 +74,7 @@ CMD ["nginx", "-g", "daemon off;"]
|
||||
'fqdn' => $fqdn,
|
||||
]);
|
||||
|
||||
$application->parseHealthcheckFromDockerfile(dockerfile: collect(str($this->dockerfile)->trim()->explode("\n")), isInit: true);
|
||||
$application->parseHealthcheckFromDockerfile(dockerfile: $this->dockerfile, isInit: true);
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
'application_uuid' => $application->uuid,
|
||||
|
@@ -73,7 +73,6 @@ class Create extends Component
|
||||
if ($oneClickService) {
|
||||
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
|
||||
$service_payload = [
|
||||
'name' => "$oneClickServiceName-".str()->random(10),
|
||||
'docker_compose_raw' => base64_decode($oneClickService),
|
||||
'environment_id' => $environment->id,
|
||||
'service_type' => $oneClickServiceName,
|
||||
|
@@ -49,7 +49,6 @@ class FileStorage extends Component
|
||||
$this->workdir = null;
|
||||
$this->fs_path = $this->fileStorage->fs_path;
|
||||
}
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
}
|
||||
|
||||
public function convertToDirectory()
|
||||
@@ -68,6 +67,18 @@ class FileStorage extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function loadStorageOnServer()
|
||||
{
|
||||
try {
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
$this->dispatch('success', 'File storage loaded from server.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->dispatch('refreshStorages');
|
||||
}
|
||||
}
|
||||
|
||||
public function convertToFile()
|
||||
{
|
||||
try {
|
||||
|
@@ -3,10 +3,13 @@
|
||||
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
||||
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Traits\EnvironmentVariableProtection;
|
||||
use Livewire\Component;
|
||||
|
||||
class All extends Component
|
||||
{
|
||||
use EnvironmentVariableProtection;
|
||||
|
||||
public $resource;
|
||||
|
||||
public string $resourceClass;
|
||||
@@ -138,17 +141,57 @@ class All extends Component
|
||||
private function handleBulkSubmit()
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$changesMade = false;
|
||||
$errorOccurred = false;
|
||||
|
||||
$this->deleteRemovedVariables(false, $variables);
|
||||
$this->updateOrCreateVariables(false, $variables);
|
||||
// Try to delete removed variables
|
||||
$deletedCount = $this->deleteRemovedVariables(false, $variables);
|
||||
if ($deletedCount > 0) {
|
||||
$changesMade = true;
|
||||
} elseif ($deletedCount === 0 && $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->exists()) {
|
||||
// If we tried to delete but couldn't (due to Docker Compose), mark as error
|
||||
$errorOccurred = true;
|
||||
}
|
||||
|
||||
// Update or create variables
|
||||
$updatedCount = $this->updateOrCreateVariables(false, $variables);
|
||||
if ($updatedCount > 0) {
|
||||
$changesMade = true;
|
||||
}
|
||||
|
||||
if ($this->showPreview) {
|
||||
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$this->deleteRemovedVariables(true, $previewVariables);
|
||||
$this->updateOrCreateVariables(true, $previewVariables);
|
||||
|
||||
// Try to delete removed preview variables
|
||||
$deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables);
|
||||
if ($deletedPreviewCount > 0) {
|
||||
$changesMade = true;
|
||||
} elseif ($deletedPreviewCount === 0 && $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($previewVariables))->exists()) {
|
||||
// If we tried to delete but couldn't (due to Docker Compose), mark as error
|
||||
$errorOccurred = true;
|
||||
}
|
||||
|
||||
// Update or create preview variables
|
||||
$updatedPreviewCount = $this->updateOrCreateVariables(true, $previewVariables);
|
||||
if ($updatedPreviewCount > 0) {
|
||||
$changesMade = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->dispatch('success', 'Environment variables updated.');
|
||||
// Debug information
|
||||
\Log::info('Environment variables update status', [
|
||||
'deletedCount' => $deletedCount,
|
||||
'updatedCount' => $updatedCount,
|
||||
'deletedPreviewCount' => $deletedPreviewCount ?? 0,
|
||||
'updatedPreviewCount' => $updatedPreviewCount ?? 0,
|
||||
'changesMade' => $changesMade,
|
||||
'errorOccurred' => $errorOccurred,
|
||||
]);
|
||||
|
||||
// Only show success message if changes were actually made and no errors occurred
|
||||
if ($changesMade && ! $errorOccurred) {
|
||||
$this->dispatch('success', 'Environment variables updated.');
|
||||
}
|
||||
}
|
||||
|
||||
private function handleSingleSubmit($data)
|
||||
@@ -184,11 +227,46 @@ class All extends Component
|
||||
private function deleteRemovedVariables($isPreview, $variables)
|
||||
{
|
||||
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
|
||||
|
||||
// Get all environment variables that will be deleted
|
||||
$variablesToDelete = $this->resource->$method()->whereNotIn('key', array_keys($variables))->get();
|
||||
|
||||
// If there are no variables to delete, return 0
|
||||
if ($variablesToDelete->isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check for system variables that shouldn't be deleted
|
||||
foreach ($variablesToDelete as $envVar) {
|
||||
if ($this->isProtectedEnvironmentVariable($envVar->key)) {
|
||||
$this->dispatch('error', "Cannot delete system environment variable '{$envVar->key}'.");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any of these variables are used in Docker Compose
|
||||
if ($this->resource->type() === 'service' || $this->resource->build_pack === 'dockercompose') {
|
||||
foreach ($variablesToDelete as $envVar) {
|
||||
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($envVar->key, $this->resource->docker_compose);
|
||||
|
||||
if ($isUsed) {
|
||||
$this->dispatch('error', "Cannot delete environment variable '{$envVar->key}' <br><br>Please remove it from the Docker Compose file first.");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, no variables are used in Docker Compose, so we can delete them
|
||||
$this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
|
||||
|
||||
return $variablesToDelete->count();
|
||||
}
|
||||
|
||||
private function updateOrCreateVariables($isPreview, $variables)
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($variables as $key => $value) {
|
||||
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) {
|
||||
continue;
|
||||
@@ -198,8 +276,12 @@ class All extends Component
|
||||
|
||||
if ($found) {
|
||||
if (! $found->is_shown_once && ! $found->is_multiline) {
|
||||
$found->value = $value;
|
||||
$found->save();
|
||||
// Only count as a change if the value actually changed
|
||||
if ($found->value !== $value) {
|
||||
$found->value = $value;
|
||||
$found->save();
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$environment = new EnvironmentVariable;
|
||||
@@ -212,8 +294,11 @@ class All extends Component
|
||||
$environment->resourceable_type = $this->resource->getMorphClass();
|
||||
|
||||
$environment->save();
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function refreshEnvs()
|
||||
|
@@ -4,10 +4,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
||||
|
||||
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Traits\EnvironmentVariableProtection;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
use EnvironmentVariableProtection;
|
||||
|
||||
public $parameters;
|
||||
|
||||
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
|
||||
@@ -40,6 +43,8 @@ class Show extends Component
|
||||
|
||||
public bool $is_really_required = false;
|
||||
|
||||
public bool $is_redis_credential = false;
|
||||
|
||||
protected $listeners = [
|
||||
'refreshEnvs' => 'refresh',
|
||||
'refresh',
|
||||
@@ -65,7 +70,9 @@ class Show extends Component
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->checkEnvs();
|
||||
|
||||
if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) {
|
||||
$this->is_redis_credential = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh()
|
||||
@@ -171,6 +178,24 @@ class Show extends Component
|
||||
public function delete()
|
||||
{
|
||||
try {
|
||||
// Check if the variable is protected
|
||||
if ($this->isProtectedEnvironmentVariable($this->env->key)) {
|
||||
$this->dispatch('error', "Cannot delete system environment variable '{$this->env->key}'.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the variable is used in Docker Compose
|
||||
if ($this->type === 'service' || $this->type === 'application' && $this->env->resource()?->docker_compose) {
|
||||
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($this->env->key, $this->env->resource()?->docker_compose);
|
||||
|
||||
if ($isUsed) {
|
||||
$this->dispatch('error', "Cannot delete environment variable '{$this->env->key}' <br><br>Please remove it from the Docker Compose file first.");
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->env->delete();
|
||||
$this->dispatch('environmentVariableDeleted');
|
||||
$this->dispatch('success', 'Environment variable deleted successfully.');
|
||||
|
@@ -2,7 +2,11 @@
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Jobs\RegenerateSslCertJob;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use Carbon\Carbon;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -10,6 +14,14 @@ class Advanced extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public ?SslCertificate $caCertificate = null;
|
||||
|
||||
public $showCertificate = false;
|
||||
|
||||
public $certificateContent = '';
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
public array $parameters = [];
|
||||
|
||||
#[Validate(['string'])]
|
||||
@@ -30,11 +42,99 @@ class Advanced extends Component
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->syncData();
|
||||
$this->loadCaCertificate();
|
||||
} catch (\Throwable) {
|
||||
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)
|
||||
{
|
||||
if ($toModel) {
|
||||
|
@@ -4,11 +4,10 @@ namespace App\Livewire\Server\Proxy;
|
||||
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Jobs\RestartProxyJob;
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Component;
|
||||
|
||||
class Deploy extends Component
|
||||
@@ -65,7 +64,7 @@ class Deploy extends Component
|
||||
public function restart()
|
||||
{
|
||||
try {
|
||||
$this->stop();
|
||||
RestartProxyJob::dispatch($this->server);
|
||||
$this->dispatch('checkProxy');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -98,43 +97,10 @@ class Deploy extends Component
|
||||
public function stop(bool $forceStop = true)
|
||||
{
|
||||
try {
|
||||
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
$timeout = 30;
|
||||
|
||||
$process = $this->stopContainer($containerName, $timeout);
|
||||
|
||||
$startTime = Carbon::now()->getTimestamp();
|
||||
while ($process->running()) {
|
||||
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
|
||||
$this->forceStopContainer($containerName);
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
$this->removeContainer($containerName);
|
||||
StopProxy::run($this->server, $forceStop);
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->server->proxy->force_stop = $forceStop;
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->save();
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||
{
|
||||
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
}
|
||||
|
||||
private function forceStopContainer(string $containerName)
|
||||
{
|
||||
instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
|
||||
}
|
||||
|
||||
private function removeContainer(string $containerName)
|
||||
{
|
||||
instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@ class SettingsBackup extends Component
|
||||
{
|
||||
public InstanceSettings $settings;
|
||||
|
||||
public Server $server;
|
||||
|
||||
public ?StandalonePostgresql $database = null;
|
||||
|
||||
public ScheduledDatabaseBackup|null|array $backup = [];
|
||||
@@ -46,6 +48,7 @@ class SettingsBackup extends Component
|
||||
return redirect()->route('dashboard');
|
||||
} else {
|
||||
$settings = instanceSettings();
|
||||
$this->server = Server::findOrFail(0);
|
||||
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
|
||||
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
|
||||
if ($this->database) {
|
||||
@@ -60,6 +63,10 @@ class SettingsBackup extends Component
|
||||
$this->database->save();
|
||||
}
|
||||
$this->backup = $this->database->scheduledBackups->first();
|
||||
if ($this->backup && ! $this->server->isFunctional()) {
|
||||
$this->backup->enabled = false;
|
||||
$this->backup->save();
|
||||
}
|
||||
$this->executions = $this->backup->executions;
|
||||
}
|
||||
$this->settings = $settings;
|
||||
|
@@ -4,7 +4,7 @@ namespace App\Livewire;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Test;
|
||||
use App\Notifications\TransactionalEmails\Test;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
@@ -225,7 +225,7 @@ class SettingsEmail extends Component
|
||||
'test-email:'.$this->team->id,
|
||||
$perMinute = 0,
|
||||
function () {
|
||||
$this->team?->notify(new Test($this->testEmailAddress, 'email'));
|
||||
$this->team?->notify(new Test($this->testEmailAddress));
|
||||
$this->dispatch('success', 'Test Email sent.');
|
||||
},
|
||||
$decaySeconds = 10,
|
||||
@@ -235,7 +235,7 @@ class SettingsEmail extends Component
|
||||
throw new \Exception('Too many messages sent!');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1068,7 +1068,6 @@ class Application extends BaseModel
|
||||
if ($this->deploymentType() === 'other') {
|
||||
$fullRepoUrl = $customRepository;
|
||||
$base_command = "{$base_command} {$customRepository}";
|
||||
$base_command = $this->setGitImportSettings($deployment_uuid, $base_command, public: true);
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $base_command));
|
||||
@@ -1511,6 +1510,7 @@ class Application extends BaseModel
|
||||
|
||||
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
|
||||
{
|
||||
$dockerfile = str($dockerfile)->trim()->explode("\n");
|
||||
if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) {
|
||||
$healthcheckCommand = null;
|
||||
$lines = $dockerfile->toArray();
|
||||
@@ -1530,27 +1530,24 @@ class Application extends BaseModel
|
||||
}
|
||||
}
|
||||
if (str($healthcheckCommand)->isNotEmpty()) {
|
||||
$interval = str($healthcheckCommand)->match('/--interval=(\d+)/');
|
||||
$timeout = str($healthcheckCommand)->match('/--timeout=(\d+)/');
|
||||
$start_period = str($healthcheckCommand)->match('/--start-period=(\d+)/');
|
||||
$start_interval = str($healthcheckCommand)->match('/--start-interval=(\d+)/');
|
||||
$interval = str($healthcheckCommand)->match('/--interval=([0-9]+[a-zµ]*)/');
|
||||
$timeout = str($healthcheckCommand)->match('/--timeout=([0-9]+[a-zµ]*)/');
|
||||
$start_period = str($healthcheckCommand)->match('/--start-period=([0-9]+[a-zµ]*)/');
|
||||
$retries = str($healthcheckCommand)->match('/--retries=(\d+)/');
|
||||
|
||||
if ($interval->isNotEmpty()) {
|
||||
$this->health_check_interval = $interval->toInteger();
|
||||
$this->health_check_interval = parseDockerfileInterval($interval);
|
||||
}
|
||||
if ($timeout->isNotEmpty()) {
|
||||
$this->health_check_timeout = $timeout->toInteger();
|
||||
$this->health_check_timeout = parseDockerfileInterval($timeout);
|
||||
}
|
||||
if ($start_period->isNotEmpty()) {
|
||||
$this->health_check_start_period = $start_period->toInteger();
|
||||
$this->health_check_start_period = parseDockerfileInterval($start_period);
|
||||
}
|
||||
// if ($start_interval) {
|
||||
// $this->health_check_start_interval = $start_interval->value();
|
||||
// }
|
||||
if ($retries->isNotEmpty()) {
|
||||
$this->health_check_retries = $retries->toInteger();
|
||||
}
|
||||
if ($interval || $timeout || $start_period || $start_interval || $retries) {
|
||||
if ($interval || $timeout || $start_period || $retries) {
|
||||
$this->custom_healthcheck_found = true;
|
||||
$this->save();
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ class DiscordNotificationSettings extends Model
|
||||
'server_disk_usage_discord_notifications',
|
||||
'server_reachable_discord_notifications',
|
||||
'server_unreachable_discord_notifications',
|
||||
'discord_ping_enabled',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -45,6 +46,7 @@ class DiscordNotificationSettings extends Model
|
||||
'server_disk_usage_discord_notifications' => 'boolean',
|
||||
'server_reachable_discord_notifications' => 'boolean',
|
||||
'server_unreachable_discord_notifications' => 'boolean',
|
||||
'discord_ping_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
public function team()
|
||||
@@ -56,4 +58,9 @@ class DiscordNotificationSettings extends Model
|
||||
{
|
||||
return $this->discord_enabled;
|
||||
}
|
||||
|
||||
public function isPingEnabled()
|
||||
{
|
||||
return $this->discord_ping_enabled;
|
||||
}
|
||||
}
|
||||
|
@@ -70,10 +70,6 @@ class EmailNotificationSettings extends Model
|
||||
|
||||
public function isEnabled()
|
||||
{
|
||||
if (isCloud()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->smtp_enabled || $this->resend_enabled || $this->use_instance_email_settings;
|
||||
}
|
||||
}
|
||||
|
@@ -3,16 +3,12 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class InstanceSettings extends Model implements SendsEmail
|
||||
class InstanceSettings extends Model
|
||||
{
|
||||
use Notifiable;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
@@ -92,15 +88,15 @@ class InstanceSettings extends Model implements SendsEmail
|
||||
return InstanceSettings::findOrFail(0);
|
||||
}
|
||||
|
||||
public function getRecipients($notification)
|
||||
{
|
||||
$recipients = data_get($notification, 'emails', null);
|
||||
if (is_null($recipients) || $recipients === '') {
|
||||
return [];
|
||||
}
|
||||
// public function getRecipients($notification)
|
||||
// {
|
||||
// $recipients = data_get($notification, 'emails', null);
|
||||
// if (is_null($recipients) || $recipients === '') {
|
||||
// return [];
|
||||
// }
|
||||
|
||||
return explode(',', $recipients);
|
||||
}
|
||||
// return explode(',', $recipients);
|
||||
// }
|
||||
|
||||
public function getTitleDisplayName(): string
|
||||
{
|
||||
|
@@ -8,6 +8,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class LocalFileVolume extends BaseModel
|
||||
{
|
||||
protected $casts = [
|
||||
// 'fs_path' => 'encrypted',
|
||||
// 'mount_path' => 'encrypted',
|
||||
'content' => 'encrypted',
|
||||
'is_directory' => 'boolean',
|
||||
];
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
@@ -169,4 +176,19 @@ class LocalFileVolume extends BaseModel
|
||||
|
||||
return instant_remote_process($commands, $server);
|
||||
}
|
||||
|
||||
// Accessor for convenient access
|
||||
protected function plainMountPath(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->mount_path,
|
||||
set: fn ($value) => $this->mount_path = $value
|
||||
);
|
||||
}
|
||||
|
||||
// Scope for searching
|
||||
public function scopeWherePlainMountPath($query, $path)
|
||||
{
|
||||
return $query->get()->where('plain_mount_path', $path);
|
||||
}
|
||||
}
|
||||
|
@@ -24,11 +24,6 @@ class LocalPersistentVolume extends Model
|
||||
return $this->morphTo('resource');
|
||||
}
|
||||
|
||||
public function standalone_postgresql()
|
||||
{
|
||||
return $this->morphTo('resource');
|
||||
}
|
||||
|
||||
protected function name(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
@@ -7,9 +7,12 @@ use App\Actions\Server\InstallDocker;
|
||||
use App\Actions\Server\StartSentinel;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ServerReachabilityChanged;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Jobs\CheckAndStartSentinelJob;
|
||||
use App\Jobs\RegenerateSslCertJob;
|
||||
use App\Notifications\Server\Reachable;
|
||||
use App\Notifications\Server\Unreachable;
|
||||
use App\Services\ConfigurationRepository;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -484,7 +487,7 @@ $schema://$host {
|
||||
$base_path = config('constants.coolify.base_config_path');
|
||||
$proxyType = $this->proxyType();
|
||||
$proxy_path = "$base_path/proxy";
|
||||
// TODO: should use /traefik for already exisiting configurations?
|
||||
// TODO: should use /traefik for already existing configurations?
|
||||
// Should move everything except /caddy and /nginx to /traefik
|
||||
// The code needs to be modified as well, so maybe it does not worth it
|
||||
if ($proxyType === ProxyTypes::TRAEFIK->value) {
|
||||
@@ -543,7 +546,7 @@ $schema://$host {
|
||||
$this->settings->save();
|
||||
$sshKeyFileLocation = "id.root@{$this->uuid}";
|
||||
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
|
||||
Storage::disk('ssh-mux')->delete($this->muxFilename());
|
||||
$this->disableSshMux();
|
||||
}
|
||||
|
||||
public function sentinelHeartbeat(bool $isReset = false)
|
||||
@@ -922,7 +925,7 @@ $schema://$host {
|
||||
|
||||
public function isFunctional()
|
||||
{
|
||||
$isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4';
|
||||
$isFunctional = data_get($this->settings, 'is_reachable') && data_get($this->settings, 'is_usable') && data_get($this->settings, 'force_disabled') === false && $this->ip !== '1.2.3.4';
|
||||
|
||||
if ($isFunctional === false) {
|
||||
Storage::disk('ssh-mux')->delete($this->muxFilename());
|
||||
@@ -1103,7 +1106,7 @@ $schema://$host {
|
||||
|
||||
public function validateConnection(bool $justCheckingNewKey = false)
|
||||
{
|
||||
config()->set('constants.ssh.mux_enabled', false);
|
||||
$this->disableSshMux();
|
||||
|
||||
if ($this->skipServer()) {
|
||||
return ['uptime' => false, 'error' => 'Server skipped.'];
|
||||
@@ -1330,4 +1333,47 @@ $schema://$host {
|
||||
$this->databases()->count() == 0 &&
|
||||
$this->services()->count() == 0;
|
||||
}
|
||||
|
||||
private function disableSshMux(): void
|
||||
{
|
||||
$configRepository = app(ConfigurationRepository::class);
|
||||
$configRepository->disableSshMux();
|
||||
}
|
||||
|
||||
public function generateCaCertificate()
|
||||
{
|
||||
try {
|
||||
ray('Generating CA certificate for server', $this->id);
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: 'Coolify CA Certificate',
|
||||
serverId: $this->id,
|
||||
isCaCertificate: true,
|
||||
validityDays: 10 * 365
|
||||
);
|
||||
$caCertificate = SslCertificate::where('server_id', $this->id)->where('is_ca_certificate', true)->first();
|
||||
ray('CA certificate generated', $caCertificate);
|
||||
if ($caCertificate) {
|
||||
$certificateContent = $caCertificate->ssl_certificate;
|
||||
$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 '{$certificateContent}' > $caCertPath/coolify-ca.crt",
|
||||
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||
]);
|
||||
|
||||
instant_remote_process($commands, $this, false);
|
||||
|
||||
dispatch(new RegenerateSslCertJob(
|
||||
server_id: $this->id,
|
||||
force_regeneration: true
|
||||
));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -50,6 +50,11 @@ class Service extends BaseModel
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($service) {
|
||||
if (blank($service->name)) {
|
||||
$service->name = 'service-'.(new Cuid2);
|
||||
}
|
||||
});
|
||||
static::created(function ($service) {
|
||||
$service->compose_parsing_version = self::$parserVersion;
|
||||
$service->save();
|
||||
|
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');
|
||||
}
|
||||
|
||||
public function sslCertificates()
|
||||
{
|
||||
return $this->morphMany(SslCertificate::class, 'resource');
|
||||
}
|
||||
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
@@ -218,7 +223,12 @@ class StandaloneClickhouse extends BaseModel
|
||||
protected function internalDbUrl(): 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(
|
||||
get: function () {
|
||||
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;
|
||||
|
@@ -168,6 +168,11 @@ class StandaloneDragonfly extends BaseModel
|
||||
return data_get($this, 'environment.project.team');
|
||||
}
|
||||
|
||||
public function sslCertificates()
|
||||
{
|
||||
return $this->morphMany(SslCertificate::class, 'resource');
|
||||
}
|
||||
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
@@ -218,7 +223,18 @@ class StandaloneDragonfly extends BaseModel
|
||||
protected function internalDbUrl(): 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(
|
||||
get: function () {
|
||||
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;
|
||||
|
@@ -168,6 +168,11 @@ class StandaloneKeydb extends BaseModel
|
||||
return data_get($this, 'environment.project.team');
|
||||
}
|
||||
|
||||
public function sslCertificates()
|
||||
{
|
||||
return $this->morphMany(SslCertificate::class, 'resource');
|
||||
}
|
||||
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
@@ -218,7 +223,18 @@ class StandaloneKeydb extends BaseModel
|
||||
protected function internalDbUrl(): 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(
|
||||
get: function () {
|
||||
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;
|
||||
|
@@ -218,7 +218,12 @@ class StandaloneMariadb extends BaseModel
|
||||
protected function internalDbUrl(): 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(
|
||||
get: function () {
|
||||
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;
|
||||
@@ -271,6 +279,11 @@ class StandaloneMariadb extends BaseModel
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function sslCertificates()
|
||||
{
|
||||
return $this->morphMany(SslCertificate::class, 'resource');
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
|
@@ -177,6 +177,11 @@ class StandaloneMongodb extends BaseModel
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
}
|
||||
|
||||
public function sslCertificates()
|
||||
{
|
||||
return $this->morphMany(SslCertificate::class, 'resource');
|
||||
}
|
||||
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
@@ -238,7 +243,19 @@ class StandaloneMongodb extends BaseModel
|
||||
protected function internalDbUrl(): 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&tlsCAFile=/etc/mongo/certs/ca.pem';
|
||||
if (in_array($this->ssl_mode, ['verify-full'])) {
|
||||
$url .= '&tlsCertificateKeyFile=/etc/mongo/certs/server.pem';
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -247,7 +264,17 @@ class StandaloneMongodb extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
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&tlsCAFile=/etc/mongo/certs/ca.pem';
|
||||
if (in_array($this->ssl_mode, ['verify-full'])) {
|
||||
$url .= '&tlsCertificateKeyFile=/etc/mongo/certs/server.pem';
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@@ -169,6 +169,11 @@ class StandaloneMysql extends BaseModel
|
||||
return data_get($this, 'environment.project.team');
|
||||
}
|
||||
|
||||
public function sslCertificates()
|
||||
{
|
||||
return $this->morphMany(SslCertificate::class, 'resource');
|
||||
}
|
||||
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
@@ -219,7 +224,19 @@ class StandaloneMysql extends BaseModel
|
||||
protected function internalDbUrl(): 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(
|
||||
get: function () {
|
||||
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;
|
||||
|
@@ -219,7 +219,19 @@ class StandalonePostgresql extends BaseModel
|
||||
protected function internalDbUrl(): 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(
|
||||
get: function () {
|
||||
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;
|
||||
@@ -241,11 +263,21 @@ class StandalonePostgresql extends BaseModel
|
||||
return $this->belongsTo(Environment::class);
|
||||
}
|
||||
|
||||
public function persistentStorages()
|
||||
{
|
||||
return $this->morphMany(LocalPersistentVolume::class, 'resource');
|
||||
}
|
||||
|
||||
public function fileStorages()
|
||||
{
|
||||
return $this->morphMany(LocalFileVolume::class, 'resource');
|
||||
}
|
||||
|
||||
public function sslCertificates()
|
||||
{
|
||||
return $this->morphMany(SslCertificate::class, 'resource');
|
||||
}
|
||||
|
||||
public function destination()
|
||||
{
|
||||
return $this->morphTo();
|
||||
@@ -256,16 +288,17 @@ class StandalonePostgresql extends BaseModel
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
|
||||
public function persistentStorages()
|
||||
{
|
||||
return $this->morphMany(LocalPersistentVolume::class, 'resource');
|
||||
}
|
||||
|
||||
public function scheduledBackups()
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function environment_variables()
|
||||
{
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
|
||||
->orderBy('key', 'asc');
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return true;
|
||||
@@ -314,10 +347,4 @@ class StandalonePostgresql extends BaseModel
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
public function sslCertificates()
|
||||
{
|
||||
return $this->morphMany(SslCertificate::class, 'resource');
|
||||
}
|
||||
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
@@ -222,9 +227,17 @@ class StandaloneRedis extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
$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 () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$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;
|
||||
|
@@ -163,14 +163,17 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
|
||||
];
|
||||
}
|
||||
|
||||
public function getRecipients($notification)
|
||||
public function getRecipients(): array
|
||||
{
|
||||
$recipients = data_get($notification, 'emails', null);
|
||||
if (is_null($recipients)) {
|
||||
return $this->members()->pluck('email')->toArray();
|
||||
$recipients = $this->members()->pluck('email')->toArray();
|
||||
$validatedEmails = array_filter($recipients, function ($email) {
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL);
|
||||
});
|
||||
if (is_null($validatedEmails)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return explode(',', $recipients);
|
||||
return array_values($validatedEmails);
|
||||
}
|
||||
|
||||
public function isAnyNotificationEnabled()
|
||||
|
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
|
||||
use App\Traits\DeletesUserSessions;
|
||||
use DateTimeInterface;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
@@ -37,7 +38,7 @@ use OpenApi\Attributes as OA;
|
||||
)]
|
||||
class User extends Authenticatable implements SendsEmail
|
||||
{
|
||||
use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||
use DeletesUserSessions, HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -57,6 +58,7 @@ class User extends Authenticatable implements SendsEmail
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::created(function (User $user) {
|
||||
$team = [
|
||||
'name' => $user->name."'s Team",
|
||||
@@ -114,9 +116,9 @@ class User extends Authenticatable implements SendsEmail
|
||||
return $this->belongsToMany(Team::class)->withPivot('role');
|
||||
}
|
||||
|
||||
public function getRecipients($notification)
|
||||
public function getRecipients(): array
|
||||
{
|
||||
return $this->email;
|
||||
return [$this->email];
|
||||
}
|
||||
|
||||
public function sendVerificationEmail()
|
||||
|
@@ -20,6 +20,10 @@ class DiscordChannel
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $discordSettings->discord_ping_enabled) {
|
||||
$message->isCritical = false;
|
||||
}
|
||||
|
||||
SendMessageToDiscordJob::dispatch($message, $discordSettings->discord_webhook_url);
|
||||
}
|
||||
}
|
||||
|
@@ -2,89 +2,69 @@
|
||||
|
||||
namespace App\Notifications\Channels;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Mail\Message;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Resend;
|
||||
|
||||
class EmailChannel
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function send(SendsEmail $notifiable, Notification $notification): void
|
||||
{
|
||||
try {
|
||||
$this->bootConfigs($notifiable);
|
||||
$recipients = $notifiable->getRecipients($notification);
|
||||
if (count($recipients) === 0) {
|
||||
throw new Exception('No email recipients found');
|
||||
}
|
||||
|
||||
$mailMessage = $notification->toMail($notifiable);
|
||||
Mail::send(
|
||||
[],
|
||||
[],
|
||||
fn (Message $message) => $message
|
||||
->to($recipients)
|
||||
->subject($mailMessage->subject)
|
||||
->html((string) $mailMessage->render())
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
if ($error === 'No email settings found.') {
|
||||
throw $e;
|
||||
}
|
||||
$message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
|
||||
if (isset($recipients)) {
|
||||
$message .= implode(', ', $recipients);
|
||||
}
|
||||
if (isset($mailMessage)) {
|
||||
$message .= " with subject: {$mailMessage->subject}";
|
||||
}
|
||||
send_internal_notification($message);
|
||||
throw $e;
|
||||
$useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings;
|
||||
$isTransactionalEmail = data_get($notification, 'isTransactionalEmail', false);
|
||||
$customEmails = data_get($notification, 'emails', null);
|
||||
if ($useInstanceEmailSettings || $isTransactionalEmail) {
|
||||
$settings = instanceSettings();
|
||||
} else {
|
||||
$settings = $notifiable->emailNotificationSettings;
|
||||
}
|
||||
}
|
||||
|
||||
private function bootConfigs($notifiable): void
|
||||
{
|
||||
$emailSettings = $notifiable->emailNotificationSettings;
|
||||
|
||||
if ($emailSettings->use_instance_email_settings) {
|
||||
$type = set_transanctional_email_settings();
|
||||
if (blank($type)) {
|
||||
throw new Exception('No email settings found.');
|
||||
}
|
||||
|
||||
return;
|
||||
$isResendEnabled = $settings->resend_enabled;
|
||||
$isSmtpEnabled = $settings->smtp_enabled;
|
||||
if ($customEmails) {
|
||||
$recipients = [$customEmails];
|
||||
} else {
|
||||
$recipients = $notifiable->getRecipients();
|
||||
}
|
||||
$mailMessage = $notification->toMail($notifiable);
|
||||
|
||||
config()->set('mail.from.address', $emailSettings->smtp_from_address ?? 'test@example.com');
|
||||
config()->set('mail.from.name', $emailSettings->smtp_from_name ?? 'Test');
|
||||
|
||||
if ($emailSettings->resend_enabled) {
|
||||
config()->set('mail.default', 'resend');
|
||||
config()->set('resend.api_key', $emailSettings->resend_api_key);
|
||||
}
|
||||
|
||||
if ($emailSettings->smtp_enabled) {
|
||||
$encryption = match (strtolower($emailSettings->smtp_encryption)) {
|
||||
if ($isResendEnabled) {
|
||||
$resend = Resend::client($settings->resend_api_key);
|
||||
$from = "{$settings->smtp_from_name} <{$settings->smtp_from_address}>";
|
||||
$resend->emails->send([
|
||||
'from' => $from,
|
||||
'to' => $recipients,
|
||||
'subject' => $mailMessage->subject,
|
||||
'html' => (string) $mailMessage->render(),
|
||||
]);
|
||||
} elseif ($isSmtpEnabled) {
|
||||
$encryption = match (strtolower($settings->smtp_encryption)) {
|
||||
'starttls' => null,
|
||||
'tls' => 'tls',
|
||||
'none' => null,
|
||||
default => null,
|
||||
};
|
||||
|
||||
config()->set('mail.default', 'smtp');
|
||||
config()->set('mail.mailers.smtp', [
|
||||
'transport' => 'smtp',
|
||||
'host' => $emailSettings->smtp_host,
|
||||
'port' => $emailSettings->smtp_port,
|
||||
'encryption' => $encryption,
|
||||
'username' => $emailSettings->smtp_username,
|
||||
'password' => $emailSettings->smtp_password,
|
||||
'timeout' => $emailSettings->smtp_timeout,
|
||||
'local_domain' => null,
|
||||
'auto_tls' => $emailSettings->smtp_encryption === 'none' ? '0' : '', // If encryption is "none", it will not try to upgrade to TLS via StartTLS to make sure it is unencrypted.
|
||||
]);
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
|
||||
$settings->smtp_host,
|
||||
$settings->smtp_port,
|
||||
$encryption
|
||||
);
|
||||
$transport->setUsername($settings->smtp_username ?? '');
|
||||
$transport->setPassword($settings->smtp_password ?? '');
|
||||
|
||||
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
|
||||
$fromEmail = $settings->smtp_from_address ?? 'noreply@localhost';
|
||||
$fromName = $settings->smtp_from_name ?? 'System';
|
||||
$from = new \Symfony\Component\Mime\Address($fromEmail, $fromName);
|
||||
$email = (new \Symfony\Component\Mime\Email)
|
||||
->from($from)
|
||||
->to(...$recipients)
|
||||
->subject($mailMessage->subject)
|
||||
->html((string) $mailMessage->render());
|
||||
|
||||
$mailer->send($email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,5 +4,5 @@ namespace App\Notifications\Channels;
|
||||
|
||||
interface SendsEmail
|
||||
{
|
||||
public function getRecipients($notification);
|
||||
public function getRecipients(): array;
|
||||
}
|
||||
|
22
app/Notifications/Notification.php
Normal file
22
app/Notifications/Notification.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Notifications;
|
||||
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use App\Notifications\Dto\DiscordMessage;
|
||||
use App\Notifications\Dto\PushoverMessage;
|
||||
use App\Notifications\Dto\SlackMessage;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
interface Notification
|
||||
{
|
||||
public function toMail(SendsEmail $notifiable): MailMessage;
|
||||
|
||||
public function toPushover(): PushoverMessage;
|
||||
|
||||
public function toDiscord(): DiscordMessage;
|
||||
|
||||
public function toSlack(): SlackMessage;
|
||||
|
||||
public function toTelegram();
|
||||
}
|
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()
|
||||
);
|
||||
}
|
||||
}
|
@@ -22,7 +22,7 @@ class Test extends Notification implements ShouldQueue
|
||||
|
||||
public $tries = 5;
|
||||
|
||||
public function __construct(public ?string $emails = null, public ?string $channel = null)
|
||||
public function __construct(public ?string $emails = null, public ?string $channel = null, public ?bool $ping = false)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
@@ -68,6 +68,7 @@ class Test extends Notification implements ShouldQueue
|
||||
title: ':white_check_mark: Test Success',
|
||||
description: 'This is a test Discord notification from Coolify. :cross_mark: :warning: :information_source:',
|
||||
color: DiscordMessage::successColor(),
|
||||
isCritical: $this->ping,
|
||||
);
|
||||
|
||||
$message->addField(name: 'Dashboard', value: '[Link]('.base_url().')', inline: true);
|
||||
@@ -82,7 +83,7 @@ class Test extends Notification implements ShouldQueue
|
||||
'buttons' => [
|
||||
[
|
||||
'text' => 'Go to your dashboard',
|
||||
'url' => base_url(),
|
||||
'url' => isDev() ? 'https://staging-but-dev.coolify.io' : base_url(),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
@@ -16,7 +16,7 @@ class InvitationLink extends CustomEmailNotification
|
||||
return [TransactionalEmailChannel::class];
|
||||
}
|
||||
|
||||
public function __construct(public User $user)
|
||||
public function __construct(public User $user, public bool $isTransactionalEmail = true)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ class ResetPassword extends Notification
|
||||
|
||||
public InstanceSettings $settings;
|
||||
|
||||
public function __construct($token)
|
||||
public function __construct($token, public bool $isTransactionalEmail = true)
|
||||
{
|
||||
$this->settings = instanceSettings();
|
||||
$this->token = $token;
|
||||
|
@@ -8,7 +8,7 @@ use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class Test extends CustomEmailNotification
|
||||
{
|
||||
public function __construct(public string $emails)
|
||||
public function __construct(public string $emails, public bool $isTransactionalEmail = true)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
21
app/Providers/ConfigurationServiceProvider.php
Normal file
21
app/Providers/ConfigurationServiceProvider.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\ConfigurationRepository;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ConfigurationServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(ConfigurationRepository::class, function ($app) {
|
||||
return new ConfigurationRepository($app['config']);
|
||||
});
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
56
app/Services/ConfigurationRepository.php
Normal file
56
app/Services/ConfigurationRepository.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Config\Repository;
|
||||
|
||||
class ConfigurationRepository
|
||||
{
|
||||
private Repository $config;
|
||||
|
||||
public function __construct(Repository $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function updateMailConfig($settings): void
|
||||
{
|
||||
if ($settings->resend_enabled) {
|
||||
$this->config->set('mail.default', 'resend');
|
||||
$this->config->set('mail.from.address', $settings->smtp_from_address ?? 'test@example.com');
|
||||
$this->config->set('mail.from.name', $settings->smtp_from_name ?? 'Test');
|
||||
$this->config->set('resend.api_key', $settings->resend_api_key);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($settings->smtp_enabled) {
|
||||
$encryption = match (strtolower($settings->smtp_encryption)) {
|
||||
'starttls' => null,
|
||||
'tls' => 'tls',
|
||||
'none' => null,
|
||||
default => null,
|
||||
};
|
||||
|
||||
$this->config->set('mail.default', 'smtp');
|
||||
$this->config->set('mail.from.address', $settings->smtp_from_address ?? 'test@example.com');
|
||||
$this->config->set('mail.from.name', $settings->smtp_from_name ?? 'Test');
|
||||
$this->config->set('mail.mailers.smtp', [
|
||||
'transport' => 'smtp',
|
||||
'host' => $settings->smtp_host,
|
||||
'port' => $settings->smtp_port,
|
||||
'encryption' => $encryption,
|
||||
'username' => $settings->smtp_username,
|
||||
'password' => $settings->smtp_password,
|
||||
'timeout' => $settings->smtp_timeout,
|
||||
'local_domain' => null,
|
||||
'auto_tls' => $settings->smtp_encryption === 'none' ? '0' : '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function disableSshMux(): void
|
||||
{
|
||||
$this->config->set('constants.ssh.mux_enabled', false);
|
||||
}
|
||||
}
|
34
app/Traits/DeletesUserSessions.php
Normal file
34
app/Traits/DeletesUserSessions.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use DB;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
trait DeletesUserSessions
|
||||
{
|
||||
/**
|
||||
* Delete all sessions for the current user.
|
||||
* This will force the user to log in again on all devices.
|
||||
*/
|
||||
public function deleteAllSessions(): void
|
||||
{
|
||||
// Invalidate the current session
|
||||
Session::invalidate();
|
||||
Session::regenerateToken();
|
||||
DB::table('sessions')->where('user_id', $this->id)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the trait.
|
||||
*/
|
||||
protected static function bootDeletesUserSessions()
|
||||
{
|
||||
static::updated(function ($user) {
|
||||
// Check if password was changed
|
||||
if ($user->isDirty('password')) {
|
||||
$user->deleteAllSessions();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
63
app/Traits/EnvironmentVariableProtection.php
Normal file
63
app/Traits/EnvironmentVariableProtection.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
trait EnvironmentVariableProtection
|
||||
{
|
||||
/**
|
||||
* Check if an environment variable is protected from deletion
|
||||
*
|
||||
* @param string $key The environment variable key to check
|
||||
* @return bool True if the variable is protected, false otherwise
|
||||
*/
|
||||
protected function isProtectedEnvironmentVariable(string $key): bool
|
||||
{
|
||||
return str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an environment variable is used in Docker Compose
|
||||
*
|
||||
* @param string $key The environment variable key to check
|
||||
* @param string|null $dockerCompose The Docker Compose YAML content
|
||||
* @return array [bool $isUsed, string $reason] Whether the variable is used and the reason if it is
|
||||
*/
|
||||
protected function isEnvironmentVariableUsedInDockerCompose(string $key, ?string $dockerCompose): array
|
||||
{
|
||||
if (empty($dockerCompose)) {
|
||||
return [false, ''];
|
||||
}
|
||||
|
||||
try {
|
||||
$dockerComposeData = Yaml::parse($dockerCompose);
|
||||
$dockerEnvVars = data_get($dockerComposeData, 'services.*.environment');
|
||||
|
||||
foreach ($dockerEnvVars as $serviceEnvs) {
|
||||
if (! is_array($serviceEnvs)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for direct variable usage
|
||||
foreach ($serviceEnvs as $env => $value) {
|
||||
if ($env === $key) {
|
||||
return [true, "Environment variable '{$key}' is used directly in the Docker Compose file."];
|
||||
}
|
||||
}
|
||||
|
||||
// Check for variable references in values
|
||||
foreach ($serviceEnvs as $env => $value) {
|
||||
if (is_string($value) && str_contains($value, '$'.$key)) {
|
||||
return [true, "Environment variable '{$key}' is referenced in the Docker Compose file."];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If there's an error parsing the Docker Compose file, we'll assume it's not used
|
||||
return [false, ''];
|
||||
}
|
||||
|
||||
return [false, ''];
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ trait HasNotificationSettings
|
||||
'server_force_disabled',
|
||||
'general',
|
||||
'test',
|
||||
'ssl_certificate_renewal',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@@ -4,7 +4,6 @@ namespace App\View\Components\Forms;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -19,7 +18,8 @@ class Select extends Component
|
||||
public ?string $label = null,
|
||||
public ?string $helper = null,
|
||||
public bool $required = false,
|
||||
public string $defaultClass = 'select'
|
||||
public bool $disabled = false,
|
||||
public string $defaultClass = 'select w-full'
|
||||
) {
|
||||
//
|
||||
}
|
||||
@@ -36,8 +36,6 @@ class Select extends Component
|
||||
$this->name = $this->id;
|
||||
}
|
||||
|
||||
$this->label = Str::title($this->label);
|
||||
|
||||
return view('components.forms.select');
|
||||
}
|
||||
}
|
||||
|
@@ -16,16 +16,12 @@ use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
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
|
||||
{
|
||||
$destination = StandaloneDocker::where('uuid', $destinationUuid)->firstOrFail();
|
||||
$database = new StandalonePostgresql;
|
||||
$database->name = generate_database_name('postgresql');
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->name = 'postgresql-database-'.$database->uuid;
|
||||
$database->image = $databaseImage;
|
||||
$database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||
$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();
|
||||
$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);
|
||||
$database->environment_id = $environment_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();
|
||||
$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->environment_id = $environment_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();
|
||||
$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_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||
$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();
|
||||
$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_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||
$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();
|
||||
$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->environment_id = $environment_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();
|
||||
$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->environment_id = $environment_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();
|
||||
$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->environment_id = $environment_id;
|
||||
$database->destination_id = $destination->id;
|
||||
|
@@ -8,6 +8,7 @@ use App\Models\ServiceApplication;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection
|
||||
@@ -834,7 +835,15 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
|
||||
if (! $server) {
|
||||
throw new \Exception('Server not found');
|
||||
}
|
||||
$base64_compose = base64_encode($compose);
|
||||
$yaml_compose = Yaml::parse($compose);
|
||||
foreach ($yaml_compose['services'] as $service_name => $service) {
|
||||
foreach ($service['volumes'] as $volume_name => $volume) {
|
||||
if (data_get($volume, 'type') === 'bind' && data_get($volume, 'content')) {
|
||||
unset($yaml_compose['services'][$service_name]['volumes'][$volume_name]['content']);
|
||||
}
|
||||
}
|
||||
}
|
||||
$base64_compose = base64_encode(Yaml::dump($yaml_compose));
|
||||
instant_remote_process([
|
||||
"echo {$base64_compose} | base64 -d | tee /tmp/{$uuid}.yml > /dev/null",
|
||||
"chmod 600 /tmp/{$uuid}.yml",
|
||||
|
@@ -129,3 +129,27 @@ function getPermissionsPath(GithubApp $source)
|
||||
|
||||
return "$github->html_url/settings/apps/$name/permissions";
|
||||
}
|
||||
|
||||
function loadRepositoryByPage(GithubApp $source, string $token, int $page)
|
||||
{
|
||||
$response = Http::withToken($token)->get("{$source->api_url}/installation/repositories?per_page=100&page={$page}");
|
||||
$json = $response->json();
|
||||
if ($response->status() !== 200) {
|
||||
return [
|
||||
'total_count' => 0,
|
||||
'repositories' => [],
|
||||
];
|
||||
}
|
||||
|
||||
if ($json['total_count'] === 0) {
|
||||
return [
|
||||
'total_count' => 0,
|
||||
'repositories' => [],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'total_count' => $json['total_count'],
|
||||
'repositories' => $json['repositories'],
|
||||
];
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Internal\GeneralNotification;
|
||||
use Illuminate\Mail\Message;
|
||||
@@ -54,7 +53,7 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null
|
||||
}
|
||||
}
|
||||
|
||||
function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string // returns null|resend|smtp and defaults to array based on mail.php config
|
||||
function set_transanctional_email_settings($settings = null)
|
||||
{
|
||||
if (! $settings) {
|
||||
$settings = instanceSettings();
|
||||
@@ -63,38 +62,16 @@ function set_transanctional_email_settings(?InstanceSettings $settings = null):
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data_get($settings, 'resend_enabled')) {
|
||||
config()->set('mail.default', 'resend');
|
||||
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
|
||||
config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
|
||||
config()->set('resend.api_key', data_get($settings, 'resend_api_key'));
|
||||
$configRepository = app('App\Services\ConfigurationRepository'::class);
|
||||
$configRepository->updateMailConfig($settings);
|
||||
|
||||
if (data_get($settings, 'resend_enabled')) {
|
||||
return 'resend';
|
||||
}
|
||||
|
||||
$encryption = match (strtolower(data_get($settings, 'smtp_encryption'))) {
|
||||
'starttls' => null,
|
||||
'tls' => 'tls',
|
||||
'none' => null,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (data_get($settings, 'smtp_enabled')) {
|
||||
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
|
||||
config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
|
||||
config()->set('mail.default', 'smtp');
|
||||
config()->set('mail.mailers.smtp', [
|
||||
'transport' => 'smtp',
|
||||
'host' => data_get($settings, 'smtp_host'),
|
||||
'port' => data_get($settings, 'smtp_port'),
|
||||
'encryption' => $encryption,
|
||||
'username' => data_get($settings, 'smtp_username'),
|
||||
'password' => data_get($settings, 'smtp_password'),
|
||||
'timeout' => data_get($settings, 'smtp_timeout'),
|
||||
'local_domain' => null,
|
||||
'auto_tls' => data_get($settings, 'smtp_encryption') === 'none' ? '0' : '', // If encryption is "none", it will not try to upgrade to TLS via StartTLS to make sure it is unencrypted.
|
||||
]);
|
||||
|
||||
return 'smtp';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@@ -1250,13 +1250,23 @@ function get_public_ips()
|
||||
function isAnyDeploymentInprogress()
|
||||
{
|
||||
$runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get();
|
||||
$basicDetails = $runningJobs->map(function ($job) {
|
||||
return [
|
||||
'id' => $job->id,
|
||||
'created_at' => $job->created_at,
|
||||
'application_id' => $job->application_id,
|
||||
'server_id' => $job->server_id,
|
||||
'horizon_job_id' => $job->horizon_job_id,
|
||||
'status' => $job->status,
|
||||
];
|
||||
});
|
||||
echo 'Running jobs: '.json_encode($basicDetails)."\n";
|
||||
$horizonJobIds = [];
|
||||
foreach ($runningJobs as $runningJob) {
|
||||
$horizonJobStatus = getJobStatus($runningJob->horizon_job_id);
|
||||
if ($horizonJobStatus === 'unknown') {
|
||||
return true;
|
||||
if ($horizonJobStatus === 'unknown' || $horizonJobStatus === 'reserved') {
|
||||
$horizonJobIds[] = $runningJob->horizon_job_id;
|
||||
}
|
||||
$horizonJobIds[] = $runningJob->horizon_job_id;
|
||||
}
|
||||
if (count($horizonJobIds) === 0) {
|
||||
echo "No deployments in progress.\n";
|
||||
@@ -1665,6 +1675,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
|
||||
return $volume;
|
||||
}
|
||||
|
||||
LocalFileVolume::updateOrCreate(
|
||||
[
|
||||
'mount_path' => $target,
|
||||
@@ -3164,6 +3175,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
||||
}
|
||||
}
|
||||
|
||||
$serviceAppsLogDrainEnabledMap = collect([]);
|
||||
if ($resource instanceof Service) {
|
||||
$serviceAppsLogDrainEnabledMap = $resource->applications()->get()->keyBy('name')->map(function ($app) {
|
||||
return $app->isLogDrainEnabled();
|
||||
});
|
||||
}
|
||||
|
||||
// Parse the rest of the services
|
||||
foreach ($services as $serviceName => $service) {
|
||||
$image = data_get_str($service, 'image');
|
||||
@@ -3174,6 +3192,9 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
||||
if ($resource instanceof Application && $resource->isLogDrainEnabled()) {
|
||||
$logging = generate_fluentd_configuration();
|
||||
}
|
||||
if ($resource instanceof Service && $serviceAppsLogDrainEnabledMap->get($serviceName)) {
|
||||
$logging = generate_fluentd_configuration();
|
||||
}
|
||||
}
|
||||
$volumes = collect(data_get($service, 'volumes', []));
|
||||
$networks = collect(data_get($service, 'networks', []));
|
||||
@@ -4050,9 +4071,35 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla
|
||||
return $rateLimited;
|
||||
}
|
||||
|
||||
function defaultNginxConfiguration(): string
|
||||
function defaultNginxConfiguration(string $type = 'static'): string
|
||||
{
|
||||
return 'server {
|
||||
if ($type === 'spa') {
|
||||
return <<<'NGINX'
|
||||
server {
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Handle 404 errors
|
||||
error_page 404 /404.html;
|
||||
location = /404.html {
|
||||
root /usr/share/nginx/html;
|
||||
internal;
|
||||
}
|
||||
|
||||
# Handle server errors (50x)
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
internal;
|
||||
}
|
||||
}
|
||||
NGINX;
|
||||
} else {
|
||||
return <<<'NGINX'
|
||||
server {
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
@@ -4072,7 +4119,9 @@ function defaultNginxConfiguration(): string
|
||||
root /usr/share/nginx/html;
|
||||
internal;
|
||||
}
|
||||
}';
|
||||
}
|
||||
NGINX;
|
||||
}
|
||||
}
|
||||
|
||||
function convertGitUrl(string $gitRepository, string $deploymentType, ?GithubApp $source = null): array
|
||||
@@ -4137,3 +4186,35 @@ function getJobStatus(?string $jobId = null)
|
||||
|
||||
return $jobFound->first()->status;
|
||||
}
|
||||
|
||||
function parseDockerfileInterval(string $something)
|
||||
{
|
||||
$value = preg_replace('/[^0-9]/', '', $something);
|
||||
$unit = preg_replace('/[0-9]/', '', $something);
|
||||
|
||||
// Default to seconds if no unit specified
|
||||
$unit = $unit ?: 's';
|
||||
|
||||
// Convert to seconds based on unit
|
||||
$seconds = (int) $value;
|
||||
switch ($unit) {
|
||||
case 'ns':
|
||||
$seconds = (int) ($value / 1000000000);
|
||||
break;
|
||||
case 'us':
|
||||
case 'µs':
|
||||
$seconds = (int) ($value / 1000000);
|
||||
break;
|
||||
case 'ms':
|
||||
$seconds = (int) ($value / 1000);
|
||||
break;
|
||||
case 'm':
|
||||
$seconds = (int) ($value * 60);
|
||||
break;
|
||||
case 'h':
|
||||
$seconds = (int) ($value * 3600);
|
||||
break;
|
||||
}
|
||||
|
||||
return $seconds;
|
||||
}
|
||||
|
102
composer.json
102
composer.json
@@ -12,65 +12,63 @@
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"3sidedcube/laravel-redoc": "^1.0",
|
||||
"danharrin/livewire-rate-limiting": "2.0.0",
|
||||
"doctrine/dbal": "^4.2",
|
||||
"guzzlehttp/guzzle": "^7.5.0",
|
||||
"laravel/fortify": "^1.16.0",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/horizon": "^5.29.1",
|
||||
"laravel/pail": "^1.1",
|
||||
"laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/socialite": "^5.14.0",
|
||||
"laravel/tinker": "^2.8.1",
|
||||
"laravel/ui": "^4.2",
|
||||
"lcobucci/jwt": "^5.0.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"league/flysystem-sftp-v3": "^3.0",
|
||||
"livewire/livewire": "^3.5",
|
||||
"log1x/laravel-webfonts": "^1.0",
|
||||
"lorisleiva/laravel-actions": "^2.8",
|
||||
"danharrin/livewire-rate-limiting": "2.1.0",
|
||||
"doctrine/dbal": "^4.2.2",
|
||||
"guzzlehttp/guzzle": "^7.9.2",
|
||||
"laravel/fortify": "^1.25.4",
|
||||
"laravel/framework": "12.4.1",
|
||||
"laravel/horizon": "^5.30.3",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/prompts": "^0.3.5|^0.3.5|^0.3.5",
|
||||
"laravel/sanctum": "^4.0.8",
|
||||
"laravel/socialite": "^5.18.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"laravel/ui": "^4.6.1",
|
||||
"lcobucci/jwt": "^5.5.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.29",
|
||||
"league/flysystem-sftp-v3": "^3.29",
|
||||
"livewire/livewire": "^3.5.20",
|
||||
"log1x/laravel-webfonts": "^2.0.1",
|
||||
"lorisleiva/laravel-actions": "^2.8.6",
|
||||
"nubs/random-name-generator": "^2.2",
|
||||
"phpseclib/phpseclib": "^3.0",
|
||||
"pion/laravel-chunk-upload": "^1.5",
|
||||
"poliander/cron": "^3.0",
|
||||
"purplepixie/phpdns": "^2.1",
|
||||
"pusher/pusher-php-server": "^7.2",
|
||||
"resend/resend-laravel": "^0.15.0",
|
||||
"sentry/sentry-laravel": "^4.6",
|
||||
"phpseclib/phpseclib": "^3.0.43",
|
||||
"pion/laravel-chunk-upload": "^1.5.4",
|
||||
"poliander/cron": "^3.2.1",
|
||||
"purplepixie/phpdns": "^2.2",
|
||||
"pusher/pusher-php-server": "^7.2.7",
|
||||
"resend/resend-laravel": "^0.17.0",
|
||||
"sentry/sentry-laravel": "^4.13",
|
||||
"socialiteproviders/authentik": "^5.2",
|
||||
"socialiteproviders/google": "^4.1",
|
||||
"socialiteproviders/infomaniak": "^4.0",
|
||||
"socialiteproviders/microsoft-azure": "^5.1",
|
||||
"spatie/laravel-activitylog": "^4.7.3",
|
||||
"spatie/laravel-data": "^4.11",
|
||||
"spatie/laravel-ray": "^1.37",
|
||||
"spatie/laravel-schemaless-attributes": "^2.4",
|
||||
"spatie/url": "^2.2",
|
||||
"stevebauman/purify": "^6.2",
|
||||
"stripe/stripe-php": "^16.2.0",
|
||||
"symfony/yaml": "^7.1.6",
|
||||
"socialiteproviders/microsoft-azure": "^5.2",
|
||||
"spatie/laravel-activitylog": "^4.10.1",
|
||||
"spatie/laravel-data": "^4.13.1",
|
||||
"spatie/laravel-ray": "^1.39.1",
|
||||
"spatie/laravel-schemaless-attributes": "^2.5.1",
|
||||
"spatie/url": "^2.4",
|
||||
"stevebauman/purify": "^6.3",
|
||||
"stripe/stripe-php": "^16.5.1",
|
||||
"symfony/yaml": "^7.2.3",
|
||||
"visus/cuid2": "^4.1.0",
|
||||
"yosymfony/toml": "^1.0",
|
||||
"zircote/swagger-php": "^5.0"
|
||||
"yosymfony/toml": "^1.0.4",
|
||||
"zircote/swagger-php": "^5.0.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.13",
|
||||
"driftingly/rector-laravel": "^2.0",
|
||||
"fakerphp/faker": "^1.21.0",
|
||||
"laravel/dusk": "^8.0",
|
||||
"laravel/pint": "^1.16",
|
||||
"laravel/telescope": "^5.2",
|
||||
"mockery/mockery": "^1.5.1",
|
||||
"nunomaduro/collision": "^8.1",
|
||||
"pestphp/pest": "^3.5",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpunit/phpunit": "^11.5",
|
||||
"rector/rector": "^2.0",
|
||||
"serversideup/spin": "^3.0",
|
||||
"spatie/laravel-ignition": "^2.1.0",
|
||||
"symfony/http-client": "^7.1"
|
||||
"barryvdh/laravel-debugbar": "^3.15.1",
|
||||
"driftingly/rector-laravel": "^2.0.2",
|
||||
"fakerphp/faker": "^1.24.1",
|
||||
"laravel/dusk": "^8.3.1",
|
||||
"laravel/pint": "^1.21",
|
||||
"laravel/telescope": "^5.5",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/collision": "^8.6.1",
|
||||
"pestphp/pest": "^3.8.0",
|
||||
"phpstan/phpstan": "^2.1.6",
|
||||
"rector/rector": "^2.0.9",
|
||||
"serversideup/spin": "^3.0.2",
|
||||
"spatie/laravel-ignition": "^2.9.1",
|
||||
"symfony/http-client": "^7.2.3"
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
|
919
composer.lock
generated
919
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -199,6 +199,7 @@ return [
|
||||
App\Providers\EventServiceProvider::class,
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\ConfigurationServiceProvider::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|
@@ -2,13 +2,14 @@
|
||||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.399',
|
||||
'helper_version' => '1.0.7',
|
||||
'version' => '4.0.0-beta.407',
|
||||
'helper_version' => '1.0.8',
|
||||
'realtime_version' => '1.0.6',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
'autoupdate' => env('AUTOUPDATE'),
|
||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'),
|
||||
'registry_url' => env('REGISTRY_URL', 'ghcr.io'),
|
||||
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'),
|
||||
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
|
||||
],
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user