Merge branch 'next' into feat-service-neon
This commit is contained in:
@@ -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 |
|
||||
|
@@ -22,74 +22,27 @@ class StartDatabaseProxy
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
|
||||
{
|
||||
$internalPort = null;
|
||||
$type = $database->getMorphClass();
|
||||
$databaseType = $database->database_type;
|
||||
$network = data_get($database, 'destination.network');
|
||||
$server = data_get($database, 'destination.server');
|
||||
$containerName = data_get($database, 'uuid');
|
||||
$proxyContainerName = "{$database->uuid}-proxy";
|
||||
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
|
||||
$databaseType = $database->databaseType();
|
||||
// $connectPredefined = data_get($database, 'service.connect_to_docker_network');
|
||||
$network = $database->service->uuid;
|
||||
$server = data_get($database, 'service.destination.server');
|
||||
$proxyContainerName = "{$database->service->uuid}-proxy";
|
||||
switch ($databaseType) {
|
||||
case 'standalone-mariadb':
|
||||
$type = \App\Models\StandaloneMariadb::class;
|
||||
$containerName = "mariadb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
$type = \App\Models\StandaloneMongodb::class;
|
||||
$containerName = "mongodb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
$type = \App\Models\StandaloneMysql::class;
|
||||
$containerName = "mysql-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
$type = \App\Models\StandalonePostgresql::class;
|
||||
$containerName = "postgresql-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
$type = \App\Models\StandaloneRedis::class;
|
||||
$containerName = "redis-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-keydb':
|
||||
$type = \App\Models\StandaloneKeydb::class;
|
||||
$containerName = "keydb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-dragonfly':
|
||||
$type = \App\Models\StandaloneDragonfly::class;
|
||||
$containerName = "dragonfly-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-clickhouse':
|
||||
$type = \App\Models\StandaloneClickhouse::class;
|
||||
$containerName = "clickhouse-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-supabase/postgres':
|
||||
$type = \App\Models\StandalonePostgresql::class;
|
||||
$containerName = "supabase-db-{$database->service->uuid}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($type === \App\Models\StandaloneRedis::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === \App\Models\StandalonePostgresql::class) {
|
||||
$internalPort = 5432;
|
||||
} elseif ($type === \App\Models\StandaloneMongodb::class) {
|
||||
$internalPort = 27017;
|
||||
} elseif ($type === \App\Models\StandaloneMysql::class) {
|
||||
$internalPort = 3306;
|
||||
} elseif ($type === \App\Models\StandaloneMariadb::class) {
|
||||
$internalPort = 3306;
|
||||
} elseif ($type === \App\Models\StandaloneKeydb::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === \App\Models\StandaloneDragonfly::class) {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === \App\Models\StandaloneClickhouse::class) {
|
||||
$internalPort = 9000;
|
||||
$containerName = "{$database->name}-{$database->service->uuid}";
|
||||
}
|
||||
$internalPort = match ($databaseType) {
|
||||
'standalone-mariadb', 'standalone-mysql' => 3306,
|
||||
'standalone-postgresql', 'standalone-supabase/postgres' => 5432,
|
||||
'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6379,
|
||||
'standalone-clickhouse' => 9000,
|
||||
'standalone-mongodb' => 27017,
|
||||
default => throw new \Exception("Unsupported database type: $databaseType"),
|
||||
};
|
||||
|
||||
$configuration_dir = database_proxy_dir($database->uuid);
|
||||
$nginxconf = <<<EOF
|
||||
user nginx;
|
||||
|
@@ -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,70 @@ 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();
|
||||
|
||||
$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 +118,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 +178,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,73 @@ 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();
|
||||
|
||||
$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 +121,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 +194,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 +262,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,53 @@ 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();
|
||||
|
||||
$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 +115,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'] = [
|
||||
'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);
|
||||
@@ -109,6 +200,9 @@ class StartMariadb
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml 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,59 @@ 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();
|
||||
|
||||
$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 +126,118 @@ 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,
|
||||
]]
|
||||
);
|
||||
}
|
||||
|
||||
$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 +246,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,53 @@ 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();
|
||||
|
||||
$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 +115,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 +200,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,54 @@ 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();
|
||||
|
||||
$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 +125,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 +215,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,51 @@ 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();
|
||||
|
||||
$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 +122,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 +191,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 +280,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);
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1310,7 +1310,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);
|
||||
|
@@ -152,7 +152,7 @@ class Gitea extends Controller
|
||||
}
|
||||
}
|
||||
if ($x_gitea_event === 'pull_request') {
|
||||
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
|
||||
if ($action === 'opened' || $action === 'synchronized' || $action === 'reopened') {
|
||||
if ($application->isPRDeployable()) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
|
@@ -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);
|
||||
|
73
app/Jobs/RegenerateSslCertJob.php
Normal file
73
app/Jobs/RegenerateSslCertJob.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Helpers\SSLHelper;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\SslExpirationNotification;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RegenerateSslCertJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 3;
|
||||
|
||||
public $backoff = 60;
|
||||
|
||||
public function __construct(
|
||||
protected ?Team $team = null,
|
||||
protected ?int $server_id = null,
|
||||
protected bool $force_regeneration = false,
|
||||
) {}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$query = SslCertificate::query();
|
||||
|
||||
if ($this->server_id) {
|
||||
$query->where('server_id', $this->server_id);
|
||||
}
|
||||
|
||||
if (! $this->force_regeneration) {
|
||||
$query->where('valid_until', '<=', now()->addDays(14));
|
||||
}
|
||||
|
||||
$query->where('is_ca_certificate', false);
|
||||
|
||||
$regenerated = collect();
|
||||
|
||||
$query->cursor()->each(function ($certificate) use ($regenerated) {
|
||||
try {
|
||||
$caCert = SslCertificate::where('server_id', $certificate->server_id)
|
||||
->where('is_ca_certificate', true)
|
||||
->first();
|
||||
|
||||
SSLHelper::generateSslCertificate(
|
||||
commonName: $certificate->common_name,
|
||||
subjectAlternativeNames: $certificate->subject_alternative_names,
|
||||
resourceType: $certificate->resource_type,
|
||||
resourceId: $certificate->resource_id,
|
||||
serverId: $certificate->server_id,
|
||||
configurationDir: $certificate->configuration_dir,
|
||||
mountPath: $certificate->mount_path,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
);
|
||||
$regenerated->push($certificate);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to regenerate SSL certificate: '.$e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
if ($regenerated->isNotEmpty()) {
|
||||
$this->team?->notify(new SslExpirationNotification($regenerated));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
|
@@ -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,6 +53,11 @@ 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()
|
||||
{
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
@@ -64,6 +72,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 +96,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 +111,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 +190,47 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveSSL()
|
||||
{
|
||||
try {
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function regenerateSslCertificate()
|
||||
{
|
||||
try {
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $existingCert) {
|
||||
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)
|
||||
->where('is_ca_certificate', true)
|
||||
->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->commonName,
|
||||
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
|
||||
resourceType: $existingCert->resource_type,
|
||||
resourceId: $existingCert->resource_id,
|
||||
serverId: $existingCert->server_id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $existingCert->configuration_dir,
|
||||
mountPath: $existingCert->mount_path,
|
||||
);
|
||||
|
||||
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
|
||||
} catch (Exception $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Keydb;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\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,6 +56,11 @@ 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()
|
||||
{
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
@@ -67,6 +75,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 +100,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 +116,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 +195,47 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveSSL()
|
||||
{
|
||||
try {
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function regenerateSslCertificate()
|
||||
{
|
||||
try {
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $existingCert) {
|
||||
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)
|
||||
->where('is_ca_certificate', true)
|
||||
->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->commonName,
|
||||
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
|
||||
resourceType: $existingCert->resource_type,
|
||||
resourceId: $existingCert->resource_id,
|
||||
serverId: $existingCert->server_id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $existingCert->configuration_dir,
|
||||
mountPath: $existingCert->mount_path,
|
||||
);
|
||||
|
||||
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
|
||||
} catch (Exception $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Mariadb;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -21,6 +24,8 @@ class General extends Component
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
'database.description' => 'nullable',
|
||||
@@ -35,6 +40,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 +56,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 +64,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 +140,47 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveSSL()
|
||||
{
|
||||
try {
|
||||
$this->database->save();
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function regenerateSslCertificate()
|
||||
{
|
||||
try {
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $existingCert) {
|
||||
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
resourceType: $existingCert->resource_type,
|
||||
resourceId: $existingCert->resource_id,
|
||||
serverId: $existingCert->server_id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $existingCert->configuration_dir,
|
||||
mountPath: $existingCert->mount_path,
|
||||
);
|
||||
|
||||
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
|
@@ -4,8 +4,11 @@ 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 Livewire\Component;
|
||||
|
||||
@@ -21,6 +24,8 @@ class General extends Component
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
'database.description' => 'nullable',
|
||||
@@ -34,6 +39,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 +55,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 +64,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 +143,52 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedDatabaseSslMode()
|
||||
{
|
||||
$this->instantSaveSSL();
|
||||
}
|
||||
|
||||
public function instantSaveSSL()
|
||||
{
|
||||
try {
|
||||
$this->database->save();
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function regenerateSslCertificate()
|
||||
{
|
||||
try {
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $existingCert) {
|
||||
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
resourceType: $existingCert->resource_type,
|
||||
resourceId: $existingCert->resource_id,
|
||||
serverId: $existingCert->server_id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $existingCert->configuration_dir,
|
||||
mountPath: $existingCert->mount_path,
|
||||
);
|
||||
|
||||
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
|
@@ -4,8 +4,11 @@ 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 Livewire\Component;
|
||||
|
||||
@@ -21,6 +24,8 @@ class General extends Component
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
'database.description' => 'nullable',
|
||||
@@ -35,6 +40,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 +57,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 +66,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 +142,52 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedDatabaseSslMode()
|
||||
{
|
||||
$this->instantSaveSSL();
|
||||
}
|
||||
|
||||
public function instantSaveSSL()
|
||||
{
|
||||
try {
|
||||
$this->database->save();
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function regenerateSslCertificate()
|
||||
{
|
||||
try {
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $existingCert) {
|
||||
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
resourceType: $existingCert->resource_type,
|
||||
resourceId: $existingCert->resource_id,
|
||||
serverId: $existingCert->server_id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $existingCert->configuration_dir,
|
||||
mountPath: $existingCert->mount_path,
|
||||
);
|
||||
|
||||
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
|
@@ -4,8 +4,11 @@ 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 Livewire\Component;
|
||||
|
||||
@@ -23,6 +26,8 @@ class General extends Component
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
return [
|
||||
@@ -48,6 +53,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 +72,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 +81,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 +106,52 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedDatabaseSslMode()
|
||||
{
|
||||
$this->instantSaveSSL();
|
||||
}
|
||||
|
||||
public function instantSaveSSL()
|
||||
{
|
||||
try {
|
||||
$this->database->save();
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function regenerateSslCertificate()
|
||||
{
|
||||
try {
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $existingCert) {
|
||||
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
resourceType: $existingCert->resource_type,
|
||||
resourceId: $existingCert->resource_id,
|
||||
serverId: $existingCert->server_id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $existingCert->configuration_dir,
|
||||
mountPath: $existingCert->mount_path,
|
||||
);
|
||||
|
||||
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
|
@@ -4,8 +4,11 @@ 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 Livewire\Component;
|
||||
|
||||
@@ -30,6 +33,8 @@ class General extends Component
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
protected $rules = [
|
||||
'database.name' => 'required',
|
||||
'database.description' => 'nullable',
|
||||
@@ -42,6 +47,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 +61,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 +148,47 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveSSL()
|
||||
{
|
||||
try {
|
||||
$this->database->save();
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function regenerateSslCertificate()
|
||||
{
|
||||
try {
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if (! $existingCert) {
|
||||
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->commonName,
|
||||
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
|
||||
resourceType: $existingCert->resource_type,
|
||||
resourceId: $existingCert->resource_id,
|
||||
serverId: $existingCert->server_id,
|
||||
caCert: $caCert->ssl_certificate,
|
||||
caKey: $caCert->ssl_private_key,
|
||||
configurationDir: $existingCert->configuration_dir,
|
||||
mountPath: $existingCert->mount_path,
|
||||
);
|
||||
|
||||
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
|
||||
} catch (Exception $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
|
@@ -4,7 +4,10 @@ namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Database extends Component
|
||||
@@ -15,6 +18,8 @@ class Database extends Component
|
||||
|
||||
public $fileStorages;
|
||||
|
||||
public $parameters;
|
||||
|
||||
protected $listeners = ['refreshFileStorages'];
|
||||
|
||||
protected $rules = [
|
||||
@@ -34,12 +39,33 @@ class Database extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
if ($this->database->is_public) {
|
||||
$this->db_url_public = $this->database->getServiceDatabaseUrl();
|
||||
}
|
||||
$this->refreshFileStorages();
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->database->delete();
|
||||
$this->dispatch('success', 'Database deleted.');
|
||||
|
||||
return redirect()->route('project.service.configuration', $this->parameters);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveExclude()
|
||||
{
|
||||
$this->submit();
|
||||
|
@@ -43,12 +43,11 @@ class EditDomain extends Component
|
||||
updateCompose($this->application);
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
} else {
|
||||
! $warning && $this->dispatch('success', 'Service saved.');
|
||||
}
|
||||
$this->application->service->parse();
|
||||
$this->dispatch('refresh');
|
||||
$this->dispatch('configurationChanged');
|
||||
$this->dispatch('refreshStatus');
|
||||
} catch (\Throwable $e) {
|
||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
|
@@ -75,7 +75,7 @@ class Add extends Component
|
||||
public function saveScheduledTask()
|
||||
{
|
||||
try {
|
||||
$task = new ScheduledTask();
|
||||
$task = new ScheduledTask;
|
||||
$task->name = $this->name;
|
||||
$task->command = $this->command;
|
||||
$task->frequency = $this->frequency;
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared\ScheduledTask;
|
||||
|
||||
use App\Models\ScheduledTask;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\On;
|
||||
@@ -42,5 +41,4 @@ class All extends Component
|
||||
{
|
||||
$this->resource->refresh();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -114,19 +114,24 @@ class SettingsEmail extends Component
|
||||
public function instantSave(string $type)
|
||||
{
|
||||
try {
|
||||
$currentSmtpEnabled = $this->settings->smtp_enabled;
|
||||
$currentResendEnabled = $this->settings->resend_enabled;
|
||||
$this->resetErrorBag();
|
||||
|
||||
if ($type === 'SMTP') {
|
||||
$this->submitSmtp();
|
||||
$this->resendEnabled = $this->settings->resend_enabled = false;
|
||||
} elseif ($type === 'Resend') {
|
||||
$this->submitResend();
|
||||
$this->smtpEnabled = $this->settings->smtp_enabled = false;
|
||||
}
|
||||
$this->settings->save();
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
if ($type === 'SMTP') {
|
||||
$this->smtpEnabled = false;
|
||||
$this->smtpEnabled = $currentSmtpEnabled;
|
||||
} elseif ($type === 'Resend') {
|
||||
$this->resendEnabled = false;
|
||||
$this->resendEnabled = $currentResendEnabled;
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
@@ -156,9 +161,6 @@ class SettingsEmail extends Component
|
||||
'smtpEncryption.required' => 'Encryption type is required.',
|
||||
]);
|
||||
|
||||
$this->resendEnabled = false;
|
||||
$this->settings->resend_enabled = false;
|
||||
|
||||
$this->settings->smtp_enabled = $this->smtpEnabled;
|
||||
$this->settings->smtp_host = $this->smtpHost;
|
||||
$this->settings->smtp_port = $this->smtpPort;
|
||||
@@ -194,9 +196,6 @@ class SettingsEmail extends Component
|
||||
'smtpFromName.required' => 'From Name is required.',
|
||||
]);
|
||||
|
||||
$this->smtpEnabled = false;
|
||||
$this->settings->smtp_enabled = false;
|
||||
|
||||
$this->settings->resend_enabled = $this->resendEnabled;
|
||||
$this->settings->resend_api_key = $this->resendApiKey;
|
||||
$this->settings->smtp_from_address = $this->smtpFromAddress;
|
||||
|
@@ -37,6 +37,8 @@ class Change extends Component
|
||||
|
||||
public $applications;
|
||||
|
||||
public $privateKeys;
|
||||
|
||||
protected $rules = [
|
||||
'github_app.name' => 'required|string',
|
||||
'github_app.organization' => 'nullable|string',
|
||||
@@ -54,6 +56,7 @@ class Change extends Component
|
||||
'github_app.metadata' => 'nullable|string',
|
||||
'github_app.pull_requests' => 'nullable|string',
|
||||
'github_app.administration' => 'nullable|string',
|
||||
'github_app.private_key_id' => 'required|int',
|
||||
];
|
||||
|
||||
public function boot()
|
||||
@@ -65,9 +68,13 @@ class Change extends Component
|
||||
|
||||
public function checkPermissions()
|
||||
{
|
||||
GithubAppPermissionJob::dispatchSync($this->github_app);
|
||||
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
$this->dispatch('success', 'Github App permissions updated.');
|
||||
try {
|
||||
GithubAppPermissionJob::dispatchSync($this->github_app);
|
||||
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
$this->dispatch('success', 'Github App permissions updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
// public function check()
|
||||
@@ -109,6 +116,7 @@ class Change extends Component
|
||||
$github_app_uuid = request()->github_app_uuid;
|
||||
$this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
|
||||
$this->github_app->makeVisible(['client_secret', 'webhook_secret']);
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
|
||||
$this->applications = $this->github_app->applications;
|
||||
$settings = instanceSettings();
|
||||
@@ -243,6 +251,7 @@ class Change extends Component
|
||||
'github_app.client_secret' => 'required|string',
|
||||
'github_app.webhook_secret' => 'required|string',
|
||||
'github_app.is_system_wide' => 'required|bool',
|
||||
'github_app.private_key_id' => 'required|int',
|
||||
]);
|
||||
$this->github_app->save();
|
||||
$this->dispatch('success', 'Github App updated.');
|
||||
@@ -251,6 +260,15 @@ class Change extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function createGithubAppManually()
|
||||
{
|
||||
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
$this->github_app->app_id = '1234567890';
|
||||
$this->github_app->installation_id = '1234567890';
|
||||
$this->github_app->save();
|
||||
$this->dispatch('success', 'Github App updated.');
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
|
@@ -1065,7 +1065,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));
|
||||
|
@@ -3,14 +3,23 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Events\FileStorageChanged;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class LocalFileVolume extends BaseModel
|
||||
{
|
||||
protected $casts = [
|
||||
'fs_path' => 'encrypted',
|
||||
'mount_path' => 'encrypted',
|
||||
'content' => 'encrypted',
|
||||
];
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public $appends = ['is_binary'];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function (LocalFileVolume $fileVolume) {
|
||||
@@ -19,6 +28,15 @@ class LocalFileVolume extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
protected function isBinary(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->content === '[binary file]';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function service()
|
||||
{
|
||||
return $this->morphTo('resource');
|
||||
@@ -44,6 +62,10 @@ class LocalFileVolume extends BaseModel
|
||||
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
|
||||
if ($isFile === 'OK') {
|
||||
$content = instant_remote_process(["cat $path"], $server, false);
|
||||
// Check if content contains binary data by looking for null bytes or non-printable characters
|
||||
if (str_contains($content, "\0") || preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', $content)) {
|
||||
$content = '[binary file]';
|
||||
}
|
||||
$this->content = $content;
|
||||
$this->is_directory = false;
|
||||
$this->save();
|
||||
|
@@ -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(
|
||||
|
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';
|
||||
if (in_array($this->ssl_mode, ['verify-full'])) {
|
||||
$url .= '&tlsCAFile=/etc/ssl/certs/coolify-ca.crt';
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -247,7 +264,17 @@ class StandaloneMongodb extends BaseModel
|
||||
return new Attribute(
|
||||
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';
|
||||
if (in_array($this->ssl_mode, ['verify-full'])) {
|
||||
$url .= '&tlsCAFile=/etc/ssl/certs/coolify-ca.crt';
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
@@ -38,6 +38,12 @@ class StandaloneRedis extends BaseModel
|
||||
$database->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
|
||||
static::retrieved(function ($database) {
|
||||
if (! $database->redis_username) {
|
||||
$database->redis_username = 'default';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
@@ -164,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')) {
|
||||
@@ -193,8 +204,8 @@ class StandaloneRedis extends BaseModel
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => is_null($this->ports_mappings)
|
||||
? []
|
||||
: explode(',', $this->ports_mappings),
|
||||
? []
|
||||
: explode(',', $this->ports_mappings),
|
||||
|
||||
);
|
||||
}
|
||||
@@ -216,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;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -229,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;
|
||||
@@ -346,7 +372,12 @@ class StandaloneRedis extends BaseModel
|
||||
get: function () {
|
||||
$username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
|
||||
if (! $username) {
|
||||
return null;
|
||||
$this->runtime_environment_variables()->create([
|
||||
'key' => 'REDIS_USERNAME',
|
||||
'value' => 'default',
|
||||
]);
|
||||
|
||||
return 'default';
|
||||
}
|
||||
|
||||
return $username->value;
|
||||
|
@@ -53,6 +53,7 @@ class EmailChannel
|
||||
if (blank($type)) {
|
||||
throw new Exception('No email settings found.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
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()
|
||||
);
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@ use Illuminate\Foundation\Events\MaintenanceModeEnabled;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use SocialiteProviders\Authentik\AuthentikExtendSocialite;
|
||||
use SocialiteProviders\Azure\AzureExtendSocialite;
|
||||
use SocialiteProviders\Google\GoogleExtendSocialite;
|
||||
use SocialiteProviders\Infomaniak\InfomaniakExtendSocialite;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
@@ -26,6 +27,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
SocialiteWasCalled::class => [
|
||||
AzureExtendSocialite::class.'@handle',
|
||||
AuthentikExtendSocialite::class.'@handle',
|
||||
GoogleExtendSocialite::class.'@handle',
|
||||
InfomaniakExtendSocialite::class.'@handle',
|
||||
],
|
||||
ProxyStarted::class => [
|
||||
|
@@ -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,7 @@ class Select extends Component
|
||||
public ?string $label = null,
|
||||
public ?string $helper = null,
|
||||
public bool $required = false,
|
||||
public string $defaultClass = 'select'
|
||||
public string $defaultClass = 'select w-full'
|
||||
) {
|
||||
//
|
||||
}
|
||||
@@ -36,8 +35,6 @@ class Select extends Component
|
||||
$this->name = $this->id;
|
||||
}
|
||||
|
||||
$this->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;
|
||||
@@ -233,15 +236,29 @@ function deleteEmptyBackupFolder($folderPath, Server $server): void
|
||||
function removeOldBackups($backup): void
|
||||
{
|
||||
try {
|
||||
$processedBackups = deleteOldBackupsLocally($backup);
|
||||
|
||||
if ($backup->save_s3) {
|
||||
$processedBackups = $processedBackups->merge(deleteOldBackupsFromS3($backup));
|
||||
if ($backup->executions) {
|
||||
$localBackupsToDelete = deleteOldBackupsLocally($backup);
|
||||
if ($localBackupsToDelete->isNotEmpty()) {
|
||||
$backup->executions()
|
||||
->whereIn('id', $localBackupsToDelete->pluck('id'))
|
||||
->update(['local_storage_deleted' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($processedBackups->isNotEmpty()) {
|
||||
$backup->executions()->whereIn('id', $processedBackups->pluck('id'))->delete();
|
||||
if ($backup->save_s3 && $backup->executions) {
|
||||
$s3BackupsToDelete = deleteOldBackupsFromS3($backup);
|
||||
if ($s3BackupsToDelete->isNotEmpty()) {
|
||||
$backup->executions()
|
||||
->whereIn('id', $s3BackupsToDelete->pluck('id'))
|
||||
->update(['s3_storage_deleted' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
$backup->executions()
|
||||
->where('local_storage_deleted', true)
|
||||
->where('s3_storage_deleted', true)
|
||||
->delete();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
@@ -255,6 +272,7 @@ function deleteOldBackupsLocally($backup): Collection
|
||||
|
||||
$successfulBackups = $backup->executions()
|
||||
->where('status', 'success')
|
||||
->where('local_storage_deleted', false)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
@@ -338,6 +356,7 @@ function deleteOldBackupsFromS3($backup): Collection
|
||||
|
||||
$successfulBackups = $backup->executions()
|
||||
->where('status', 'success')
|
||||
->where('s3_storage_deleted', false)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
|
@@ -4054,29 +4054,24 @@ function defaultNginxConfiguration(): string
|
||||
{
|
||||
return 'server {
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri.html $uri/index.html $uri/index.htm $uri/ /index.html /index.htm =404;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri.html $uri/index.html $uri/index.htm $uri/ =404;
|
||||
}
|
||||
|
||||
# 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;
|
||||
try_files $uri @redirect_to_index;
|
||||
internal;
|
||||
}
|
||||
|
||||
error_page 404 = @handle_404;
|
||||
|
||||
location @handle_404 {
|
||||
root /usr/share/nginx/html;
|
||||
try_files /404.html @redirect_to_index;
|
||||
internal;
|
||||
}
|
||||
|
||||
location @redirect_to_index {
|
||||
return 302 /;
|
||||
}
|
||||
}';
|
||||
}
|
||||
|
||||
|
@@ -29,6 +29,18 @@ function get_socialite_provider(string $provider)
|
||||
return Socialite::driver('authentik')->setConfig($authentik_config);
|
||||
}
|
||||
|
||||
if ($provider == 'google') {
|
||||
$google_config = new \SocialiteProviders\Manager\Config(
|
||||
$oauth_setting->client_id,
|
||||
$oauth_setting->client_secret,
|
||||
$oauth_setting->redirect_uri
|
||||
);
|
||||
|
||||
return Socialite::driver('google')
|
||||
->setConfig($google_config)
|
||||
->with(['hd' => $oauth_setting->tenant]);
|
||||
}
|
||||
|
||||
$config = [
|
||||
'client_id' => $oauth_setting->client_id,
|
||||
'client_secret' => $oauth_setting->client_secret,
|
||||
@@ -39,7 +51,6 @@ function get_socialite_provider(string $provider)
|
||||
'bitbucket' => \Laravel\Socialite\Two\BitbucketProvider::class,
|
||||
'github' => \Laravel\Socialite\Two\GithubProvider::class,
|
||||
'gitlab' => \Laravel\Socialite\Two\GitlabProvider::class,
|
||||
'google' => \Laravel\Socialite\Two\GoogleProvider::class,
|
||||
'infomaniak' => \SocialiteProviders\Infomaniak\Provider::class,
|
||||
];
|
||||
|
||||
|
@@ -40,6 +40,7 @@
|
||||
"resend/resend-laravel": "^0.15.0",
|
||||
"sentry/sentry-laravel": "^4.6",
|
||||
"socialiteproviders/authentik": "^5.2",
|
||||
"socialiteproviders/google": "^4.1",
|
||||
"socialiteproviders/infomaniak": "^4.0",
|
||||
"socialiteproviders/microsoft-azure": "^5.1",
|
||||
"spatie/laravel-activitylog": "^4.7.3",
|
||||
@@ -125,4 +126,4 @@
|
||||
"@php artisan key:generate --ansi"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
188
composer.lock
generated
188
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9c1a0833be38d1f058f216dcaa522077",
|
||||
"content-hash": "dcf6b2f554372a570628d7f85184df7b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "3sidedcube/laravel-redoc",
|
||||
@@ -1079,16 +1079,16 @@
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/brick/math.git",
|
||||
"reference": "f510c0a40911935b77b86859eb5223d58d660df1"
|
||||
"reference": "866551da34e9a618e64a819ee1e01c20d8a588ba"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1",
|
||||
"reference": "f510c0a40911935b77b86859eb5223d58d660df1",
|
||||
"url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba",
|
||||
"reference": "866551da34e9a618e64a819ee1e01c20d8a588ba",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1097,7 +1097,7 @@
|
||||
"require-dev": {
|
||||
"php-coveralls/php-coveralls": "^2.2",
|
||||
"phpunit/phpunit": "^10.1",
|
||||
"vimeo/psalm": "5.16.0"
|
||||
"vimeo/psalm": "6.8.8"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -1127,7 +1127,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/brick/math/issues",
|
||||
"source": "https://github.com/brick/math/tree/0.12.1"
|
||||
"source": "https://github.com/brick/math/tree/0.12.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1135,7 +1135,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-11-29T23:19:16+00:00"
|
||||
"time": "2025-02-28T13:11:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
@@ -2732,16 +2732,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v11.44.0",
|
||||
"version": "v11.44.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "e9a33da34815ac1ed46c7e4c477a775f4592f0a7"
|
||||
"reference": "0883d4175f4e2b5c299e7087ad3c74f2ce195c6d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/e9a33da34815ac1ed46c7e4c477a775f4592f0a7",
|
||||
"reference": "e9a33da34815ac1ed46c7e4c477a775f4592f0a7",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/0883d4175f4e2b5c299e7087ad3c74f2ce195c6d",
|
||||
"reference": "0883d4175f4e2b5c299e7087ad3c74f2ce195c6d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2849,7 +2849,7 @@
|
||||
"league/flysystem-read-only": "^3.25.1",
|
||||
"league/flysystem-sftp-v3": "^3.25.1",
|
||||
"mockery/mockery": "^1.6.10",
|
||||
"orchestra/testbench-core": "^9.9.4",
|
||||
"orchestra/testbench-core": "^9.11.2",
|
||||
"pda/pheanstalk": "^5.0.6",
|
||||
"php-http/discovery": "^1.15",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
@@ -2943,7 +2943,7 @@
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-02-24T13:08:54+00:00"
|
||||
"time": "2025-03-05T15:34:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/horizon",
|
||||
@@ -6944,16 +6944,16 @@
|
||||
},
|
||||
{
|
||||
"name": "ramsey/collection",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ramsey/collection.git",
|
||||
"reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5"
|
||||
"reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5",
|
||||
"reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5",
|
||||
"url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109",
|
||||
"reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6961,25 +6961,22 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"captainhook/plugin-composer": "^5.3",
|
||||
"ergebnis/composer-normalize": "^2.28.3",
|
||||
"fakerphp/faker": "^1.21",
|
||||
"ergebnis/composer-normalize": "^2.45",
|
||||
"fakerphp/faker": "^1.24",
|
||||
"hamcrest/hamcrest-php": "^2.0",
|
||||
"jangregor/phpstan-prophecy": "^1.0",
|
||||
"mockery/mockery": "^1.5",
|
||||
"jangregor/phpstan-prophecy": "^2.1",
|
||||
"mockery/mockery": "^1.6",
|
||||
"php-parallel-lint/php-console-highlighter": "^1.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3",
|
||||
"phpcsstandards/phpcsutils": "^1.0.0-rc1",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpstan/extension-installer": "^1.2",
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"phpstan/phpstan-mockery": "^1.1",
|
||||
"phpstan/phpstan-phpunit": "^1.3",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"psalm/plugin-mockery": "^1.1",
|
||||
"psalm/plugin-phpunit": "^0.18.4",
|
||||
"ramsey/coding-standard": "^2.0.3",
|
||||
"ramsey/conventional-commits": "^1.3",
|
||||
"vimeo/psalm": "^5.4"
|
||||
"php-parallel-lint/php-parallel-lint": "^1.4",
|
||||
"phpspec/prophecy-phpunit": "^2.3",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-mockery": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"ramsey/coding-standard": "^2.3",
|
||||
"ramsey/conventional-commits": "^1.6",
|
||||
"roave/security-advisories": "dev-latest"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -7017,19 +7014,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/ramsey/collection/issues",
|
||||
"source": "https://github.com/ramsey/collection/tree/2.0.0"
|
||||
"source": "https://github.com/ramsey/collection/tree/2.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/ramsey",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/ramsey/collection",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-12-31T21:50:55+00:00"
|
||||
"time": "2025-03-02T04:48:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ramsey/uuid",
|
||||
@@ -7549,6 +7536,47 @@
|
||||
},
|
||||
"time": "2023-11-07T22:21:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "socialiteproviders/google",
|
||||
"version": "4.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/SocialiteProviders/Google-Plus.git",
|
||||
"reference": "1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/SocialiteProviders/Google-Plus/zipball/1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401",
|
||||
"reference": "1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": "^7.2 || ^8.0",
|
||||
"socialiteproviders/manager": "~4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SocialiteProviders\\Google\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "xstoop",
|
||||
"email": "myenglishnameisx@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Google OAuth2 Provider for Laravel Socialite",
|
||||
"support": {
|
||||
"source": "https://github.com/SocialiteProviders/Google-Plus/tree/4.1.0"
|
||||
},
|
||||
"time": "2020-12-01T23:10:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "socialiteproviders/infomaniak",
|
||||
"version": "4.0.0",
|
||||
@@ -8887,16 +8915,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/error-handler",
|
||||
"version": "v7.2.3",
|
||||
"version": "v7.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/error-handler.git",
|
||||
"reference": "959a74d044a6db21f4caa6d695648dcb5584cb49"
|
||||
"reference": "aabf79938aa795350c07ce6464dd1985607d95d5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/error-handler/zipball/959a74d044a6db21f4caa6d695648dcb5584cb49",
|
||||
"reference": "959a74d044a6db21f4caa6d695648dcb5584cb49",
|
||||
"url": "https://api.github.com/repos/symfony/error-handler/zipball/aabf79938aa795350c07ce6464dd1985607d95d5",
|
||||
"reference": "aabf79938aa795350c07ce6464dd1985607d95d5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8942,7 +8970,7 @@
|
||||
"description": "Provides tools to manage errors and ease debugging PHP code",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/error-handler/tree/v7.2.3"
|
||||
"source": "https://github.com/symfony/error-handler/tree/v7.2.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8958,7 +8986,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-07T09:39:55+00:00"
|
||||
"time": "2025-02-02T20:27:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
@@ -9260,16 +9288,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-kernel",
|
||||
"version": "v7.2.3",
|
||||
"version": "v7.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-kernel.git",
|
||||
"reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b"
|
||||
"reference": "9f1103734c5789798fefb90e91de4586039003ed"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b",
|
||||
"reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b",
|
||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f1103734c5789798fefb90e91de4586039003ed",
|
||||
"reference": "9f1103734c5789798fefb90e91de4586039003ed",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -9354,7 +9382,7 @@
|
||||
"description": "Provides a structured process for converting a Request into a Response",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-kernel/tree/v7.2.3"
|
||||
"source": "https://github.com/symfony/http-kernel/tree/v7.2.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -9370,7 +9398,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-29T07:40:13+00:00"
|
||||
"time": "2025-02-26T11:01:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mailer",
|
||||
@@ -9454,16 +9482,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/mime",
|
||||
"version": "v7.2.3",
|
||||
"version": "v7.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mime.git",
|
||||
"reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204"
|
||||
"reference": "87ca22046b78c3feaff04b337f33b38510fd686b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204",
|
||||
"reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b",
|
||||
"reference": "87ca22046b78c3feaff04b337f33b38510fd686b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -9518,7 +9546,7 @@
|
||||
"mime-type"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mime/tree/v7.2.3"
|
||||
"source": "https://github.com/symfony/mime/tree/v7.2.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -9534,7 +9562,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-27T11:08:17+00:00"
|
||||
"time": "2025-02-19T08:51:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
@@ -10321,16 +10349,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v7.2.0",
|
||||
"version": "v7.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e"
|
||||
"reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e",
|
||||
"reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf",
|
||||
"reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10362,7 +10390,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v7.2.0"
|
||||
"source": "https://github.com/symfony/process/tree/v7.2.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10378,7 +10406,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-11-06T14:24:19+00:00"
|
||||
"time": "2025-02-05T08:33:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/psr-http-message-bridge",
|
||||
@@ -10778,16 +10806,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation",
|
||||
"version": "v7.2.2",
|
||||
"version": "v7.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/translation.git",
|
||||
"reference": "e2674a30132b7cc4d74540d6c2573aa363f05923"
|
||||
"reference": "283856e6981286cc0d800b53bd5703e8e363f05a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923",
|
||||
"reference": "e2674a30132b7cc4d74540d6c2573aa363f05923",
|
||||
"url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a",
|
||||
"reference": "283856e6981286cc0d800b53bd5703e8e363f05a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10853,7 +10881,7 @@
|
||||
"description": "Provides tools to internationalize your application",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/translation/tree/v7.2.2"
|
||||
"source": "https://github.com/symfony/translation/tree/v7.2.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10869,7 +10897,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-12-07T08:18:10+00:00"
|
||||
"time": "2025-02-13T10:27:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation-contracts",
|
||||
@@ -15569,12 +15597,12 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"stability-flags": [],
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.4"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.3.0"
|
||||
}
|
||||
|
@@ -45,4 +45,12 @@ return [
|
||||
'client_secret' => env('AUTHENTIK_CLIENT_SECRET'),
|
||||
'redirect' => env('AUTHENTIK_REDIRECT_URI'),
|
||||
],
|
||||
|
||||
'google' => [
|
||||
'client_id' => env('GOOGLE_CLIENT_ID'),
|
||||
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
||||
'redirect' => env('GOOGLE_REDIRECT_URI'),
|
||||
'tenant' => env('GOOGLE_TENANT'),
|
||||
],
|
||||
|
||||
];
|
||||
|
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('standalone_postgresqls', function (Blueprint $table) {
|
||||
$table->boolean('enable_ssl')->default(false);
|
||||
$table->enum('ssl_mode', ['allow', 'prefer', 'require', 'verify-ca', 'verify-full'])->default('require');
|
||||
});
|
||||
Schema::table('standalone_mysqls', function (Blueprint $table) {
|
||||
$table->boolean('enable_ssl')->default(false);
|
||||
$table->enum('ssl_mode', ['PREFERRED', 'REQUIRED', 'VERIFY_CA', 'VERIFY_IDENTITY'])->default('REQUIRED');
|
||||
});
|
||||
Schema::table('standalone_mariadbs', function (Blueprint $table) {
|
||||
$table->boolean('enable_ssl')->default(false);
|
||||
});
|
||||
Schema::table('standalone_redis', function (Blueprint $table) {
|
||||
$table->boolean('enable_ssl')->default(false);
|
||||
});
|
||||
Schema::table('standalone_keydbs', function (Blueprint $table) {
|
||||
$table->boolean('enable_ssl')->default(false);
|
||||
});
|
||||
Schema::table('standalone_dragonflies', function (Blueprint $table) {
|
||||
$table->boolean('enable_ssl')->default(false);
|
||||
});
|
||||
Schema::table('standalone_mongodbs', function (Blueprint $table) {
|
||||
$table->boolean('enable_ssl')->default(true);
|
||||
$table->enum('ssl_mode', ['allow', 'prefer', 'require', 'verify-full'])->default('require');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('standalone_postgresqls', function (Blueprint $table) {
|
||||
$table->dropColumn('enable_ssl');
|
||||
$table->dropColumn('ssl_mode');
|
||||
});
|
||||
Schema::table('standalone_mysqls', function (Blueprint $table) {
|
||||
$table->dropColumn('enable_ssl');
|
||||
$table->dropColumn('ssl_mode');
|
||||
});
|
||||
Schema::table('standalone_mariadbs', function (Blueprint $table) {
|
||||
$table->dropColumn('enable_ssl');
|
||||
});
|
||||
Schema::table('standalone_redis', function (Blueprint $table) {
|
||||
$table->dropColumn('enable_ssl');
|
||||
});
|
||||
Schema::table('standalone_keydbs', function (Blueprint $table) {
|
||||
$table->dropColumn('enable_ssl');
|
||||
});
|
||||
Schema::table('standalone_dragonflies', function (Blueprint $table) {
|
||||
$table->dropColumn('enable_ssl');
|
||||
});
|
||||
Schema::table('standalone_mongodbs', function (Blueprint $table) {
|
||||
$table->dropColumn('enable_ssl');
|
||||
$table->dropColumn('ssl_mode');
|
||||
});
|
||||
}
|
||||
};
|
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('ssl_certificates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->text('ssl_certificate');
|
||||
$table->text('ssl_private_key');
|
||||
$table->text('configuration_dir')->nullable();
|
||||
$table->text('mount_path')->nullable();
|
||||
$table->string('resource_type')->nullable();
|
||||
$table->unsignedBigInteger('resource_id')->nullable();
|
||||
$table->unsignedBigInteger('server_id');
|
||||
$table->text('common_name');
|
||||
$table->json('subject_alternative_names')->nullable();
|
||||
$table->timestamp('valid_until');
|
||||
$table->boolean('is_ca_certificate')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('server_id')->references('id')->on('servers');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('ssl_certificates');
|
||||
}
|
||||
};
|
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('local_file_volumes', function (Blueprint $table) {
|
||||
$table->text('mount_path')->nullable()->change();
|
||||
});
|
||||
|
||||
if (DB::table('local_file_volumes')->exists()) {
|
||||
DB::table('local_file_volumes')
|
||||
->orderBy('id')
|
||||
->chunk(100, function ($volumes) {
|
||||
foreach ($volumes as $volume) {
|
||||
try {
|
||||
DB::table('local_file_volumes')->where('id', $volume->id)->update([
|
||||
'fs_path' => $volume->fs_path ? Crypt::encryptString($volume->fs_path) : null,
|
||||
'mount_path' => $volume->mount_path ? Crypt::encryptString($volume->mount_path) : null,
|
||||
'content' => $volume->content ? Crypt::encryptString($volume->content) : null,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error encrypting local file volume fields: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('local_file_volumes', function (Blueprint $table) {
|
||||
$table->string('fs_path')->change();
|
||||
$table->string('mount_path')->nullable()->change();
|
||||
$table->longText('content')->nullable()->change();
|
||||
});
|
||||
|
||||
if (DB::table('local_file_volumes')->exists()) {
|
||||
DB::table('local_file_volumes')
|
||||
->orderBy('id')
|
||||
->chunk(100, function ($volumes) {
|
||||
foreach ($volumes as $volume) {
|
||||
try {
|
||||
DB::table('local_file_volumes')->where('id', $volume->id)->update([
|
||||
'fs_path' => $volume->fs_path ? Crypt::decryptString($volume->fs_path) : null,
|
||||
'mount_path' => $volume->mount_path ? Crypt::decryptString($volume->mount_path) : null,
|
||||
'content' => $volume->content ? Crypt::decryptString($volume->content) : null,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error decrypting local file volume fields: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
|
||||
$table->boolean('local_storage_deleted')->default(false);
|
||||
$table->boolean('s3_storage_deleted')->default(false);
|
||||
});
|
||||
}
|
||||
};
|
43
database/seeders/CaSslCertSeeder.php
Normal file
43
database/seeders/CaSslCertSeeder.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class CaSslCertSeeder extends Seeder
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
Server::chunk(200, function ($servers) {
|
||||
foreach ($servers as $server) {
|
||||
$existingCaCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $existingCaCert) {
|
||||
$caCert = SslHelper::generateSslCertificate(
|
||||
commonName: 'Coolify CA Certificate',
|
||||
serverId: $server->id,
|
||||
isCaCertificate: true,
|
||||
validityDays: 10 * 365
|
||||
);
|
||||
} else {
|
||||
$caCert = $existingCaCert;
|
||||
}
|
||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||
|
||||
$commands = collect([
|
||||
"mkdir -p $caCertPath",
|
||||
"chown -R 9999:root $caCertPath",
|
||||
"chmod -R 700 $caCertPath",
|
||||
"rm -rf $caCertPath/coolify-ca.crt",
|
||||
"echo '{$caCert->ssl_certificate}' > $caCertPath/coolify-ca.crt",
|
||||
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||
]);
|
||||
|
||||
remote_process($commands, $server);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -28,6 +28,7 @@ class DatabaseSeeder extends Seeder
|
||||
OauthSettingSeeder::class,
|
||||
DisableTwoStepConfirmationSeeder::class,
|
||||
SentinelSeeder::class,
|
||||
CaSslCertSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ class GithubAppSeeder extends Seeder
|
||||
'team_id' => 0,
|
||||
]);
|
||||
GithubApp::create([
|
||||
'name' => 'coolify-laravel-development-public',
|
||||
'name' => 'coolify-laravel-dev-public',
|
||||
'uuid' => '69420',
|
||||
'organization' => 'coollabsio',
|
||||
'api_url' => 'https://api.github.com',
|
||||
|
@@ -193,5 +193,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
$this->call(PopulateSshKeysDirectorySeeder::class);
|
||||
$this->call(SentinelSeeder::class);
|
||||
$this->call(RootUserSeeder::class);
|
||||
$this->call(CaSslCertSeeder::class);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,15 @@
|
||||
#!/bin/bash
|
||||
## Do not modify this file. You will lose the ability to install and auto-update!
|
||||
|
||||
## Environment variables that can be set:
|
||||
## ROOT_USERNAME - Predefined root username
|
||||
## ROOT_USER_EMAIL - Predefined root user email
|
||||
## ROOT_USER_PASSWORD - Predefined root user password
|
||||
## DOCKER_ADDRESS_POOL_BASE - Custom Docker address pool base (default: 10.0.0.0/8)
|
||||
## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24)
|
||||
## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false)
|
||||
## AUTOUPDATE - Set to "false" to disable auto-updates
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
## $1 could be empty, so we need to disable this check
|
||||
#set -u # Treat unset variables as an error and exit
|
||||
@@ -8,7 +17,7 @@ set -o pipefail # Cause a pipeline to return the status of the last command that
|
||||
CDN="https://cdn.coollabs.io/coolify-nightly"
|
||||
DATE=$(date +"%Y%m%d-%H%M%S")
|
||||
|
||||
VERSION="1.7"
|
||||
VERSION="1.8"
|
||||
DOCKER_VERSION="27.0"
|
||||
# TODO: Ask for a user
|
||||
CURRENT_USER=$USER
|
||||
@@ -27,6 +36,144 @@ ROOT_USERNAME=${ROOT_USERNAME:-}
|
||||
ROOT_USER_EMAIL=${ROOT_USER_EMAIL:-}
|
||||
ROOT_USER_PASSWORD=${ROOT_USER_PASSWORD:-}
|
||||
|
||||
# Docker address pool configuration defaults
|
||||
DOCKER_ADDRESS_POOL_BASE_DEFAULT="10.0.0.0/8"
|
||||
DOCKER_ADDRESS_POOL_SIZE_DEFAULT=24
|
||||
|
||||
# Check if environment variables were explicitly provided
|
||||
DOCKER_POOL_BASE_PROVIDED=false
|
||||
DOCKER_POOL_SIZE_PROVIDED=false
|
||||
DOCKER_POOL_FORCE_OVERRIDE=${DOCKER_POOL_FORCE_OVERRIDE:-false}
|
||||
|
||||
if [ -n "${DOCKER_ADDRESS_POOL_BASE+x}" ]; then
|
||||
DOCKER_POOL_BASE_PROVIDED=true
|
||||
fi
|
||||
|
||||
if [ -n "${DOCKER_ADDRESS_POOL_SIZE+x}" ]; then
|
||||
DOCKER_POOL_SIZE_PROVIDED=true
|
||||
fi
|
||||
|
||||
restart_docker_service() {
|
||||
# Check if systemctl is available
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl restart docker
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker daemon restarted successfully"
|
||||
else
|
||||
echo " - Failed to restart Docker daemon"
|
||||
return 1
|
||||
fi
|
||||
# Check if service command is available
|
||||
elif command -v service >/dev/null 2>&1; then
|
||||
service docker restart
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker daemon restarted successfully"
|
||||
else
|
||||
echo " - Failed to restart Docker daemon"
|
||||
return 1
|
||||
fi
|
||||
# If neither systemctl nor service is available
|
||||
else
|
||||
echo " - Error: No service management system found"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to compare address pools
|
||||
compare_address_pools() {
|
||||
local base1="$1"
|
||||
local size1="$2"
|
||||
local base2="$3"
|
||||
local size2="$4"
|
||||
|
||||
# Normalize CIDR notation for comparison
|
||||
local ip1=$(echo "$base1" | cut -d'/' -f1)
|
||||
local prefix1=$(echo "$base1" | cut -d'/' -f2)
|
||||
local ip2=$(echo "$base2" | cut -d'/' -f1)
|
||||
local prefix2=$(echo "$base2" | cut -d'/' -f2)
|
||||
|
||||
# Compare IPs and prefixes
|
||||
if [ "$ip1" = "$ip2" ] && [ "$prefix1" = "$prefix2" ] && [ "$size1" = "$size2" ]; then
|
||||
return 0 # Pools are the same
|
||||
else
|
||||
return 1 # Pools are different
|
||||
fi
|
||||
}
|
||||
|
||||
# Docker address pool configuration
|
||||
DOCKER_ADDRESS_POOL_BASE=${DOCKER_ADDRESS_POOL_BASE:-"$DOCKER_ADDRESS_POOL_BASE_DEFAULT"}
|
||||
DOCKER_ADDRESS_POOL_SIZE=${DOCKER_ADDRESS_POOL_SIZE:-$DOCKER_ADDRESS_POOL_SIZE_DEFAULT}
|
||||
|
||||
# Load Docker address pool configuration from .env file if it exists and environment variables were not provided
|
||||
if [ -f "/data/coolify/source/.env" ] && [ "$DOCKER_POOL_BASE_PROVIDED" = false ] && [ "$DOCKER_POOL_SIZE_PROVIDED" = false ]; then
|
||||
ENV_DOCKER_ADDRESS_POOL_BASE=$(grep -E "^DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env | cut -d '=' -f2 || true)
|
||||
ENV_DOCKER_ADDRESS_POOL_SIZE=$(grep -E "^DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env | cut -d '=' -f2 || true)
|
||||
|
||||
if [ -n "$ENV_DOCKER_ADDRESS_POOL_BASE" ]; then
|
||||
DOCKER_ADDRESS_POOL_BASE="$ENV_DOCKER_ADDRESS_POOL_BASE"
|
||||
fi
|
||||
|
||||
if [ -n "$ENV_DOCKER_ADDRESS_POOL_SIZE" ]; then
|
||||
DOCKER_ADDRESS_POOL_SIZE="$ENV_DOCKER_ADDRESS_POOL_SIZE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if daemon.json exists and extract existing address pool configuration
|
||||
EXISTING_POOL_CONFIGURED=false
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
if jq -e '.["default-address-pools"]' /etc/docker/daemon.json >/dev/null 2>&1; then
|
||||
EXISTING_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null || true)
|
||||
EXISTING_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null || true)
|
||||
|
||||
if [ -n "$EXISTING_POOL_BASE" ] && [ -n "$EXISTING_POOL_SIZE" ] && [ "$EXISTING_POOL_BASE" != "null" ] && [ "$EXISTING_POOL_SIZE" != "null" ]; then
|
||||
echo "Found existing Docker network pool: $EXISTING_POOL_BASE/$EXISTING_POOL_SIZE"
|
||||
EXISTING_POOL_CONFIGURED=true
|
||||
|
||||
# Check if environment variables were explicitly provided
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = false ] && [ "$DOCKER_POOL_SIZE_PROVIDED" = false ]; then
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
else
|
||||
# Check if force override is enabled
|
||||
if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ]; then
|
||||
echo "Force override enabled - network pool will be updated with $DOCKER_ADDRESS_POOL_BASE/$DOCKER_ADDRESS_POOL_SIZE."
|
||||
else
|
||||
echo "Custom pool provided but force override not enabled - using existing configuration."
|
||||
echo "To force override, set DOCKER_POOL_FORCE_OVERRIDE=true"
|
||||
echo "This won't change the existing docker networks, only the pool configuration for the newly created networks."
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
DOCKER_POOL_BASE_PROVIDED=false
|
||||
DOCKER_POOL_SIZE_PROVIDED=false
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate Docker address pool configuration
|
||||
if ! [[ $DOCKER_ADDRESS_POOL_BASE =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]]; then
|
||||
echo "Warning: Invalid network pool base format: $DOCKER_ADDRESS_POOL_BASE"
|
||||
if [ "$EXISTING_POOL_CONFIGURED" = true ]; then
|
||||
echo "Using existing configuration: $EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
else
|
||||
echo "Using default configuration: $DOCKER_ADDRESS_POOL_BASE_DEFAULT"
|
||||
DOCKER_ADDRESS_POOL_BASE="$DOCKER_ADDRESS_POOL_BASE_DEFAULT"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! [[ $DOCKER_ADDRESS_POOL_SIZE =~ ^[0-9]+$ ]] || [ "$DOCKER_ADDRESS_POOL_SIZE" -lt 16 ] || [ "$DOCKER_ADDRESS_POOL_SIZE" -gt 28 ]; then
|
||||
echo "Warning: Invalid network pool size: $DOCKER_ADDRESS_POOL_SIZE (must be 16-28)"
|
||||
if [ "$EXISTING_POOL_CONFIGURED" = true ]; then
|
||||
echo "Using existing configuration: $EXISTING_POOL_SIZE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
else
|
||||
echo "Using default configuration: $DOCKER_ADDRESS_POOL_SIZE_DEFAULT"
|
||||
DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE_DEFAULT
|
||||
fi
|
||||
fi
|
||||
|
||||
TOTAL_SPACE=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
|
||||
AVAILABLE_SPACE=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//')
|
||||
REQUIRED_TOTAL_SPACE=30
|
||||
@@ -35,7 +182,7 @@ WARNING_SPACE=false
|
||||
|
||||
if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then
|
||||
WARNING_SPACE=true
|
||||
cat << EOF
|
||||
cat <<EOF
|
||||
WARNING: Insufficient total disk space!
|
||||
|
||||
Total disk space: ${TOTAL_SPACE}GB
|
||||
@@ -46,7 +193,7 @@ EOF
|
||||
fi
|
||||
|
||||
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then
|
||||
cat << EOF
|
||||
cat <<EOF
|
||||
WARNING: Insufficient available disk space!
|
||||
|
||||
Available disk space: ${AVAILABLE_SPACE}GB
|
||||
@@ -54,7 +201,7 @@ Required available space: ${REQUIRED_AVAILABLE_SPACE}GB
|
||||
|
||||
==================
|
||||
EOF
|
||||
WARNING_SPACE=true
|
||||
WARNING_SPACE=true
|
||||
fi
|
||||
|
||||
if [ "$WARNING_SPACE" = true ]; then
|
||||
@@ -136,7 +283,6 @@ if [ -z "$LATEST_REALTIME_VERSION" ]; then
|
||||
LATEST_REALTIME_VERSION=latest
|
||||
fi
|
||||
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
*)
|
||||
@@ -152,14 +298,13 @@ if [ "$1" != "" ]; then
|
||||
LATEST_VERSION="${LATEST_VERSION#v}"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo "| Coolify | $LATEST_VERSION"
|
||||
echo "| Helper | $LATEST_HELPER_VERSION"
|
||||
echo "| Realtime | $LATEST_REALTIME_VERSION"
|
||||
echo "| Docker Pool | $DOCKER_ADDRESS_POOL_BASE (size $DOCKER_ADDRESS_POOL_SIZE)"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
@@ -199,7 +344,6 @@ sles | opensuse-leap | opensuse-tumbleweed)
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
echo -e "2. Check OpenSSH server configuration. "
|
||||
|
||||
# Detect OpenSSH server
|
||||
@@ -222,7 +366,6 @@ elif [ -x "$(command -v service)" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
if [ "$SSH_DETECTED" = "false" ]; then
|
||||
echo " - OpenSSH server not detected. Installing OpenSSH server."
|
||||
case "$OS_TYPE" in
|
||||
@@ -288,86 +431,112 @@ if [ -x "$(command -v snap)" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
install_docker() {
|
||||
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
echo -e "3. Check Docker Installation. "
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
getAJoke
|
||||
case "$OS_TYPE" in
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"centos" | "fedora" | "rhel")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"ubuntu" | "debian" | "raspbian")
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
echo " - Installing Docker for Ubuntu 24.10..."
|
||||
apt-get update >/dev/null
|
||||
apt-get install -y ca-certificates curl >/dev/null
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
# Add the repository to Apt sources
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" |
|
||||
tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||||
apt-get update >/dev/null
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null
|
||||
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
echo " - Docker installation failed."
|
||||
echo " Please visit https://docs.docker.com/engine/install/ubuntu/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"fedora")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
|
||||
echo "Please install Docker manually."
|
||||
exit 1
|
||||
fi
|
||||
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo " - Docker installed successfully for Ubuntu 24.10."
|
||||
else
|
||||
install_docker
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
install_docker
|
||||
;;
|
||||
esac
|
||||
echo " - Docker installed successfully."
|
||||
else
|
||||
@@ -375,82 +544,132 @@ else
|
||||
fi
|
||||
|
||||
echo -e "4. Check Docker Configuration. "
|
||||
|
||||
echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
echo " - To override existing configuration: DOCKER_POOL_FORCE_OVERRIDE=true"
|
||||
|
||||
mkdir -p /etc/docker
|
||||
# shellcheck disable=SC2015
|
||||
test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" || cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"10.0.0.0/8","size":24}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
cat >/etc/docker/daemon.json.coolify <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"10.0.0.0/8","size":24}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
TEMP_FILE=$(mktemp)
|
||||
if ! jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify >"$TEMP_FILE"; then
|
||||
echo "Error merging JSON files"
|
||||
exit 1
|
||||
|
||||
# Backup original daemon.json if it exists
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE"
|
||||
fi
|
||||
mv "$TEMP_FILE" /etc/docker/daemon.json
|
||||
|
||||
restart_docker_service() {
|
||||
# Check if systemctl is available
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
echo " - Using systemctl to restart Docker."
|
||||
systemctl restart docker
|
||||
# Create coolify configuration with or without address pools based on whether they were explicitly provided
|
||||
if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ] || [ "$EXISTING_POOL_CONFIGURED" = false ]; then
|
||||
# First check if the configuration would actually change anything
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
CURRENT_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null)
|
||||
CURRENT_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null)
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker restarted successfully using systemctl."
|
||||
if [ "$CURRENT_POOL_BASE" = "$DOCKER_ADDRESS_POOL_BASE" ] && [ "$CURRENT_POOL_SIZE" = "$DOCKER_ADDRESS_POOL_SIZE" ]; then
|
||||
echo " - Network pool configuration unchanged, skipping update"
|
||||
NEED_MERGE=false
|
||||
else
|
||||
echo " - Failed to restart Docker using systemctl."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if service command is available
|
||||
elif command -v service >/dev/null 2>&1; then
|
||||
echo " - Using service command to restart Docker."
|
||||
service docker restart
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker restarted successfully using service."
|
||||
else
|
||||
echo " - Failed to restart Docker using service."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If neither systemctl nor service is available
|
||||
else
|
||||
echo " - Neither systemctl nor service command is available on this system."
|
||||
return 1
|
||||
fi
|
||||
# If force override is enabled or no existing configuration exists,
|
||||
# create a new configuration with the specified address pools
|
||||
echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
|
||||
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
|
||||
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE"))
|
||||
if [ "$DIFF" != "" ]; then
|
||||
echo " - Docker configuration updated, restart docker daemon..."
|
||||
restart_docker_service
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
else
|
||||
echo " - Docker configuration is up to date."
|
||||
# No existing configuration, create new one
|
||||
echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
else
|
||||
echo " - Docker configuration updated, restart docker daemon..."
|
||||
restart_docker_service
|
||||
# Check if we need to update log settings
|
||||
if [ -f /etc/docker/daemon.json ] && jq -e '.["log-driver"] == "json-file" and .["log-opts"]["max-size"] == "10m" and .["log-opts"]["max-file"] == "3"' /etc/docker/daemon.json >/dev/null 2>&1; then
|
||||
echo " - Log configuration is up to date"
|
||||
NEED_MERGE=false
|
||||
else
|
||||
# Create a configuration without address pools to preserve existing ones
|
||||
cat >/etc/docker/daemon.json.coolify <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
}
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove the duplicate daemon.json creation since we handle it above
|
||||
if ! [ -f /etc/docker/daemon.json ]; then
|
||||
# If no daemon.json exists, create it with default settings
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=false
|
||||
fi
|
||||
|
||||
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
|
||||
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE") || true)
|
||||
if [ "$DIFF" != "" ]; then
|
||||
echo " - Checking configuration changes..."
|
||||
|
||||
# Check if address pools were changed
|
||||
if echo "$DIFF" | grep -q "default-address-pools"; then
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ] || [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
echo " - Network pool updated per user request"
|
||||
else
|
||||
echo " - Warning: Network pool modified without explicit request"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove this redundant restart since we already restarted when writing the config
|
||||
echo " - Configuration changes confirmed"
|
||||
if [ "$NEED_MERGE" = true ]; then
|
||||
echo " - Configuration updated - restarting Docker daemon..."
|
||||
restart_docker_service
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
else
|
||||
if [ "$NEED_MERGE" = true ]; then
|
||||
echo " - Configuration updated - restarting Docker daemon..."
|
||||
restart_docker_service
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "5. Download required files from CDN. "
|
||||
@@ -501,7 +720,7 @@ fi
|
||||
|
||||
# Merge .env and .env.production. New values will be added to .env
|
||||
echo -e "7. Propagating .env with new values - if necessary."
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production >$ENV_FILE
|
||||
|
||||
if [ "$AUTOUPDATE" = "false" ]; then
|
||||
if ! grep -q "AUTOUPDATE=" /data/coolify/source/.env; then
|
||||
@@ -510,6 +729,26 @@ if [ "$AUTOUPDATE" = "false" ]; then
|
||||
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save Docker address pool configuration to .env file
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE" >>/data/coolify/source/.env
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_BASE=.*|DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE" >>/data/coolify/source/.env
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_SIZE=.*|DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "8. Checking for SSH key for localhost access."
|
||||
if [ ! -f ~/.ssh/authorized_keys ]; then
|
||||
mkdir -p ~/.ssh
|
||||
@@ -527,7 +766,7 @@ if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
|
||||
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify
|
||||
chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal
|
||||
sed -i "/coolify/d" ~/.ssh/authorized_keys
|
||||
cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >> ~/.ssh/authorized_keys
|
||||
cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >>~/.ssh/authorized_keys
|
||||
rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub
|
||||
fi
|
||||
|
||||
|
9
public/coolify-logo.svg
Normal file
9
public/coolify-logo.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 2048 2048" width="512" height="512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="translate(257,640)" d="m0 0h254l1 1v127h127l1 1v639h895l1 1v127h127l1 1v254l-5 1h-1018l-1-1v-127h-127l-1-1v-127h-127l-1-1v-127h-127l-1-1v-766z" fill="#8550FC"/>
|
||||
<path transform="translate(513,384)" d="m0 0h1022l1 1v127h127l1 1v254l-5 1h-1018l-1-1v-127h-127l-1-1v-254z" fill="#8550FC"/>
|
||||
<path transform="translate(1537,1536)" d="m0 0h126l1 1v254l-5 1h-1018l-1-1v-126l896-1v-29z" fill="#452E72"/>
|
||||
<path transform="translate(1537,512)" d="m0 0h126l1 1v254l-5 1h-1018l-1-1v-126l896-1v-29z" fill="#452E72"/>
|
||||
<path transform="translate(513,768)" d="m0 0h126l1 1v638l-7 1-108-1-12-1z" fill="#452E72"/>
|
||||
<path transform="translate(478,1408)" d="m0 0h32l1 1v127h-126l-1-1v-126z" fill="#452E72"/>
|
||||
</svg>
|
After Width: | Height: | Size: 853 B |
1
public/svgs/denoKV.svg
Normal file
1
public/svgs/denoKV.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4410 4410"><circle cx="2200" cy="2200" r="2200" fill="#fff"/><path d="M384 3039a1991 1991 0 0 1-184-839c0-78 4-154 13-229a2007 2007 0 0 1 39-224A2000 2000 0 0 1 871 706a2001 2001 0 0 1 910-462 2004 2004 0 0 1 571-38 1994 1994 0 0 1 680 175 2003 2003 0 0 1 463 295 2004 2004 0 0 1 676 1180 2009 2009 0 0 1 29 344 2025 2025 0 0 1-6 152 1998 1998 0 0 1-121 551 2001 2001 0 0 1-397 646 1035 1035 0 0 1-738 323 722 722 0 0 1-688-910c17-64 62-186 127-240a762 762 0 0 1-205-138c-7-8-6-22 1-31a26 26 0 0 1 29-8 1458 1458 0 0 0 228 58c111 18 248 42 387 48 338 17 692-135 802-437 109-303 67-602-327-781-394-180-576-393-894-522-208-84-439-34-676 98-640 353-1213 1470-949 2505a32 32 0 0 1-52 31 2011 2011 0 0 1-209-272 2003 2003 0 0 1-128-234zm1745-1989c107-8 202 84 218 206 21 163-39 332-235 336-169 3-220-166-208-269 11-103 95-263 224-273z"/></svg>
|
After Width: | Height: | Size: 891 B |
BIN
public/svgs/freescout.png
Normal file
BIN
public/svgs/freescout.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
150
public/svgs/wakapi.svg
Normal file
150
public/svgs/wakapi.svg
Normal file
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="275.9418mm"
|
||||
height="107.06042mm"
|
||||
viewBox="0 0 275.9418 107.06042"
|
||||
version="1.1"
|
||||
id="svg900"
|
||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||
sodipodi:docname="logo-dark-bg.svg">
|
||||
<defs
|
||||
id="defs894">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath20">
|
||||
<path
|
||||
d="M 0,700 H 1100 V 0 H 0 Z"
|
||||
id="path18" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="501.46484"
|
||||
inkscape:cy="865.17604"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1346"
|
||||
inkscape:window-height="1198"
|
||||
inkscape:window-x="149"
|
||||
inkscape:window-y="113"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata897">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(26.845906,80.744493)">
|
||||
<g
|
||||
id="g14"
|
||||
transform="matrix(0.35277777,0,0,-0.35277777,-100.60779,96.696294)">
|
||||
<g
|
||||
id="g16"
|
||||
clip-path="url(#clipPath20)">
|
||||
<g
|
||||
id="g22"
|
||||
transform="translate(600.9092,345.958)" />
|
||||
<g
|
||||
id="g26"
|
||||
transform="translate(706.707,299.5566)" />
|
||||
<g
|
||||
id="g30"
|
||||
transform="translate(810.2764,345.958)" />
|
||||
<g
|
||||
id="g34"
|
||||
transform="translate(930.5527,345.958)" />
|
||||
<g
|
||||
id="g40"
|
||||
transform="translate(503.8398,367.5195)">
|
||||
<path
|
||||
d="m 8.7266003,-16.286056 c 0,-3.170064 -0.10674,-6.340128 -0.32021,-9.492657 -0.85541,-13.857403 -3.56194,-27.197135 -7.89009005,-39.789715 C -18.95161,-122.31318 -71.13644,-163.86557 -133.5283,-167.69511 c -3.13424,-0.23101 -6.30506,-0.32097 -9.49342,-0.32097 -3.1876,0 -6.33937,0.09 -9.4919,0.32097 -76.24773,4.68419 -137.23296,65.6679 -141.91639,141.916397 -0.23101,3.152529 -0.32098,6.322593 -0.32098,9.492657 0,3.188361 0.09,6.358425 0.32098,9.493419 4.68343,76.22944 65.66866,137.249737 141.91639,141.933927 3.15253,0.21348 6.3043,0.32098 9.4919,0.32098 3.18836,0 6.35918,-0.1075 9.49342,-0.32098 V 94.354359 h -18.98532 V 110.75818 C -215.22645,106.14489 -265.43361,55.901133 -270.06444,-6.792637 h 14.65793 v -18.986076 h -14.65793 c 4.63083,-62.712067 54.83799,-112.920757 117.55082,-117.551577 v 16.72479 h 18.98532 v -16.72479 c 46.32654,3.42013 85.86619,31.73876 105.20907,71.59937 6.83873,14.070117 11.14859,29.584217 12.34251,45.952207 h -0.0358 l 8.8697703,18.986076 h 15.54917 c 0.21347,-3.134994 0.32021,-6.305058 0.32021,-9.493419"
|
||||
style="fill:#2f855a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.762401"
|
||||
id="path42" />
|
||||
</g>
|
||||
<g
|
||||
id="g44"
|
||||
transform="translate(504.2754,431.2373)">
|
||||
<path
|
||||
d="m 11.6231,-31.425333 -17.1029497,8.252992 -4.6666503,-9.703842 -9.61922,-20.006168 -37.98968,-78.855149 -0.0572,-0.0854 h -37.47886 l -0.0282,0.0572 0.0282,0.0854 14.77,30.64853 3.78456,7.85425 0.19898,0.42618 19.2087,39.869009 7.37013,15.25336 2.27653,4.752808 11.72497,24.386926 -5.77672,2.761416 -9.59024,4.638449 49.9708203,23.249422 z"
|
||||
style="fill:#2f855a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.762401"
|
||||
id="path46" />
|
||||
</g>
|
||||
<g
|
||||
id="g48"
|
||||
transform="translate(258.5654,361.2139)">
|
||||
<path
|
||||
d="m 65.50352,-19.287853 v -0.0183 L 46.73396,-58.308305 12.56848,12.654468 2.9523203,32.646912 H 40.49066 l 9.63446,-19.992444 z"
|
||||
style="fill:#2f855a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.762401"
|
||||
id="path50" />
|
||||
</g>
|
||||
<g
|
||||
id="g52"
|
||||
transform="translate(320.9248,367.3867)">
|
||||
<path
|
||||
d="M 52.18699,-16.254503 27.28545,-67.966647 v -0.01906 h -37.57494 v 0.01906 l 18.7870903,39.002155 19.2506297,39.94677 5.65168,11.764611 5.66921,-11.764611 9.2647,-19.232331 z"
|
||||
style="fill:#2f855a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.762401"
|
||||
id="path54" />
|
||||
</g>
|
||||
<g
|
||||
id="g56"
|
||||
transform="translate(383.7656,359.833)">
|
||||
<path
|
||||
d="m 38.75609,-18.959752 -3.78456,-7.854257 -14.7982,-30.591345 -0.0282,-0.08539 -25.2979997,52.560696 -9.1358503,18.95253 -9.61769,20.005406 h 37.10759 z"
|
||||
style="fill:#2f855a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.762401"
|
||||
id="path58" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
aria-label="akapi"
|
||||
transform="matrix(0.35277777,0,0,0.35277777,-84.30991,101.5377)"
|
||||
id="text856"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:7.5px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.75">
|
||||
<path
|
||||
d="m 565.57956,-313.41107 q -1.375,-2.66406 -2.40625,-8.67969 -9.96875,10.39844 -24.40625,10.39844 -14.00781,0 -22.85938,-7.99219 -8.85156,-7.99219 -8.85156,-19.76562 0,-14.86719 11,-22.77344 11.08594,-7.99219 31.625,-7.99219 h 12.80469 v -6.10156 q 0,-7.21875 -4.03906,-11.51563 -4.03907,-4.38281 -12.28907,-4.38281 -7.13281,0 -11.6875,3.60938 -4.55468,3.52343 -4.55468,9.02343 h -20.88282 q 0,-7.64843 5.07032,-14.26562 5.07031,-6.70313 13.75,-10.48438 8.76562,-3.78125 19.50781,-3.78125 16.32812,0 26.03906,8.25 9.71094,8.16407 9.96875,23.03125 v 41.9375 q 0,12.54688 3.52344,20.02344 v 1.46094 z m -22.94531,-15.03906 q 6.1875,0 11.60156,-3.00782 5.5,-3.00781 8.25,-8.07812 v -17.53125 H 551.228 q -11.60157,0 -17.44532,4.03906 -5.84375,4.03906 -5.84375,11.42969 0,6.01562 3.95313,9.625 4.03906,3.52344 10.74219,3.52344 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.75"
|
||||
id="path851" />
|
||||
<path
|
||||
d="m 642.94675,-353.28607 -9.28125,9.53906 v 30.33594 h -20.88282 v -132 h 20.88282 v 76.14062 l 6.53125,-8.16406 25.69531,-28.96094 h 25.09375 l -34.54688,38.75782 38.24219,54.22656 h -24.14844 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.75"
|
||||
id="path853" />
|
||||
<path
|
||||
d="m 767.6655,-313.41107 q -1.375,-2.66406 -2.40625,-8.67969 -9.96875,10.39844 -24.40625,10.39844 -14.00782,0 -22.85938,-7.99219 -8.85156,-7.99219 -8.85156,-19.76562 0,-14.86719 11,-22.77344 11.08594,-7.99219 31.625,-7.99219 h 12.80469 v -6.10156 q 0,-7.21875 -4.03907,-11.51563 -4.03906,-4.38281 -12.28906,-4.38281 -7.13281,0 -11.6875,3.60938 -4.55469,3.52343 -4.55469,9.02343 h -20.88281 q 0,-7.64843 5.07031,-14.26562 5.07032,-6.70313 13.75,-10.48438 8.76563,-3.78125 19.50782,-3.78125 16.32812,0 26.03906,8.25 9.71094,8.16407 9.96875,23.03125 v 41.9375 q 0,12.54688 3.52344,20.02344 v 1.46094 z m -22.94532,-15.03906 q 6.1875,0 11.60157,-3.00782 5.5,-3.00781 8.25,-8.07812 v -17.53125 h -11.25782 q -11.60156,0 -17.44531,4.03906 -5.84375,4.03906 -5.84375,11.42969 0,6.01562 3.95313,9.625 4.03906,3.52344 10.74218,3.52344 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.75"
|
||||
id="path855" />
|
||||
<path
|
||||
d="m 896.25143,-358.95795 q 0,21.57032 -9.79687,34.46094 -9.79688,12.80469 -26.29688,12.80469 -15.29687,0 -24.49218,-10.05469 v 44.08594 h -20.88282 v -128.73438 h 19.25 l 0.85938,9.45313 q 9.19531,-11.17188 25.00781,-11.17188 17.01563,0 26.64063,12.71875 9.71093,12.63282 9.71093,35.14844 z m -20.79687,-1.80468 q 0,-13.92188 -5.58594,-22.08594 -5.5,-8.16406 -15.8125,-8.16406 -12.80469,0 -18.39062,10.57031 v 41.25 q 5.67187,10.82812 18.5625,10.82812 9.96875,0 15.55468,-7.99218 5.67188,-8.07813 5.67188,-24.40625 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.75"
|
||||
id="path857" />
|
||||
<path
|
||||
d="m 943.62643,-313.41107 h -20.88281 v -92.98438 h 20.88281 z m -22.17187,-117.13281 q 0,-4.8125 3.00781,-7.99219 3.09375,-3.17969 8.76563,-3.17969 5.67187,0 8.76562,3.17969 3.09375,3.17969 3.09375,7.99219 0,4.72656 -3.09375,7.90625 -3.09375,3.09375 -8.76562,3.09375 -5.67188,0 -8.76563,-3.09375 -3.00781,-3.17969 -3.00781,-7.90625 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.75"
|
||||
id="path859" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 10 KiB |
@@ -25,11 +25,6 @@ body {
|
||||
@apply hidden !important;
|
||||
}
|
||||
|
||||
.input,
|
||||
.select {
|
||||
@apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300;
|
||||
}
|
||||
|
||||
.input-sticky {
|
||||
@apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 dark:focus:ring-coolgray-300 focus:ring-neutral-400 block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset;
|
||||
}
|
||||
@@ -51,7 +46,11 @@ body {
|
||||
|
||||
.input,
|
||||
.select {
|
||||
@apply block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset;
|
||||
@apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset;
|
||||
}
|
||||
|
||||
.select {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.input[type="password"] {
|
||||
|
@@ -1,10 +1,3 @@
|
||||
// import { createApp } from "vue";
|
||||
// import MagicBar from "./components/MagicBar.vue";
|
||||
|
||||
// const app = createApp({});
|
||||
// app.component("magic-bar", MagicBar);
|
||||
// app.mount("#vue");
|
||||
|
||||
import { initializeTerminalComponent } from './terminal.js';
|
||||
|
||||
['livewire:navigated', 'alpine:init'].forEach((event) => {
|
||||
|
@@ -1,682 +0,0 @@
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div>
|
||||
<div class="flex items-center p-1 px-2 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-100"
|
||||
@click="showCommandPalette = true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
|
||||
<path d="M21 21l-6 -6" />
|
||||
</svg>
|
||||
<span class="flex-1"></span>
|
||||
<span class="ml-2 kbd-custom">/</span>
|
||||
</div>
|
||||
<div class="relative" role="dialog" aria-modal="true" v-if="showCommandPalette" @keyup.esc="resetState">
|
||||
<div class="fixed inset-0 transition-opacity bg-opacity-90 bg-coolgray-100" @click.self="resetState">
|
||||
</div>
|
||||
<div class="fixed inset-0 p-4 mx-auto overflow-y-auto lg:w-[70rem] sm:p-10 md:px-20"
|
||||
@click.self="resetState">
|
||||
<div class="overflow-hidden transition-all transform bg-coolgray-200 ring-1 ring-black ring-opacity-5">
|
||||
<div class="relative">
|
||||
<svg class="absolute w-5 h-5 text-gray-400 pointer-events-none left-3 top-2.5"
|
||||
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<input type="text" v-model="search" ref="searchInput" @keydown.down="focusNext(magic.length)"
|
||||
@keydown.up="focusPrev(magic.length)" @keyup.enter="callAction"
|
||||
class="w-full h-10 pr-4 rounded outline-none dark:text-white bg-coolgray-400 pl-11 placeholder:text-neutral-700 sm:text-sm focus:outline-none"
|
||||
placeholder="Search, jump or create... magically... 🪄" role="combobox"
|
||||
aria-expanded="false" aria-controls="options">
|
||||
</div>
|
||||
|
||||
<ul class="px-4 pb-2 overflow-y-auto max-h-96 scroll-py-10 scroll-pb-2 scrollbar" id="options"
|
||||
role="listbox">
|
||||
<li v-if="sequenceState.sequence.length !== 0">
|
||||
<h2 v-if="sequenceState.sequence[sequenceState.currentActionIndex] && possibleSequences[sequenceState.sequence[sequenceState.currentActionIndex]]"
|
||||
class="mt-4 mb-2 text-xs font-semibold text-neutral-500">{{
|
||||
possibleSequences[sequenceState.sequence[sequenceState.currentActionIndex]].newTitle }}
|
||||
</h2>
|
||||
<ul class="mt-2 -mx-4 dark:text-white">
|
||||
<li class="flex items-center px-4 py-2 cursor-pointer select-none group hover:bg-coolgray-400"
|
||||
id="option-1" role="option" tabindex="-1"
|
||||
@click="addNew(sequenceState.sequence[sequenceState.currentActionIndex])">
|
||||
<svg xmlns=" http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 5l0 14" />
|
||||
<path d="M5 12l14 0" />
|
||||
</svg>
|
||||
<span class="flex-auto ml-3 truncate">
|
||||
<span v-if="search"><span class="capitalize ">{{
|
||||
sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name
|
||||
will be:
|
||||
<span class="inline-block dark:text-warning">{{ search }}</span>
|
||||
</span>
|
||||
<span v-else><span class="capitalize ">{{
|
||||
sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name
|
||||
will be:
|
||||
<span class="inline-block dark:text-warning">randomly generated (type to
|
||||
change)</span>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul v-if="magic.length == 0" class="mt-2 -mx-4 dark:text-white">
|
||||
<li class="flex items-center px-4 py-2 select-none group">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M9 10l.01 0" />
|
||||
<path d="M15 10l.01 0" />
|
||||
<path d="M9 15l6 0" />
|
||||
</svg>
|
||||
<span class="flex-auto ml-3 truncate">Nothing found. Ooops.</span>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 v-if="magic.length !== 0 && sequenceState.sequence[sequenceState.currentActionIndex] && possibleSequences[sequenceState.sequence[sequenceState.currentActionIndex]]"
|
||||
class="mt-4 mb-2 text-xs font-semibold text-neutral-500">{{
|
||||
possibleSequences[sequenceState.sequence[sequenceState.currentActionIndex]].title }}
|
||||
</h2>
|
||||
<ul v-if="magic.length != 0" class="mt-2 -mx-4 dark:text-white">
|
||||
<li class="flex items-center px-4 py-2 transition-all cursor-pointer select-none group hover:bg-coolgray-400"
|
||||
:class="{ 'bg-coollabs': currentFocus === index }" id="option-1" role="option"
|
||||
tabindex="-1" v-for="action, index in magic" @click="goThroughSequence(index)"
|
||||
ref="magicItems">
|
||||
<div class="relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<template
|
||||
v-if="action.icon === 'git' || sequenceState.sequence[sequenceState.currentActionIndex] === 'git'">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M16 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 8m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 16m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 15v-6" />
|
||||
<path d="M15 11l-2 -2" />
|
||||
<path d="M11 7l-1.9 -1.9" />
|
||||
<path
|
||||
d="M13.446 2.6l7.955 7.954a2.045 2.045 0 0 1 0 2.892l-7.955 7.955a2.045 2.045 0 0 1 -2.892 0l-7.955 -7.955a2.045 2.045 0 0 1 0 -2.892l7.955 -7.955a2.045 2.045 0 0 1 2.892 0z" />
|
||||
</template>
|
||||
<template
|
||||
v-if="action.icon === 'server' || sequenceState.sequence[sequenceState.currentActionIndex] === 'server'">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z" />
|
||||
<path d="M15 20h-9a3 3 0 0 1 -3 -3v-2a3 3 0 0 1 3 -3h12" />
|
||||
<path d="M7 8v.01" />
|
||||
<path d="M7 16v.01" />
|
||||
<path d="M20 15l-2 3h3l-2 3" />
|
||||
</template>
|
||||
<template
|
||||
v-if="action.icon === 'destination' || sequenceState.sequence[sequenceState.currentActionIndex] === 'destination'">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z" />
|
||||
<path d="M5 10h3v3h-3z" />
|
||||
<path d="M8 10h3v3h-3z" />
|
||||
<path d="M11 10h3v3h-3z" />
|
||||
<path d="M8 7h3v3h-3z" />
|
||||
<path d="M11 7h3v3h-3z" />
|
||||
<path d="M11 4h3v3h-3z" />
|
||||
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
|
||||
<path d="M10 16l0 .01" />
|
||||
</template>
|
||||
<template
|
||||
v-if="action.icon === 'storage' || sequenceState.sequence[sequenceState.currentActionIndex] === 'storage'">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M4 6a8 3 0 1 0 16 0A8 3 0 1 0 4 6" />
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0V6" />
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||
</g>
|
||||
</template>
|
||||
<template
|
||||
v-if="action.icon === 'project' || sequenceState.sequence[sequenceState.currentActionIndex] === 'project'">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M9 4h3l2 2h5a2 2 0 0 1 2 2v7a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-9a2 2 0 0 1 2 -2" />
|
||||
<path
|
||||
d="M17 17v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-9a2 2 0 0 1 2 -2h2" />
|
||||
</template>
|
||||
<template
|
||||
v-if="action.icon === 'environment' || sequenceState.sequence[sequenceState.currentActionIndex] === 'environment'">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M16 5l3 3l-2 1l4 4l-3 1l4 4h-9" />
|
||||
<path d="M15 21l0 -3" />
|
||||
<path d="M8 13l-2 -2" />
|
||||
<path d="M8 12l2 -2" />
|
||||
<path d="M8 21v-13" />
|
||||
<path
|
||||
d="M5.824 16a3 3 0 0 1 -2.743 -3.69a3 3 0 0 1 .304 -4.833a3 3 0 0 1 4.615 -3.707a3 3 0 0 1 4.614 3.707a3 3 0 0 1 .305 4.833a3 3 0 0 1 -2.919 3.695h-4z" />
|
||||
</template>
|
||||
<template
|
||||
v-if="action.icon === 'key' || sequenceState.sequence[sequenceState.currentActionIndex] === 'key'">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M14 10m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M21 12a9 9 0 1 1 -18 0a9 9 0 0 1 18 0z" />
|
||||
<path d="M12.5 11.5l-4 4l1.5 1.5" />
|
||||
<path d="M12 15l-1.5 -1.5" />
|
||||
</template>
|
||||
<template
|
||||
v-if="action.icon === 'goto' || sequenceState.sequence[sequenceState.currentActionIndex] === 'goto'">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 18h4" />
|
||||
<path
|
||||
d="M3 8a9 9 0 0 1 9 9v1l1.428 -4.285a12 12 0 0 1 6.018 -6.938l.554 -.277" />
|
||||
<path d="M15 6h5v5" />
|
||||
</template>
|
||||
<template
|
||||
v-if="action.icon === 'team' || sequenceState.sequence[sequenceState.currentActionIndex] === 'team'">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1" />
|
||||
<path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M17 10h2a2 2 0 0 1 2 2v1" />
|
||||
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
|
||||
</template>
|
||||
</svg>
|
||||
<div v-if="action.new"
|
||||
class="absolute top-0 right-0 -mt-2 -mr-2 font-bold dark:text-warning">+
|
||||
</div>
|
||||
</div>
|
||||
<span class="flex-auto ml-3 truncate">{{ action.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import axios from "axios";
|
||||
const currentFocus = ref(0)
|
||||
const magicItems = ref()
|
||||
function focusNext(length) {
|
||||
if (currentFocus.value === undefined) {
|
||||
currentFocus.value = 0
|
||||
} else {
|
||||
if (length > currentFocus.value + 1) {
|
||||
currentFocus.value = currentFocus.value + 1
|
||||
}
|
||||
}
|
||||
if (currentFocus.value > 4) {
|
||||
magicItems.value[currentFocus.value].scrollIntoView({ block: "center", inline: "center", behavior: 'auto' })
|
||||
}
|
||||
}
|
||||
function focusPrev(length) {
|
||||
if (currentFocus.value === undefined) {
|
||||
currentFocus.value = length - 1
|
||||
} else {
|
||||
if (currentFocus.value > 0) {
|
||||
currentFocus.value = currentFocus.value - 1
|
||||
}
|
||||
}
|
||||
if (currentFocus.value < length - 4) {
|
||||
magicItems.value[currentFocus.value].scrollIntoView({ block: "center", inline: "center", behavior: 'auto' })
|
||||
}
|
||||
}
|
||||
async function callAction() {
|
||||
await goThroughSequence(currentFocus.value)
|
||||
}
|
||||
const showCommandPalette = ref(false)
|
||||
const search = ref()
|
||||
const searchInput = ref()
|
||||
|
||||
const baseUrl = '/magic'
|
||||
|
||||
const uuidSelector = ['project', 'destination']
|
||||
const nameSelector = ['environment']
|
||||
const possibleSequences = {
|
||||
server: {
|
||||
newTitle: 'Create a new Server',
|
||||
title: 'Select a server'
|
||||
},
|
||||
destination: {
|
||||
newTitle: 'Create a new Destination',
|
||||
title: 'Select a destination'
|
||||
},
|
||||
project: {
|
||||
newTitle: 'Create a new Project',
|
||||
title: 'Select a project'
|
||||
},
|
||||
environment: {
|
||||
newTitle: 'Create a new Environment',
|
||||
title: 'Select an environment'
|
||||
},
|
||||
}
|
||||
const magicActions = [{
|
||||
id: 1,
|
||||
name: 'Deploy: Public Repository',
|
||||
tags: 'git,github,public',
|
||||
icon: 'git',
|
||||
new: true,
|
||||
sequence: ['main', 'server', 'destination', 'project', 'environment', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Deploy: Private Repository (with GitHub Apps)',
|
||||
tags: 'git,github,private',
|
||||
icon: 'git',
|
||||
new: true,
|
||||
sequence: ['main', 'server', 'destination', 'project', 'environment', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Deploy: Private Repository (with Deploy Key)',
|
||||
tags: 'git,github,private,deploy,key',
|
||||
icon: 'git',
|
||||
new: true,
|
||||
sequence: ['main', 'server', 'destination', 'project', 'environment', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Deploy: Dockerfile',
|
||||
tags: 'dockerfile,deploy',
|
||||
icon: 'destination',
|
||||
new: true,
|
||||
sequence: ['main', 'server', 'destination', 'project', 'environment', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Create: Server',
|
||||
tags: 'server,ssh,new,create',
|
||||
icon: 'server',
|
||||
new: true,
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Create: Source',
|
||||
tags: 'source,git,gitlab,github,bitbucket,gitea,new,create',
|
||||
icon: 'git',
|
||||
new: true,
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Create: Private Key',
|
||||
tags: 'private,key,ssh,new,create',
|
||||
icon: 'key',
|
||||
new: true,
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Create: Destination',
|
||||
tags: 'destination,docker,network,new,create',
|
||||
icon: 'destination',
|
||||
new: true,
|
||||
sequence: ['main', 'server', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Create: Team',
|
||||
tags: 'team,member,new,create',
|
||||
icon: 'team',
|
||||
new: true,
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Create: S3 Storage',
|
||||
tags: 's3,storage,new,create',
|
||||
icon: 'storage',
|
||||
new: true,
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'Goto: S3 Storage',
|
||||
tags: 's3,storage',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Goto: Dashboard',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'Goto: Servers',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: 'Goto: Private Keys',
|
||||
tags: 'destination,docker,network,new,create,ssh,private,key',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'Goto: Projects',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'Goto: Sources',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: 'Goto: Destinations',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'Goto: Settings',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: 'Goto: Terminal',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: 'Goto: Notifications',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: 'Goto: Profile',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: 'Goto: Teams',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: 'Goto: Switch Teams',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: 'Goto: Onboarding process',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 25,
|
||||
name: 'Goto: API Tokens',
|
||||
tags: 'api,tokens,rest',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
},
|
||||
{
|
||||
id: 26,
|
||||
name: 'Goto: Team Shared Variables',
|
||||
tags: 'team,shared,variables',
|
||||
icon: 'goto',
|
||||
sequence: ['main', 'redirect']
|
||||
}
|
||||
]
|
||||
const initialState = {
|
||||
sequence: [],
|
||||
currentActionIndex: 0,
|
||||
magicActions,
|
||||
selected: {}
|
||||
}
|
||||
const sequenceState = ref({ ...initialState })
|
||||
|
||||
function focusSearch(event) {
|
||||
if (event.target.nodeName === 'BODY') {
|
||||
if (event.key === '/') {
|
||||
event.preventDefault();
|
||||
showCommandPalette.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", focusSearch);
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", focusSearch);
|
||||
})
|
||||
|
||||
watch(showCommandPalette, async (value) => {
|
||||
if (value) {
|
||||
await nextTick();
|
||||
searchInput.value.focus();
|
||||
}
|
||||
})
|
||||
watch(search, async () => {
|
||||
currentFocus.value = 0
|
||||
})
|
||||
const magic = computed(() => {
|
||||
if (search.value) {
|
||||
return sequenceState.value.magicActions.filter(action => {
|
||||
return action.name.toLowerCase().includes(search.value.toLowerCase()) || action.tags?.toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
}
|
||||
return sequenceState.value.magicActions
|
||||
})
|
||||
async function addNew(name) {
|
||||
let targetUrl = new URL(window.location.origin)
|
||||
let newUrl = new URL(`${window.location.origin}${baseUrl}/${name}/new`);
|
||||
if (search.value) {
|
||||
targetUrl.searchParams.append('name', search.value)
|
||||
newUrl.searchParams.append('name', search.value)
|
||||
}
|
||||
switch (name) {
|
||||
case 'server':
|
||||
targetUrl.pathname = '/server/new'
|
||||
window.location.href = targetUrl.href
|
||||
break;
|
||||
case 'destination':
|
||||
targetUrl.pathname = '/destination/new'
|
||||
window.location.href = targetUrl.href
|
||||
break;
|
||||
case 'project':
|
||||
const { data: { project_uuid } } = await axios(newUrl.href)
|
||||
search.value = ''
|
||||
sequenceState.value.selected['project'] = project_uuid
|
||||
sequenceState.value.magicActions = await getEnvironments(project_uuid)
|
||||
sequenceState.value.currentActionIndex += 1
|
||||
break;
|
||||
case 'environment':
|
||||
newUrl.searchParams.append('project_uuid', sequenceState.value.selected.project)
|
||||
const { data: { environment_name } } = await axios(newUrl.href)
|
||||
search.value = ''
|
||||
sequenceState.value.selected['environment'] = environment_name
|
||||
redirect()
|
||||
break;
|
||||
}
|
||||
}
|
||||
function resetState() {
|
||||
showCommandPalette.value = false
|
||||
sequenceState.value = { ...initialState }
|
||||
search.value = ''
|
||||
}
|
||||
async function goThroughSequence(actionId) {
|
||||
let currentSequence = null;
|
||||
let nextSequence = null;
|
||||
if (sequenceState.value.selected.main === undefined) {
|
||||
const { sequence, id } = magic.value[actionId];
|
||||
currentSequence = sequence[sequenceState.value.currentActionIndex]
|
||||
nextSequence = sequence[sequenceState.value.currentActionIndex + 1]
|
||||
sequenceState.value.sequence = sequence
|
||||
sequenceState.value.selected = {
|
||||
main: id
|
||||
}
|
||||
} else {
|
||||
currentSequence = sequenceState.value.sequence[sequenceState.value.currentActionIndex]
|
||||
nextSequence = sequenceState.value.sequence[sequenceState.value.currentActionIndex + 1]
|
||||
let selectedId = sequenceState.value.magicActions[actionId].id
|
||||
if (uuidSelector.includes(currentSequence)) {
|
||||
selectedId = sequenceState.value.magicActions[actionId].uuid
|
||||
}
|
||||
if (nameSelector.includes(currentSequence)) {
|
||||
selectedId = sequenceState.value.magicActions[actionId].name
|
||||
}
|
||||
sequenceState.value.selected = {
|
||||
...sequenceState.value.selected,
|
||||
[currentSequence]: selectedId
|
||||
}
|
||||
}
|
||||
switch (nextSequence) {
|
||||
case 'server':
|
||||
sequenceState.value.magicActions = await getServers();
|
||||
break;
|
||||
case 'destination':
|
||||
sequenceState.value.magicActions = await getDestinations(sequenceState.value.selected[currentSequence]);
|
||||
break;
|
||||
case 'project':
|
||||
sequenceState.value.magicActions = await getProjects()
|
||||
break;
|
||||
case 'environment':
|
||||
sequenceState.value.magicActions = await getEnvironments(sequenceState.value.selected[currentSequence])
|
||||
break;
|
||||
case 'redirect':
|
||||
redirect()
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
sequenceState.value.currentActionIndex += 1
|
||||
search.value = ''
|
||||
searchInput.value.focus()
|
||||
currentFocus.value = 0
|
||||
}
|
||||
async function getServers() {
|
||||
const { data: { servers } } = await axios.get(`${baseUrl}/servers`);
|
||||
return servers;
|
||||
}
|
||||
async function getDestinations(serverId) {
|
||||
const { data: { destinations } } = await axios.get(`${baseUrl}/destinations?server_id=${serverId}`);
|
||||
return destinations;
|
||||
}
|
||||
async function getProjects() {
|
||||
const { data: { projects } } = await axios.get(`${baseUrl}/projects`);
|
||||
return projects;
|
||||
}
|
||||
async function getEnvironments(project_uuid) {
|
||||
const { data: { environments } } = await axios.get(`${baseUrl}/environments?project_uuid=${project_uuid}`);
|
||||
return environments;
|
||||
}
|
||||
|
||||
async function redirect() {
|
||||
let targetUrl = new URL(window.location.origin)
|
||||
const selected = sequenceState.value.selected
|
||||
const { main, destination = null, project = null, environment = null, server = null } = selected
|
||||
switch (main) {
|
||||
case 1:
|
||||
targetUrl.pathname = `/project/${project}/${environment}/new`
|
||||
targetUrl.searchParams.append('type', 'public')
|
||||
targetUrl.searchParams.append('destination', destination)
|
||||
break;
|
||||
case 2:
|
||||
targetUrl.pathname = `/project/${project}/${environment}/new`
|
||||
targetUrl.searchParams.append('type', 'private-gh-app')
|
||||
targetUrl.searchParams.append('destination', destination)
|
||||
break;
|
||||
case 3:
|
||||
targetUrl.pathname = `/project/${project}/${environment}/new`
|
||||
targetUrl.searchParams.append('type', 'private-deploy-key')
|
||||
targetUrl.searchParams.append('destination', destination)
|
||||
break;
|
||||
case 4:
|
||||
targetUrl.pathname = `/project/${project}/${environment}/new`
|
||||
targetUrl.searchParams.append('type', 'dockerfile')
|
||||
targetUrl.searchParams.append('destination', destination)
|
||||
break;
|
||||
case 5:
|
||||
targetUrl.pathname = `/server/new`
|
||||
break;
|
||||
case 6:
|
||||
targetUrl.pathname = `/source/new`
|
||||
break;
|
||||
case 7:
|
||||
targetUrl.pathname = `/security/private-key/new`
|
||||
break;
|
||||
case 8:
|
||||
targetUrl.pathname = `/destination/new`
|
||||
targetUrl.searchParams.append('server', server)
|
||||
break;
|
||||
case 9:
|
||||
targetUrl.pathname = `/team/new`
|
||||
break;
|
||||
case 10:
|
||||
targetUrl.pathname = `/team/storages/new`
|
||||
break;
|
||||
case 11:
|
||||
targetUrl.pathname = `/team/storages/`
|
||||
break;
|
||||
case 12:
|
||||
targetUrl.pathname = `/`
|
||||
break;
|
||||
case 13:
|
||||
targetUrl.pathname = `/servers`
|
||||
break;
|
||||
case 14:
|
||||
targetUrl.pathname = `/security/private-key`
|
||||
break;
|
||||
case 15:
|
||||
targetUrl.pathname = `/projects`
|
||||
break;
|
||||
case 16:
|
||||
targetUrl.pathname = `/sources`
|
||||
break;
|
||||
case 17:
|
||||
targetUrl.pathname = `/destinations`
|
||||
break;
|
||||
case 18:
|
||||
targetUrl.pathname = `/settings`
|
||||
break;
|
||||
case 19:
|
||||
targetUrl.pathname = `/terminal`
|
||||
break;
|
||||
case 20:
|
||||
targetUrl.pathname = `/team/notifications`
|
||||
break;
|
||||
case 21:
|
||||
targetUrl.pathname = `/profile`
|
||||
break;
|
||||
case 22:
|
||||
targetUrl.pathname = `/team`
|
||||
break;
|
||||
case 23:
|
||||
targetUrl.pathname = `/team`
|
||||
break;
|
||||
case 24:
|
||||
targetUrl.pathname = `/onboarding`
|
||||
break;
|
||||
case 25:
|
||||
targetUrl.pathname = `/security/api-tokens`
|
||||
break;
|
||||
case 26:
|
||||
targetUrl.pathname = `/team/shared-variables`
|
||||
break;
|
||||
}
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
</script>
|
17
resources/views/components/forms/copy-button.blade.php
Normal file
17
resources/views/components/forms/copy-button.blade.php
Normal file
@@ -0,0 +1,17 @@
|
||||
@props(['text'])
|
||||
|
||||
<div class="relative" x-data="{ copied: false }">
|
||||
<input type="text" value="{{ $text }}" readonly class="input">
|
||||
<button
|
||||
@click.prevent="copied = true; navigator.clipboard.writeText('{{ $text }}'); setTimeout(() => copied = false, 1000)"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-300 transition-colors"
|
||||
title="Copy to clipboard">
|
||||
<svg x-show="!copied" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<svg x-show="copied" class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
@@ -40,7 +40,6 @@
|
||||
userConfirmationText: '',
|
||||
confirmWithText: @js($confirmWithText && !$disableTwoStepConfirmation),
|
||||
confirmWithPassword: @js($confirmWithPassword && !$disableTwoStepConfirmation),
|
||||
copied: false,
|
||||
submitAction: @js($submitAction),
|
||||
passwordError: '',
|
||||
selectedActions: @js(collect($checkboxes)->pluck('id')->filter(fn($id) => $this->$id)->values()->all()),
|
||||
@@ -91,13 +90,6 @@
|
||||
}
|
||||
});
|
||||
},
|
||||
copyConfirmationText() {
|
||||
navigator.clipboard.writeText(this.confirmationText);
|
||||
this.copied = true;
|
||||
setTimeout(() => {
|
||||
this.copied = false;
|
||||
}, 2000);
|
||||
},
|
||||
toggleAction(id) {
|
||||
const index = this.selectedActions.indexOf(id);
|
||||
if (index > -1) {
|
||||
@@ -255,29 +247,9 @@
|
||||
<h4 class="mb-2 text-lg font-semibold">Confirm Actions</h4>
|
||||
<p class="mb-2 text-sm">{{ $confirmationLabel }}</p>
|
||||
<div class="relative mb-2">
|
||||
<input type="text" x-model="confirmationText"
|
||||
class="p-2 pr-10 w-full text-black rounded cursor-text input" readonly>
|
||||
<button @click="copyConfirmationText()"
|
||||
x-show="window.isSecureContext"
|
||||
class="absolute right-2 top-1/2 text-gray-500 transform -translate-y-1/2 hover:text-gray-700"
|
||||
title="Copy confirmation text" x-ref="copyButton">
|
||||
<template x-if="!copied">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
||||
<path
|
||||
d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="copied">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-green-500"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
</button>
|
||||
<x-forms.copy-button
|
||||
text="{{ $confirmationText }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label for="userConfirmationText"
|
||||
|
@@ -1,6 +1,11 @@
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a wire:navigate class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}"
|
||||
href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}">General</a>
|
||||
@if ($server->isFunctional())
|
||||
<a wire:navigate class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}"
|
||||
href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}">Advanced
|
||||
</a>
|
||||
@endif
|
||||
<a wire:navigate class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}"
|
||||
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key
|
||||
</a>
|
||||
@@ -16,9 +21,6 @@
|
||||
<a wire:navigate class="menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}"
|
||||
href="{{ route('server.destinations', ['server_uuid' => $server->uuid]) }}">Destinations
|
||||
</a>
|
||||
<a wire:navigate class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}"
|
||||
href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}">Advanced
|
||||
</a>
|
||||
<a wire:navigate class="menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}"
|
||||
href="{{ route('server.log-drains', ['server_uuid' => $server->uuid]) }}">Log
|
||||
Drains</a>
|
||||
|
28
resources/views/emails/ssl-certificate-renewed.blade.php
Normal file
28
resources/views/emails/ssl-certificate-renewed.blade.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<x-emails.layout>
|
||||
<h2>SSL Certificates Renewed</h2>
|
||||
|
||||
<p>SSL certificates have been renewed for the following resources:</p>
|
||||
|
||||
<ul>
|
||||
@foreach($resources as $resource)
|
||||
<li>{{ $resource->name }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
<div style="margin: 20px 0; padding: 15px; background-color: #fff3cd; border: 1px solid #ffeeba; border-radius: 4px;">
|
||||
<strong>⚠️ Action Required:</strong> These resources need to be redeployed manually for the new SSL certificates to take effect. Please do this in the next few days to ensure your database connections remain accessible.
|
||||
</div>
|
||||
|
||||
<p>The old SSL certificates will remain valid for approximately 14 more days, as we renew certificates 14 days before their expiration.</p>
|
||||
|
||||
@if(isset($urls) && count($urls) > 0)
|
||||
<div style="margin-top: 20px;">
|
||||
<p>You can redeploy these resources here:</p>
|
||||
<ul>
|
||||
@foreach($urls as $name => $url)
|
||||
<li><a href="{{ $url }}">{{ $name }}</a></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</x-emails.layout>
|
@@ -2,7 +2,7 @@
|
||||
<div class="flex flex-col items-center justify-center h-full">
|
||||
<div>
|
||||
<p class="font-mono font-semibold text-7xl dark:text-warning">404</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">How did you got here?</h1>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">How did you get here?</h1>
|
||||
<p class="text-base leading-7 text-neutral-300">Sorry, we couldn’t find the page you’re looking
|
||||
for.
|
||||
</p>
|
||||
|
@@ -45,52 +45,14 @@
|
||||
</div>
|
||||
<div x-data="{
|
||||
showCode: false,
|
||||
secretKey: '{{ decrypt(request()->user()->two_factor_secret) }}',
|
||||
otpUrl: '{{ request()->user()->twoFactorQrCodeUrl() }}',
|
||||
copiedSecretKey: false,
|
||||
copiedOtpUrl: false
|
||||
}" class="py-4 w-full">
|
||||
<div class="flex flex-col gap-2" x-show="showCode">
|
||||
<div class="relative">
|
||||
<x-forms.input
|
||||
x-model="secretKey"
|
||||
label="Secret Key"
|
||||
readonly
|
||||
class="font-mono pr-10"
|
||||
/>
|
||||
<button
|
||||
x-show="window.isSecureContext"
|
||||
@click="navigator.clipboard.writeText(secretKey); copiedSecretKey = true; setTimeout(() => copiedSecretKey = false, 2000)"
|
||||
class="absolute right-2 bottom-1 p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
<svg x-show="!copiedSecretKey" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
||||
</svg>
|
||||
<svg x-show="copiedSecretKey" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-green-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative" >
|
||||
<x-forms.input
|
||||
x-model="otpUrl"
|
||||
label="OTP URL"
|
||||
readonly
|
||||
class="font-mono pr-10"
|
||||
/>
|
||||
<button
|
||||
x-show="window.isSecureContext"
|
||||
@click="navigator.clipboard.writeText(otpUrl); copiedOtpUrl = true; setTimeout(() => copiedOtpUrl = false, 2000)"
|
||||
class="absolute right-2 bottom-1 p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
<svg x-show="!copiedOtpUrl" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
||||
</svg>
|
||||
<svg x-show="copiedOtpUrl" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-green-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 pb-2" x-show="showCode">
|
||||
<x-forms.copy-button
|
||||
text="{{ decrypt(request()->user()->two_factor_secret) }}"
|
||||
/>
|
||||
<x-forms.copy-button
|
||||
text="{{ request()->user()->twoFactorQrCodeUrl() }}"
|
||||
/>
|
||||
</div>
|
||||
<x-forms.button x-on:click="showCode = !showCode" class="mt-2">
|
||||
<span x-text="showCode ? 'Hide Secret Key and OTP URL' : 'Show Secret Key and OTP URL'"></span>
|
||||
|
@@ -58,12 +58,9 @@
|
||||
<x-forms.checkbox instantSave id="isConnectToDockerNetworkEnabled" label="Connect To Predefined Network"
|
||||
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>." />
|
||||
@endif
|
||||
@if ($isLogDrainEnabled === false)
|
||||
<h3 class="pt-4">Logs</h3>
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave id="isLogDrainEnabled" label="Drain Logs" />
|
||||
@endif
|
||||
|
||||
<h3 class="pt-4">Logs</h3>
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave id="isLogDrainEnabled" label="Drain Logs" />
|
||||
@if ($application->git_based())
|
||||
<h3>Git</h3>
|
||||
<x-forms.checkbox instantSave id="isGitSubmodulesEnabled" label="Submodules"
|
||||
|
@@ -15,8 +15,7 @@
|
||||
href="{{ route('project.application.advanced', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
|
||||
wire:navigate>Advanced</a>
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<a class="menu-item"
|
||||
wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
|
||||
wire:navigate>Swarm Configuration</a>
|
||||
@endif
|
||||
@@ -60,21 +59,22 @@
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.preview-deployments', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
|
||||
wire:navigate>Preview Deployments</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.healthcheck', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
|
||||
wire:navigate>Healthcheck</a>
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.healthcheck', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
|
||||
wire:navigate>Healthcheck</a>
|
||||
@endif
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.rollback', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
|
||||
wire:navigate>Rollback</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
|
||||
wire:navigate>Resource Limits</a>
|
||||
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
|
||||
wire:navigate>Resource Operations</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}" >Metrics</a>
|
||||
href="{{ route('project.application.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Metrics</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
|
||||
wire:navigate>Tags</a>
|
||||
@@ -103,7 +103,7 @@
|
||||
<livewire:project.shared.webhooks :resource="$application" />
|
||||
@elseif ($currentRoute === 'project.application.preview-deployments')
|
||||
<livewire:project.application.previews :application="$application" />
|
||||
@elseif ($currentRoute === 'project.application.healthcheck')
|
||||
@elseif ($currentRoute === 'project.application.healthcheck' && $application->build_pack !== 'dockercompose')
|
||||
<livewire:project.shared.health-checks :resource="$application" />
|
||||
@elseif ($currentRoute === 'project.application.rollback')
|
||||
<livewire:project.application.rollback :application="$application" />
|
||||
|
@@ -185,7 +185,7 @@
|
||||
</div>
|
||||
<div class="pt-1 text-xs">Nixpacks will detect the required configuration
|
||||
automatically.
|
||||
<a class="underline" href="https://coolify.io/docs/applications">Framework
|
||||
<a class="underline" href="https://coolify.io/docs/applications/">Framework
|
||||
Specific Docs</a>
|
||||
</div>
|
||||
@endif
|
||||
|
@@ -54,6 +54,49 @@
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Location: {{ data_get($execution, 'filename', 'N/A') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mt-2">
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Backup Availability:
|
||||
</div>
|
||||
<span @class([
|
||||
'px-2 py-1 rounded text-xs font-medium',
|
||||
'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200' => !data_get($execution, 'local_storage_deleted', false),
|
||||
'bg-gray-100 text-gray-600 dark:bg-gray-800/50 dark:text-gray-400' => data_get($execution, 'local_storage_deleted', false),
|
||||
])>
|
||||
<span class="flex items-center gap-1">
|
||||
@if(!data_get($execution, 'local_storage_deleted', false))
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
@else
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
@endif
|
||||
Local Storage
|
||||
</span>
|
||||
</span>
|
||||
@if($backup->save_s3)
|
||||
<span @class([
|
||||
'px-2 py-1 rounded text-xs font-medium',
|
||||
'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200' => !data_get($execution, 's3_storage_deleted', false),
|
||||
'bg-gray-100 text-gray-600 dark:bg-gray-800/50 dark:text-gray-400' => data_get($execution, 's3_storage_deleted', false),
|
||||
])>
|
||||
<span class="flex items-center gap-1">
|
||||
@if(!data_get($execution, 's3_storage_deleted', false))
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
@else
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
@endif
|
||||
S3 Storage
|
||||
</span>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@if (data_get($execution, 'message'))
|
||||
<div class="mt-2 p-2 bg-gray-100 dark:bg-coolgray-200 rounded">
|
||||
<pre class="whitespace-pre-wrap text-sm">{{ data_get($execution, 'message') }}</pre>
|
||||
|
@@ -49,6 +49,37 @@
|
||||
readonly value="Starting the database will generate this." />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.checkbox id="enable_ssl" label="Enable SSL" wire:model.live="enable_ssl"
|
||||
instantSave="instantSaveSSL" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
|
@@ -49,6 +49,40 @@
|
||||
readonly value="Starting the database will generate this." />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if($database->enable_ssl && $certificateValidUntil)
|
||||
<x-modal-confirmation
|
||||
title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates"
|
||||
:actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.'
|
||||
]"
|
||||
submitAction="regenerateSslCertificate"
|
||||
:confirmWithText="false"
|
||||
:confirmWithPassword="false"
|
||||
/>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if($database->enable_ssl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if(now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.checkbox id="enable_ssl" label="Enable SSL" wire:model.live="enable_ssl" instantSave="instantSaveSSL" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
|
@@ -65,6 +65,41 @@
|
||||
type="password" readonly wire:model="db_url_public" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if($database->enable_ssl && $certificateValidUntil)
|
||||
<x-modal-confirmation
|
||||
title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates"
|
||||
:actions="['The SSL certificate of this database will be regenerated.','You must restart the database after regenerating the certificate to start using the new certificate.']"
|
||||
submitAction="regenerateSslCertificate"
|
||||
:confirmWithText="false"
|
||||
:confirmWithPassword="false"
|
||||
/>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if($database->enable_ssl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if(now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.checkbox id="database.enable_ssl" label="Enable SSL" wire:model.live="database.enable_ssl" instantSave="instantSaveSSL" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
|
@@ -55,6 +55,53 @@
|
||||
type="password" readonly wire:model="db_url_public" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($database->enable_ssl)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.checkbox id="database.enable_ssl" label="Enable SSL" wire:model.live="database.enable_ssl"
|
||||
instantSave="instantSaveSSL" />
|
||||
@if ($database->enable_ssl)
|
||||
<div class="mx-2">
|
||||
<x-forms.select id="database.ssl_mode" label="SSL Mode" wire:model.live="database.ssl_mode"
|
||||
instantSave="instantSaveSSL"
|
||||
helper="Choose the SSL verification mode for MongoDB connections">
|
||||
<option value="allow" title="Allow insecure connections">allow (insecure)</option>
|
||||
<option value="prefer" title="Prefer secure connections">prefer (secure)</option>
|
||||
<option value="require" title="Require secure connections">require (secure)</option>
|
||||
<option value="verify-full" title="Verify full certificate">verify-full (secure)</option>
|
||||
</x-forms.select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
|
@@ -65,6 +65,54 @@
|
||||
type="password" readonly wire:model="db_url_public" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.checkbox id="database.enable_ssl" label="Enable SSL" wire:model.live="database.enable_ssl"
|
||||
instantSave="instantSaveSSL" />
|
||||
@if ($database->enable_ssl)
|
||||
<div class="mx-2">
|
||||
<x-forms.select id="database.ssl_mode" label="SSL Mode" wire:model.live="database.ssl_mode"
|
||||
instantSave="instantSaveSSL"
|
||||
helper="Choose the SSL verification mode for MySQL connections">
|
||||
<option value="PREFERRED" title="Prefer secure connections">Prefer (secure)</option>
|
||||
<option value="REQUIRED" title="Require secure connections">Require (secure)</option>
|
||||
<option value="VERIFY_CA" title="Verify CA certificate">Verify CA (secure)</option>
|
||||
<option value="VERIFY_IDENTITY" title="Verify full certificate">Verify Full (secure)
|
||||
</option>
|
||||
</x-forms.select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
|
@@ -73,58 +73,112 @@
|
||||
type="password" readonly wire:model="db_url_public" />
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<div class="flex items-center">
|
||||
<h3>Proxy</h3>
|
||||
<x-loading wire:loading wire:target="instantSave" />
|
||||
</div>
|
||||
@if (data_get($database, 'is_public'))
|
||||
<x-slide-over fullScreen>
|
||||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
</x-slide-over>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
<x-forms.checkbox instantSave id="database.is_public" label="Make it publicly available" />
|
||||
</div>
|
||||
<x-forms.input placeholder="5432" disabled="{{ data_get($database, 'is_public') }}"
|
||||
id="database.public_port" label="Public Port" />
|
||||
</div>
|
||||
<x-forms.textarea label="Custom PostgreSQL Configuration" rows="10" id="database.postgres_conf" />
|
||||
</form>
|
||||
<h3 class="pt-4">Advanced</h3>
|
||||
<div class="flex flex-col">
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveAdvanced" id="database.is_log_drain_enabled" label="Drain Logs" />
|
||||
</div>
|
||||
<div class="pb-16">
|
||||
<div class="flex gap-2 pt-4 pb-2">
|
||||
<h3>Initialization scripts</h3>
|
||||
<x-modal-input buttonTitle="+ Add" title="New Init Script">
|
||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='save_new_init_script'>
|
||||
<x-forms.input placeholder="create_test_db.sql" id="new_filename" label="Filename" required />
|
||||
<x-forms.textarea rows="20" placeholder="CREATE DATABASE test;" id="new_content"
|
||||
label="Content" required />
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</form>
|
||||
</x-modal-input>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@forelse(data_get($database,'init_scripts', []) as $script)
|
||||
<livewire:project.database.init-script :script="$script" :wire:key="$script['index']" />
|
||||
@empty
|
||||
<div>No initialization scripts found.</div>
|
||||
@endforelse
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.checkbox id="database.enable_ssl" label="Enable SSL" wire:model.live="database.enable_ssl"
|
||||
instantSave="instantSaveSSL" />
|
||||
@if ($database->enable_ssl)
|
||||
<div class="mx-2">
|
||||
<x-forms.select id="database.ssl_mode" label="SSL Mode" wire:model.live="database.ssl_mode"
|
||||
instantSave="instantSaveSSL"
|
||||
helper="Choose the SSL verification mode for PostgreSQL connections">
|
||||
<option value="allow" title="Allow insecure connections">allow (insecure)</option>
|
||||
<option value="prefer" title="Prefer secure connections">prefer (secure)</option>
|
||||
<option value="require" title="Require secure connections">require (secure)</option>
|
||||
<option value="verify-ca" title="Verify CA certificate">verify-ca (secure)</option>
|
||||
<option value="verify-full" title="Verify full certificate">verify-full (secure)</option>
|
||||
</x-forms.select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>Proxy</h3>
|
||||
<x-loading wire:loading wire:target="instantSave" />
|
||||
</div>
|
||||
@if (data_get($database, 'is_public'))
|
||||
<x-slide-over fullScreen>
|
||||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
</x-slide-over>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.checkbox instantSave id="database.is_public" label="Make it publicly available" />
|
||||
<x-forms.input placeholder="5432" disabled="{{ data_get($database, 'is_public') }}"
|
||||
id="database.public_port" label="Public Port" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.textarea label="Custom PostgreSQL Configuration" rows="10" id="database.postgres_conf" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-col gap-4 pt-4">
|
||||
<h3>Advanced</h3>
|
||||
<div class="flex flex-col">
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveAdvanced" id="database.is_log_drain_enabled" label="Drain Logs" />
|
||||
</div>
|
||||
|
||||
<div class="pb-16">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<h3>Initialization scripts</h3>
|
||||
<x-modal-input buttonTitle="+ Add" title="New Init Script">
|
||||
<form class="flex flex-col w-full gap-2 rounded" wire:submit='save_new_init_script'>
|
||||
<x-forms.input placeholder="create_test_db.sql" id="new_filename" label="Filename"
|
||||
required />
|
||||
<x-forms.textarea rows="20" placeholder="CREATE DATABASE test;" id="new_content"
|
||||
label="Content" required />
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</form>
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@forelse(data_get($database,'init_scripts', []) as $script)
|
||||
<livewire:project.database.init-script :script="$script" :wire:key="$script['index']" />
|
||||
@empty
|
||||
<div>No initialization scripts found.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -49,6 +49,40 @@
|
||||
type="password" readonly wire:model="db_url_public" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if($database->enable_ssl && $certificateValidUntil)
|
||||
<x-modal-confirmation
|
||||
title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates"
|
||||
:actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.'
|
||||
]"
|
||||
submitAction="regenerateSslCertificate"
|
||||
:confirmWithText="false"
|
||||
:confirmWithPassword="false"
|
||||
/>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if($database->enable_ssl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if(now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.checkbox id="database.enable_ssl" label="Enable SSL" wire:model.live="database.enable_ssl" instantSave="instantSaveSSL" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
|
@@ -15,9 +15,9 @@
|
||||
</div>
|
||||
<div class="pb-4">Deploy any public or private Git repositories through a GitHub App.</div>
|
||||
@if ($github_apps->count() !== 0)
|
||||
<h2 class="pt-4 pb-4">Select a Github App</h2>
|
||||
<div class="flex flex-col gap-2">
|
||||
@if ($current_step === 'github_apps')
|
||||
<h2 class="pt-4 pb-4">Select a Github App</h2>
|
||||
<div class="flex flex-col justify-center gap-2 text-left">
|
||||
@foreach ($github_apps as $ghapp)
|
||||
<div class="flex">
|
||||
@@ -43,25 +43,28 @@
|
||||
@endif
|
||||
@if ($current_step === 'repository')
|
||||
@if ($repositories->count() > 0)
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.select class="w-full" label="Repository" wire:model="selected_repository_id">
|
||||
@foreach ($repositories as $repo)
|
||||
@if ($loop->first)
|
||||
<option selected value="{{ data_get($repo, 'id') }}">
|
||||
{{ data_get($repo, 'name') }}
|
||||
</option>
|
||||
@else
|
||||
<option value="{{ data_get($repo, 'id') }}">{{ data_get($repo, 'name') }}
|
||||
</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
<div class="flex flex-col gap-2 pb-6">
|
||||
<div class="flex gap-2">
|
||||
<x-forms.select class="w-full" label="Repository" wire:model="selected_repository_id">
|
||||
@foreach ($repositories as $repo)
|
||||
@if ($loop->first)
|
||||
<option selected value="{{ data_get($repo, 'id') }}">
|
||||
{{ data_get($repo, 'name') }}
|
||||
</option>
|
||||
@else
|
||||
<option value="{{ data_get($repo, 'id') }}">{{ data_get($repo, 'name') }}
|
||||
</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
</div>
|
||||
<x-forms.button wire:click.prevent="loadBranches"> Load Repository </x-forms.button>
|
||||
</div>
|
||||
@else
|
||||
<div>No repositories found. Check your GitHub App configuration.</div>
|
||||
@endif
|
||||
@if ($branches->count() > 0)
|
||||
<h2 class="text-lg font-bold">Configuration</h2>
|
||||
<div class="flex flex-col gap-2 pb-6">
|
||||
<form class="flex flex-col" wire:submit='submit'>
|
||||
<div class="flex flex-col gap-2 pb-6">
|
||||
|
@@ -80,7 +80,7 @@
|
||||
respective
|
||||
companies, and use of them does not imply any affiliation or endorsement.<br>Find more services
|
||||
<a class="dark:text-white underline" target="_blank"
|
||||
href="https://coolify.io/docs/services">here</a>.
|
||||
href="https://coolify.io/docs/services/overview">here</a>.
|
||||
</div>
|
||||
|
||||
<div class="grid justify-start grid-cols-1 gap-4 text-left xl:grid-cols-2">
|
||||
|
@@ -7,6 +7,10 @@
|
||||
<h2>{{ Str::headline($database->name) }}</h2>
|
||||
@endif
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Database Deletion?" buttonTitle="Delete" isErrorButton
|
||||
submitAction="delete" :actions="['The selected service database container will be stopped and permanently deleted.']" confirmationText="{{ Str::headline($database->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Database Name below"
|
||||
shortConfirmationLabel="Service Database Name" step3ButtonText="Permanently Delete" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
|
@@ -28,12 +28,15 @@
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" step3ButtonText="Permanently Delete" />
|
||||
@else
|
||||
<x-modal-confirmation title="Confirm File Conversion to Directory?" buttonTitle="Convert to directory"
|
||||
submitAction="convertToDirectory" :actions="[
|
||||
'The selected file will be permanently deleted and an empty directory will be created in its place.',
|
||||
]" confirmationText="{{ $fs_path }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to directory" />
|
||||
@if (!$fileStorage->is_binary)
|
||||
<x-modal-confirmation title="Confirm File Conversion to Directory?"
|
||||
buttonTitle="Convert to directory" submitAction="convertToDirectory" :actions="[
|
||||
'The selected file will be permanently deleted and an empty directory will be created in its place.',
|
||||
]"
|
||||
confirmationText="{{ $fs_path }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to directory" />
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm File Deletion?" buttonTitle="Delete File" isErrorButton
|
||||
submitAction="delete" :checkboxes="$fileDeletionCheckboxes" :actions="['The selected file will be permanently deleted from the container.']" confirmationText="{{ $fs_path }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
@@ -66,8 +69,8 @@
|
||||
<x-forms.textarea
|
||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||
rows="20" id="fileStorage.content"
|
||||
readonly="{{ $fileStorage->is_based_on_git }}"></x-forms.textarea>
|
||||
@if (!$fileStorage->is_based_on_git)
|
||||
readonly="{{ $fileStorage->is_based_on_git || $fileStorage->is_binary }}"></x-forms.textarea>
|
||||
@if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary)
|
||||
<x-forms.button class="w-full" type="submit">Save</x-forms.button>
|
||||
@endif
|
||||
@endif
|
||||
|
@@ -3,8 +3,8 @@
|
||||
<div class="flex flex-col h-full gap-8 pt-6 sm:flex-row">
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="menu-item"
|
||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
|
||||
wire:navigate href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}">
|
||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}" wire:navigate
|
||||
href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}">
|
||||
<button><- Back</button>
|
||||
</a>
|
||||
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
|
||||
@@ -12,8 +12,8 @@
|
||||
wire:navigate href="#">General</a>
|
||||
@if ($serviceDatabase?->isBackupSolutionAvailable())
|
||||
<a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item"
|
||||
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'"
|
||||
wire:navigate href="#backups">Backups</a>
|
||||
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" wire:navigate
|
||||
href="#backups">Backups</a>
|
||||
@endif
|
||||
</div>
|
||||
<div class="w-full">
|
||||
|
@@ -38,6 +38,95 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 pt-8">
|
||||
<h3>CA SSL Certificate</h3>
|
||||
<div class="flex gap-2">
|
||||
<x-modal-confirmation
|
||||
title="Confirm changing of CA Certificate?"
|
||||
buttonTitle="Save Certificate"
|
||||
submitAction="saveCaCertificate"
|
||||
:actions="[
|
||||
'This will overwrite the existing CA certificate at /data/coolify/ssl/coolify-ca.crt with your custom CA certificate.',
|
||||
'This will regenerate all SSL certificates for databases on this server and it will sign them with your custom CA.',
|
||||
'You must manually redeploy all your databases on this server so that they use the new SSL certificates singned with your new CA certificate.',
|
||||
'Because of caching, you probably also need to redeploy all your resources on this server that are using this CA certificate.'
|
||||
]"
|
||||
confirmationText="/data/coolify/ssl/coolify-ca.crt"
|
||||
shortConfirmationLabel="CA Certificate Path"
|
||||
step3ButtonText="Save Certificate">
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation
|
||||
title="Confirm Regenerate Certificate?"
|
||||
buttonTitle="Regenerate Certificate"
|
||||
submitAction="regenerateCaCertificate"
|
||||
:actions="[
|
||||
'This will generate a new CA certificate at /data/coolify/ssl/coolify-ca.crt and replace the existing one.',
|
||||
'This will regenerate all SSL certificates for databases on this server and it will sign them with the new CA certificate.',
|
||||
'You must manually redeploy all your databases on this server so that they use the new SSL certificates singned with the new CA certificate.',
|
||||
'Because of caching, you probably also need to redeploy all your resources on this server that are using this CA certificate.'
|
||||
]"
|
||||
confirmationText="/data/coolify/ssl/coolify-ca.crt"
|
||||
shortConfirmationLabel="CA Certificate Path"
|
||||
step3ButtonText="Regenerate Certificate">
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="text-sm">
|
||||
<p class="font-medium mb-2">Recommended Configuration:</p>
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li>Mount this CA certificate of Coolify into all containers that need to connect to one of your databases over SSL. You can see and copy the bind mount below.</li>
|
||||
<li>Read more when and why this is needed <a class="underline" href="https://coolify.io/docs/databases/ssl" target="_blank">here</a>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<x-forms.copy-button
|
||||
text="- /data/coolify/ssl/coolify-ca.crt:/etc/ssl/certs/coolify-ca.crt:ro"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm">CA Certificate</span>
|
||||
@if($certificateValidUntil)
|
||||
<span class="text-sm">(Valid until:
|
||||
@if(now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired)</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon)</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }})</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<x-forms.button
|
||||
wire:click="toggleCertificate"
|
||||
type="button"
|
||||
class="!py-1 !px-2 text-sm">
|
||||
{{ $showCertificate ? 'Hide' : 'Show' }}
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@if($showCertificate)
|
||||
<textarea
|
||||
class="w-full h-[370px] input"
|
||||
wire:model="certificateContent"
|
||||
placeholder="Paste or edit CA certificate content here..."></textarea>
|
||||
@else
|
||||
<div class="w-full h-[370px] input">
|
||||
<div class="h-full flex flex-col items-center justify-center text-gray-300">
|
||||
<div class="mb-2">
|
||||
━━━━━━━━ CERTIFICATE CONTENT ━━━━━━━━
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
Click "Show" to view or edit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -32,6 +32,11 @@
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting->provider }}.tenant"
|
||||
label="Tenant" />
|
||||
@endif
|
||||
@if ($oauth_setting->provider == 'google')
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting->provider }}.tenant"
|
||||
helper="Optional parameter that supplies a hosted domain (HD) to Google, which<br>triggers a login hint to be displayed on the OAuth screen with this domain.<br><br><a class='underline dark:text-warning text-coollabs' href='https://developers.google.com/identity/openid-connect/openid-connect#hd-param' target='_blank'>Google Documentation</a>"
|
||||
label="Tenant" />
|
||||
@endif
|
||||
@if ($oauth_setting->provider == 'authentik')
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting->provider }}.base_url"
|
||||
label="Base URL" />
|
||||
|
@@ -27,6 +27,7 @@
|
||||
confirmationText="{{ data_get($github_app, 'name') }}" :confirmWithPassword="false"
|
||||
step2ButtonText="Permanently Delete" />
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtitle">Your Private GitHub App for private repositories.</div>
|
||||
@@ -46,7 +47,7 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex items-end gap-2 w-full">
|
||||
<x-forms.input id="github_app.name" label="App Name" disabled />
|
||||
<x-forms.input id="github_app.name" label="App Name" />
|
||||
<x-forms.button wire:click.prevent="updateGithubAppName" class="bg-coollabs">
|
||||
Sync Name
|
||||
</x-forms.button>
|
||||
@@ -57,7 +58,7 @@
|
||||
</x-forms.button>
|
||||
</a>
|
||||
</div>
|
||||
<x-forms.input id="github_app.organization" label="Organization" disabled
|
||||
<x-forms.input id="github_app.organization" label="Organization"
|
||||
placeholder="If empty, personal user will be used" />
|
||||
</div>
|
||||
@if (!isCloud())
|
||||
@@ -68,27 +69,32 @@
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input id="github_app.html_url" label="HTML Url" disabled />
|
||||
<x-forms.input id="github_app.api_url" label="API Url" disabled />
|
||||
<x-forms.input id="github_app.html_url" label="HTML Url" />
|
||||
<x-forms.input id="github_app.api_url" label="API Url" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@if ($github_app->html_url === 'https://github.com')
|
||||
<x-forms.input id="github_app.custom_user" label="User" disabled />
|
||||
<x-forms.input type="number" id="github_app.custom_port" label="Port" disabled />
|
||||
@else
|
||||
<x-forms.input id="github_app.custom_user" label="User" required />
|
||||
<x-forms.input type="number" id="github_app.custom_port" label="Port" required />
|
||||
@endif
|
||||
<x-forms.input id="github_app.custom_user" label="User" required />
|
||||
<x-forms.input type="number" id="github_app.custom_port" label="Port" required />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input type="number" id="github_app.app_id" label="App Id" disabled />
|
||||
<x-forms.input type="number" id="github_app.app_id" label="App Id" required />
|
||||
<x-forms.input type="number" id="github_app.installation_id" label="Installation Id"
|
||||
disabled />
|
||||
required />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input id="github_app.client_id" label="Client Id" type="password" disabled />
|
||||
<x-forms.input id="github_app.client_secret" label="Client Secret" type="password" />
|
||||
<x-forms.input id="github_app.webhook_secret" label="Webhook Secret" type="password" />
|
||||
<x-forms.input id="github_app.client_id" label="Client Id" type="password" required />
|
||||
<x-forms.input id="github_app.client_secret" label="Client Secret" type="password" required />
|
||||
<x-forms.input id="github_app.webhook_secret" label="Webhook Secret" type="password" required />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.select id="github_app.private_key_id" label="Private Key" required>
|
||||
@if (blank($github_app->private_key_id))
|
||||
<option value="0" selected>Select a private key</option>
|
||||
@endif
|
||||
@foreach ($privateKeys as $privateKey)
|
||||
<option value="{{ $privateKey->id }}">{{ $privateKey->name }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
</div>
|
||||
<div class="flex items-end gap-2 ">
|
||||
<h2 class="pt-4">Permissions</h2>
|
||||
@@ -182,120 +188,129 @@
|
||||
shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" />
|
||||
</div>
|
||||
</div>
|
||||
<div class=" pb-5 rounded alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>You must complete this step before you can use this source!</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="pb-10">
|
||||
@if (!isCloud() || isDev())
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.select wire:model.live='webhook_endpoint' label="Webhook Endpoint"
|
||||
helper="All Git webhooks will be sent to this endpoint. <br><br>If you would like to use domain instead of IP address, set your Coolify instance's FQDN in the Settings menu.">
|
||||
@if ($ipv4)
|
||||
<option value="{{ $ipv4 }}">Use {{ $ipv4 }}</option>
|
||||
@endif
|
||||
@if ($ipv6)
|
||||
<option value="{{ $ipv6 }}">Use {{ $ipv6 }}</option>
|
||||
@endif
|
||||
@if ($fqdn)
|
||||
<option value="{{ $fqdn }}">Use {{ $fqdn }}</option>
|
||||
@endif
|
||||
@if (config('app.url'))
|
||||
<option value="{{ config('app.url') }}">Use {{ config('app.url') }}</option>
|
||||
@endif
|
||||
</x-forms.select>
|
||||
<x-forms.button isHighlighted
|
||||
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}',{{ $administration }})">
|
||||
Register Now
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex gap-2">
|
||||
<h2>Register a GitHub App</h2>
|
||||
<x-forms.button isHighlighted
|
||||
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}',{{ $administration }})">
|
||||
Register Now
|
||||
</x-forms.button>
|
||||
</div>
|
||||
<div>You need to register a GitHub App before using this source.</div>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3>Manual Installation</h3>
|
||||
<div class="flex gap-2 items-center">
|
||||
If you want to fill the form manually, you can continue below. Only for advanced users.
|
||||
<x-forms.button wire:click.prevent="createGithubAppManually">
|
||||
Continue
|
||||
</x-forms.button>
|
||||
</div>
|
||||
<h3>Automated Installation</h3>
|
||||
<div class=" pb-5 rounded alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>You must complete this step before you can use this source!</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="pb-10">
|
||||
@if (!isCloud() || isDev())
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.select wire:model.live='webhook_endpoint' label="Webhook Endpoint"
|
||||
helper="All Git webhooks will be sent to this endpoint. <br><br>If you would like to use domain instead of IP address, set your Coolify instance's FQDN in the Settings menu.">
|
||||
@if ($ipv4)
|
||||
<option value="{{ $ipv4 }}">Use {{ $ipv4 }}</option>
|
||||
@endif
|
||||
@if ($ipv6)
|
||||
<option value="{{ $ipv6 }}">Use {{ $ipv6 }}</option>
|
||||
@endif
|
||||
@if ($fqdn)
|
||||
<option value="{{ $fqdn }}">Use {{ $fqdn }}</option>
|
||||
@endif
|
||||
@if (config('app.url'))
|
||||
<option value="{{ config('app.url') }}">Use {{ config('app.url') }}</option>
|
||||
@endif
|
||||
</x-forms.select>
|
||||
<x-forms.button isHighlighted
|
||||
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}',{{ $administration }})">
|
||||
Register Now
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex gap-2">
|
||||
<h2>Register a GitHub App</h2>
|
||||
<x-forms.button isHighlighted
|
||||
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}',{{ $administration }})">
|
||||
Register Now
|
||||
</x-forms.button>
|
||||
</div>
|
||||
<div>You need to register a GitHub App before using this source.</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col gap-2 pt-4 w-96">
|
||||
<x-forms.checkbox disabled instantSave id="default_permissions" label="Mandatory"
|
||||
helper="Contents: read<br>Metadata: read<br>Email: read" />
|
||||
<x-forms.checkbox instantSave id="preview_deployment_permissions" label="Preview Deployments "
|
||||
helper="Necessary for updating pull requests with useful comments (deployment status, links, etc.)<br><br>Pull Request: read & write" />
|
||||
{{-- <x-forms.checkbox instantSave id="administration" label="Administration (for Github Runners)"
|
||||
<div class="flex flex-col gap-2 pt-4 w-96">
|
||||
<x-forms.checkbox disabled instantSave id="default_permissions" label="Mandatory"
|
||||
helper="Contents: read<br>Metadata: read<br>Email: read" />
|
||||
<x-forms.checkbox instantSave id="preview_deployment_permissions" label="Preview Deployments "
|
||||
helper="Necessary for updating pull requests with useful comments (deployment status, links, etc.)<br><br>Pull Request: read & write" />
|
||||
{{-- <x-forms.checkbox instantSave id="administration" label="Administration (for Github Runners)"
|
||||
helper="Necessary for adding Github Runners to repositories.<br><br>Administration: read & write" /> --}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function createGithubApp(webhook_endpoint, preview_deployment_permissions, administration) {
|
||||
const {
|
||||
organization,
|
||||
uuid,
|
||||
html_url
|
||||
} = @json($github_app);
|
||||
if (!webhook_endpoint) {
|
||||
alert('Please select a webhook endpoint.');
|
||||
return;
|
||||
<script>
|
||||
function createGithubApp(webhook_endpoint, preview_deployment_permissions, administration) {
|
||||
const {
|
||||
organization,
|
||||
uuid,
|
||||
html_url
|
||||
} = @json($github_app);
|
||||
if (!webhook_endpoint) {
|
||||
alert('Please select a webhook endpoint.');
|
||||
return;
|
||||
}
|
||||
let baseUrl = webhook_endpoint;
|
||||
const name = @js($name);
|
||||
const isDev = @js(config('app.env')) ===
|
||||
'local';
|
||||
const devWebhook = @js(config('constants.webhooks.dev_webhook'));
|
||||
if (isDev && devWebhook) {
|
||||
baseUrl = devWebhook;
|
||||
}
|
||||
const webhookBaseUrl = `${baseUrl}/webhooks`;
|
||||
const path = organization ? `organizations/${organization}/settings/apps/new` : 'settings/apps/new';
|
||||
const default_permissions = {
|
||||
contents: 'read',
|
||||
metadata: 'read',
|
||||
emails: 'read',
|
||||
administration: 'read'
|
||||
};
|
||||
if (preview_deployment_permissions) {
|
||||
default_permissions.pull_requests = 'write';
|
||||
}
|
||||
if (administration) {
|
||||
default_permissions.administration = 'write';
|
||||
}
|
||||
const data = {
|
||||
name,
|
||||
url: baseUrl,
|
||||
hook_attributes: {
|
||||
url: `${webhookBaseUrl}/source/github/events`,
|
||||
active: true,
|
||||
},
|
||||
redirect_url: `${webhookBaseUrl}/source/github/redirect`,
|
||||
callback_urls: [`${baseUrl}/login/github/app`],
|
||||
public: false,
|
||||
request_oauth_on_install: false,
|
||||
setup_url: `${webhookBaseUrl}/source/github/install?source=${uuid}`,
|
||||
setup_on_update: true,
|
||||
default_permissions,
|
||||
default_events: ['pull_request', 'push']
|
||||
};
|
||||
const form = document.createElement('form');
|
||||
form.setAttribute('method', 'post');
|
||||
form.setAttribute('action', `${html_url}/${path}?state=${uuid}`);
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('id', 'manifest');
|
||||
input.setAttribute('name', 'manifest');
|
||||
input.setAttribute('type', 'hidden');
|
||||
input.setAttribute('value', JSON.stringify(data));
|
||||
form.appendChild(input);
|
||||
document.getElementsByTagName('body')[0].appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
let baseUrl = webhook_endpoint;
|
||||
const name = @js($name);
|
||||
const isDev = @js(config('app.env')) ===
|
||||
'local';
|
||||
const devWebhook = @js(config('constants.webhooks.dev_webhook'));
|
||||
if (isDev && devWebhook) {
|
||||
baseUrl = devWebhook;
|
||||
}
|
||||
const webhookBaseUrl = `${baseUrl}/webhooks`;
|
||||
const path = organization ? `organizations/${organization}/settings/apps/new` : 'settings/apps/new';
|
||||
const default_permissions = {
|
||||
contents: 'read',
|
||||
metadata: 'read',
|
||||
emails: 'read',
|
||||
administration: 'read'
|
||||
};
|
||||
if (preview_deployment_permissions) {
|
||||
default_permissions.pull_requests = 'write';
|
||||
}
|
||||
if (administration) {
|
||||
default_permissions.administration = 'write';
|
||||
}
|
||||
const data = {
|
||||
name,
|
||||
url: baseUrl,
|
||||
hook_attributes: {
|
||||
url: `${webhookBaseUrl}/source/github/events`,
|
||||
active: true,
|
||||
},
|
||||
redirect_url: `${webhookBaseUrl}/source/github/redirect`,
|
||||
callback_urls: [`${baseUrl}/login/github/app`],
|
||||
public: false,
|
||||
request_oauth_on_install: false,
|
||||
setup_url: `${webhookBaseUrl}/source/github/install?source=${uuid}`,
|
||||
setup_on_update: true,
|
||||
default_permissions,
|
||||
default_events: ['pull_request', 'push']
|
||||
};
|
||||
const form = document.createElement('form');
|
||||
form.setAttribute('method', 'post');
|
||||
form.setAttribute('action', `${html_url}/${path}?state=${uuid}`);
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('id', 'manifest');
|
||||
input.setAttribute('name', 'manifest');
|
||||
input.setAttribute('type', 'hidden');
|
||||
input.setAttribute('value', JSON.stringify(data));
|
||||
form.appendChild(input);
|
||||
document.getElementsByTagName('body')[0].appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@endif
|
||||
</div>
|
||||
|
571
scripts/install-1.6.sh
Normal file
571
scripts/install-1.6.sh
Normal file
@@ -0,0 +1,571 @@
|
||||
#!/bin/bash
|
||||
## Do not modify this file. You will lose the ability to install and auto-update!
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
## $1 could be empty, so we need to disable this check
|
||||
#set -u # Treat unset variables as an error and exit
|
||||
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
|
||||
CDN="https://cdn.coollabs.io/coolify"
|
||||
DATE=$(date +"%Y%m%d-%H%M%S")
|
||||
|
||||
VERSION="1.6"
|
||||
DOCKER_VERSION="27.0"
|
||||
# TODO: Ask for a user
|
||||
CURRENT_USER=$USER
|
||||
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo -e "Welcome to Coolify Installer!"
|
||||
echo -e "This script will install everything for you. Sit back and relax."
|
||||
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
|
||||
|
||||
# Predefined root user
|
||||
ROOT_USERNAME=${ROOT_USERNAME:-}
|
||||
ROOT_USER_EMAIL=${ROOT_USER_EMAIL:-}
|
||||
ROOT_USER_PASSWORD=${ROOT_USER_PASSWORD:-}
|
||||
|
||||
TOTAL_SPACE=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
|
||||
AVAILABLE_SPACE=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//')
|
||||
REQUIRED_TOTAL_SPACE=30
|
||||
REQUIRED_AVAILABLE_SPACE=20
|
||||
WARNING_SPACE=false
|
||||
|
||||
if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then
|
||||
WARNING_SPACE=true
|
||||
cat <<EOF
|
||||
WARNING: Insufficient total disk space!
|
||||
|
||||
Total disk space: ${TOTAL_SPACE}GB
|
||||
Required disk space: ${REQUIRED_TOTAL_SPACE}GB
|
||||
|
||||
==================
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then
|
||||
cat <<EOF
|
||||
WARNING: Insufficient available disk space!
|
||||
|
||||
Available disk space: ${AVAILABLE_SPACE}GB
|
||||
Required available space: ${REQUIRED_AVAILABLE_SPACE}GB
|
||||
|
||||
==================
|
||||
EOF
|
||||
WARNING_SPACE=true
|
||||
fi
|
||||
|
||||
if [ "$WARNING_SPACE" = true ]; then
|
||||
echo "Sleeping for 5 seconds."
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
|
||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||
mkdir -p /data/coolify/proxy/dynamic
|
||||
|
||||
chown -R 9999:root /data/coolify
|
||||
chmod -R 700 /data/coolify
|
||||
|
||||
INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
|
||||
|
||||
exec > >(tee -a $INSTALLATION_LOG_WITH_DATE) 2>&1
|
||||
|
||||
getAJoke() {
|
||||
JOKES=$(curl -s --max-time 2 "https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit&format=txt&type=single" || true)
|
||||
if [ "$JOKES" != "" ]; then
|
||||
echo -e " - Until then, here's a joke for you:\n"
|
||||
echo -e "$JOKES\n"
|
||||
fi
|
||||
}
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Endeavour OS, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "endeavouros" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||
OS_TYPE="fedora"
|
||||
fi
|
||||
|
||||
# Check if the OS is popOS, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "pop" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
#Check if the OS is zorin, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "zorin" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||
OS_VERSION="rolling"
|
||||
else
|
||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
fi
|
||||
|
||||
# Install xargs on Amazon Linux 2023 - lol
|
||||
if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
dnf install -y findutils >/dev/null
|
||||
fi
|
||||
|
||||
LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',')
|
||||
LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',')
|
||||
LATEST_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',')
|
||||
|
||||
if [ -z "$LATEST_HELPER_VERSION" ]; then
|
||||
LATEST_HELPER_VERSION=latest
|
||||
fi
|
||||
|
||||
if [ -z "$LATEST_REALTIME_VERSION" ]; then
|
||||
LATEST_REALTIME_VERSION=latest
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
# Overwrite LATEST_VERSION if user pass a version number
|
||||
if [ "$1" != "" ]; then
|
||||
LATEST_VERSION=$1
|
||||
LATEST_VERSION="${LATEST_VERSION,,}"
|
||||
LATEST_VERSION="${LATEST_VERSION#v}"
|
||||
fi
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo "| Coolify | $LATEST_VERSION"
|
||||
echo "| Helper | $LATEST_HELPER_VERSION"
|
||||
echo "| Realtime | $LATEST_REALTIME_VERSION"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||
;;
|
||||
alpine)
|
||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update >/dev/null
|
||||
apk add curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
fi
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
fi
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper refresh >/dev/null
|
||||
zypper install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "2. Check OpenSSH server configuration. "
|
||||
|
||||
# Detect OpenSSH server
|
||||
SSH_DETECTED=false
|
||||
if [ -x "$(command -v systemctl)" ]; then
|
||||
if systemctl status sshd >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
elif systemctl status ssh >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
elif [ -x "$(command -v service)" ]; then
|
||||
if service sshd status >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
elif service ssh status >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$SSH_DETECTED" = "false" ]; then
|
||||
echo " - OpenSSH server not detected. Installing OpenSSH server."
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm openssh >/dev/null
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
alpine)
|
||||
apk add openssh >/dev/null
|
||||
rc-update add sshd default >/dev/null 2>&1
|
||||
service sshd start >/dev/null 2>&1
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y openssh-server >/dev/null
|
||||
systemctl enable ssh >/dev/null 2>&1
|
||||
systemctl start ssh >/dev/null 2>&1
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y openssh-server >/dev/null
|
||||
else
|
||||
dnf install -y openssh-server >/dev/null
|
||||
fi
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper install -y openssh >/dev/null
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
echo "###############################################################################"
|
||||
echo "WARNING: Could not detect and install OpenSSH server - this does not mean that it is not installed or not running, just that we could not detect it."
|
||||
echo -e "Please make sure it is installed and running, otherwise Coolify cannot connect to the host system. \n"
|
||||
echo "###############################################################################"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo " - OpenSSH server installed successfully."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
|
||||
# Detect SSH PermitRootLogin
|
||||
SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true
|
||||
if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then
|
||||
echo " - SSH PermitRootLogin is enabled."
|
||||
else
|
||||
echo " - SSH PermitRootLogin is disabled."
|
||||
echo " If you have problems with SSH, please read this: https://coolify.io/docs/knowledge-base/server/openssh"
|
||||
fi
|
||||
|
||||
# Detect if docker is installed via snap
|
||||
if [ -x "$(command -v snap)" ]; then
|
||||
SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false")
|
||||
if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then
|
||||
echo " - Docker is installed via snap."
|
||||
echo " Please note that Coolify does not support Docker installed via snap."
|
||||
echo " Please remove Docker with snap (snap remove docker) and reexecute this script."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "3. Check Docker Installation. "
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
getAJoke
|
||||
case "$OS_TYPE" in
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"fedora")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
|
||||
echo "Please install Docker manually."
|
||||
exit 1
|
||||
fi
|
||||
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
echo " - Docker installed successfully."
|
||||
else
|
||||
echo " - Docker is installed."
|
||||
fi
|
||||
|
||||
echo -e "4. Check Docker Configuration. "
|
||||
mkdir -p /etc/docker
|
||||
# shellcheck disable=SC2015
|
||||
test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" || cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"10.0.0.0/8","size":24}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
cat >/etc/docker/daemon.json.coolify <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"10.0.0.0/8","size":24}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
TEMP_FILE=$(mktemp)
|
||||
if ! jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify >"$TEMP_FILE"; then
|
||||
echo "Error merging JSON files"
|
||||
exit 1
|
||||
fi
|
||||
mv "$TEMP_FILE" /etc/docker/daemon.json
|
||||
|
||||
restart_docker_service() {
|
||||
# Check if systemctl is available
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
echo " - Using systemctl to restart Docker."
|
||||
systemctl restart docker
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker restarted successfully using systemctl."
|
||||
else
|
||||
echo " - Failed to restart Docker using systemctl."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if service command is available
|
||||
elif command -v service >/dev/null 2>&1; then
|
||||
echo " - Using service command to restart Docker."
|
||||
service docker restart
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker restarted successfully using service."
|
||||
else
|
||||
echo " - Failed to restart Docker using service."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If neither systemctl nor service is available
|
||||
else
|
||||
echo " - Neither systemctl nor service command is available on this system."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
|
||||
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE"))
|
||||
if [ "$DIFF" != "" ]; then
|
||||
echo " - Docker configuration updated, restart docker daemon..."
|
||||
restart_docker_service
|
||||
else
|
||||
echo " - Docker configuration is up to date."
|
||||
fi
|
||||
else
|
||||
echo " - Docker configuration updated, restart docker daemon..."
|
||||
restart_docker_service
|
||||
fi
|
||||
|
||||
echo -e "5. Download required files from CDN. "
|
||||
curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
||||
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh
|
||||
|
||||
echo -e "6. Make backup of .env to .env-$DATE"
|
||||
|
||||
# Copy .env.example if .env does not exist
|
||||
if [ -f $ENV_FILE ]; then
|
||||
cp $ENV_FILE $ENV_FILE-$DATE
|
||||
else
|
||||
echo " - File does not exist: $ENV_FILE"
|
||||
echo " - Copying .env.production to .env-$DATE"
|
||||
cp /data/coolify/source/.env.production $ENV_FILE-$DATE
|
||||
# Generate a secure APP_ID and APP_KEY
|
||||
sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Postgres DB username and password
|
||||
# Causes issues: database "random-user" does not exist
|
||||
# sed -i "s|^DB_USERNAME=.*|DB_USERNAME=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Redis password
|
||||
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate secure Pusher credentials
|
||||
sed -i "s|^PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
|
||||
# Add default root user credentials from environment variables
|
||||
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
if grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USERNAME=.*|ROOT_USERNAME=$ROOT_USERNAME|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_EMAIL=.*|ROOT_USER_EMAIL=$ROOT_USER_EMAIL|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_PASSWORD=.*|ROOT_USER_PASSWORD=$ROOT_USER_PASSWORD|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Merge .env and .env.production. New values will be added to .env
|
||||
echo -e "7. Propagating .env with new values - if necessary."
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production >$ENV_FILE
|
||||
|
||||
if [ "$AUTOUPDATE" = "false" ]; then
|
||||
if ! grep -q "AUTOUPDATE=" /data/coolify/source/.env; then
|
||||
echo "AUTOUPDATE=false" >>/data/coolify/source/.env
|
||||
else
|
||||
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
echo -e "8. Checking for SSH key for localhost access."
|
||||
if [ ! -f ~/.ssh/authorized_keys ]; then
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
touch ~/.ssh/authorized_keys
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
set +e
|
||||
IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l)
|
||||
set -e
|
||||
|
||||
if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
|
||||
echo " - Generating SSH key."
|
||||
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify
|
||||
chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal
|
||||
sed -i "/coolify/d" ~/.ssh/authorized_keys
|
||||
cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >>~/.ssh/authorized_keys
|
||||
rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub
|
||||
fi
|
||||
|
||||
chown -R 9999:root /data/coolify
|
||||
chmod -R 700 /data/coolify
|
||||
|
||||
echo -e "9. Installing Coolify ($LATEST_VERSION)"
|
||||
echo -e " - It could take a while based on your server's performance, network speed, stars, etc."
|
||||
echo -e " - Please wait."
|
||||
getAJoke
|
||||
|
||||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
|
||||
echo " - Coolify installed successfully."
|
||||
rm -f $ENV_FILE-$DATE
|
||||
|
||||
echo " - Waiting for 20 seconds for Coolify (database migrations) to be ready."
|
||||
getAJoke
|
||||
|
||||
sleep 20
|
||||
echo -e "\033[0;35m
|
||||
____ _ _ _ _ _
|
||||
/ ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| |
|
||||
| | / _ \| '_ \ / _\` | '__/ _\` | __| | | | |/ _\` | __| |/ _ \| '_ \/ __| |
|
||||
| |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_|
|
||||
\____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_)
|
||||
|___/
|
||||
\033[0m"
|
||||
echo -e "\nYour instance is ready to use!\n"
|
||||
echo -e "You can access Coolify through your Public IP: http://$(curl -4s https://ifconfig.io):8000"
|
||||
|
||||
set +e
|
||||
DEFAULT_PRIVATE_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
|
||||
PRIVATE_IPS=$(hostname -I 2>/dev/null || ip -o addr show scope global | awk '{print $4}' | cut -d/ -f1)
|
||||
set -e
|
||||
|
||||
if [ -n "$PRIVATE_IPS" ]; then
|
||||
echo -e "\nIf your Public IP is not accessible, you can use the following Private IPs:\n"
|
||||
for IP in $PRIVATE_IPS; do
|
||||
if [ "$IP" != "$DEFAULT_PRIVATE_IP" ]; then
|
||||
echo -e "http://$IP:8000"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n"
|
||||
cp /data/coolify/source/.env /data/coolify/source/.env.backup
|
789
scripts/install-1.7.sh
Executable file
789
scripts/install-1.7.sh
Executable file
@@ -0,0 +1,789 @@
|
||||
#!/bin/bash
|
||||
## Do not modify this file. You will lose the ability to install and auto-update!
|
||||
|
||||
## Environment variables that can be set:
|
||||
## ROOT_USERNAME - Predefined root username
|
||||
## ROOT_USER_EMAIL - Predefined root user email
|
||||
## ROOT_USER_PASSWORD - Predefined root user password
|
||||
## DOCKER_ADDRESS_POOL_BASE - Custom Docker address pool base (default: 10.0.0.0/8)
|
||||
## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24)
|
||||
## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false)
|
||||
## AUTOUPDATE - Set to "false" to disable auto-updates
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
## $1 could be empty, so we need to disable this check
|
||||
#set -u # Treat unset variables as an error and exit
|
||||
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
|
||||
CDN="https://cdn.coollabs.io/coolify"
|
||||
DATE=$(date +"%Y%m%d-%H%M%S")
|
||||
|
||||
VERSION="1.7"
|
||||
DOCKER_VERSION="27.0"
|
||||
# TODO: Ask for a user
|
||||
CURRENT_USER=$USER
|
||||
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo -e "Welcome to Coolify Installer!"
|
||||
echo -e "This script will install everything for you. Sit back and relax."
|
||||
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
|
||||
|
||||
# Predefined root user
|
||||
ROOT_USERNAME=${ROOT_USERNAME:-}
|
||||
ROOT_USER_EMAIL=${ROOT_USER_EMAIL:-}
|
||||
ROOT_USER_PASSWORD=${ROOT_USER_PASSWORD:-}
|
||||
|
||||
# Docker address pool configuration defaults
|
||||
DOCKER_ADDRESS_POOL_BASE_DEFAULT="10.0.0.0/8"
|
||||
DOCKER_ADDRESS_POOL_SIZE_DEFAULT=24
|
||||
|
||||
# Check if environment variables were explicitly provided
|
||||
DOCKER_POOL_BASE_PROVIDED=false
|
||||
DOCKER_POOL_SIZE_PROVIDED=false
|
||||
DOCKER_POOL_FORCE_OVERRIDE=${DOCKER_POOL_FORCE_OVERRIDE:-false}
|
||||
|
||||
if [ -n "${DOCKER_ADDRESS_POOL_BASE+x}" ]; then
|
||||
DOCKER_POOL_BASE_PROVIDED=true
|
||||
fi
|
||||
|
||||
if [ -n "${DOCKER_ADDRESS_POOL_SIZE+x}" ]; then
|
||||
DOCKER_POOL_SIZE_PROVIDED=true
|
||||
fi
|
||||
|
||||
restart_docker_service() {
|
||||
# Check if systemctl is available
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl restart docker
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker daemon restarted successfully"
|
||||
else
|
||||
echo " - Failed to restart Docker daemon"
|
||||
return 1
|
||||
fi
|
||||
# Check if service command is available
|
||||
elif command -v service >/dev/null 2>&1; then
|
||||
service docker restart
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker daemon restarted successfully"
|
||||
else
|
||||
echo " - Failed to restart Docker daemon"
|
||||
return 1
|
||||
fi
|
||||
# If neither systemctl nor service is available
|
||||
else
|
||||
echo " - Error: No service management system found"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to compare address pools
|
||||
compare_address_pools() {
|
||||
local base1="$1"
|
||||
local size1="$2"
|
||||
local base2="$3"
|
||||
local size2="$4"
|
||||
|
||||
# Normalize CIDR notation for comparison
|
||||
local ip1=$(echo "$base1" | cut -d'/' -f1)
|
||||
local prefix1=$(echo "$base1" | cut -d'/' -f2)
|
||||
local ip2=$(echo "$base2" | cut -d'/' -f1)
|
||||
local prefix2=$(echo "$base2" | cut -d'/' -f2)
|
||||
|
||||
# Compare IPs and prefixes
|
||||
if [ "$ip1" = "$ip2" ] && [ "$prefix1" = "$prefix2" ] && [ "$size1" = "$size2" ]; then
|
||||
return 0 # Pools are the same
|
||||
else
|
||||
return 1 # Pools are different
|
||||
fi
|
||||
}
|
||||
|
||||
# Docker address pool configuration
|
||||
DOCKER_ADDRESS_POOL_BASE=${DOCKER_ADDRESS_POOL_BASE:-"$DOCKER_ADDRESS_POOL_BASE_DEFAULT"}
|
||||
DOCKER_ADDRESS_POOL_SIZE=${DOCKER_ADDRESS_POOL_SIZE:-$DOCKER_ADDRESS_POOL_SIZE_DEFAULT}
|
||||
|
||||
# Load Docker address pool configuration from .env file if it exists and environment variables were not provided
|
||||
if [ -f "/data/coolify/source/.env" ] && [ "$DOCKER_POOL_BASE_PROVIDED" = false ] && [ "$DOCKER_POOL_SIZE_PROVIDED" = false ]; then
|
||||
ENV_DOCKER_ADDRESS_POOL_BASE=$(grep -E "^DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env | cut -d '=' -f2)
|
||||
ENV_DOCKER_ADDRESS_POOL_SIZE=$(grep -E "^DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env | cut -d '=' -f2)
|
||||
|
||||
if [ -n "$ENV_DOCKER_ADDRESS_POOL_BASE" ]; then
|
||||
DOCKER_ADDRESS_POOL_BASE="$ENV_DOCKER_ADDRESS_POOL_BASE"
|
||||
fi
|
||||
|
||||
if [ -n "$ENV_DOCKER_ADDRESS_POOL_SIZE" ]; then
|
||||
DOCKER_ADDRESS_POOL_SIZE="$ENV_DOCKER_ADDRESS_POOL_SIZE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if daemon.json exists and extract existing address pool configuration
|
||||
EXISTING_POOL_CONFIGURED=false
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
if jq -e '.["default-address-pools"]' /etc/docker/daemon.json >/dev/null 2>&1; then
|
||||
EXISTING_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null)
|
||||
EXISTING_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null)
|
||||
|
||||
if [ -n "$EXISTING_POOL_BASE" ] && [ -n "$EXISTING_POOL_SIZE" ] && [ "$EXISTING_POOL_BASE" != "null" ] && [ "$EXISTING_POOL_SIZE" != "null" ]; then
|
||||
echo "Found existing Docker network pool: $EXISTING_POOL_BASE/$EXISTING_POOL_SIZE"
|
||||
EXISTING_POOL_CONFIGURED=true
|
||||
|
||||
# Check if environment variables were explicitly provided
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = false ] && [ "$DOCKER_POOL_SIZE_PROVIDED" = false ]; then
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
else
|
||||
# Check if force override is enabled
|
||||
if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ]; then
|
||||
echo "Force override enabled - network pool will be updated with $DOCKER_ADDRESS_POOL_BASE/$DOCKER_ADDRESS_POOL_SIZE."
|
||||
else
|
||||
echo "Custom pool provided but force override not enabled - using existing configuration."
|
||||
echo "To force override, set DOCKER_POOL_FORCE_OVERRIDE=true"
|
||||
echo "This won't change the existing docker networks, only the pool configuration for the newly created networks."
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
DOCKER_POOL_BASE_PROVIDED=false
|
||||
DOCKER_POOL_SIZE_PROVIDED=false
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate Docker address pool configuration
|
||||
if ! [[ $DOCKER_ADDRESS_POOL_BASE =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]]; then
|
||||
echo "Warning: Invalid network pool base format: $DOCKER_ADDRESS_POOL_BASE"
|
||||
if [ "$EXISTING_POOL_CONFIGURED" = true ]; then
|
||||
echo "Using existing configuration: $EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
else
|
||||
echo "Using default configuration: $DOCKER_ADDRESS_POOL_BASE_DEFAULT"
|
||||
DOCKER_ADDRESS_POOL_BASE="$DOCKER_ADDRESS_POOL_BASE_DEFAULT"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! [[ $DOCKER_ADDRESS_POOL_SIZE =~ ^[0-9]+$ ]] || [ "$DOCKER_ADDRESS_POOL_SIZE" -lt 16 ] || [ "$DOCKER_ADDRESS_POOL_SIZE" -gt 28 ]; then
|
||||
echo "Warning: Invalid network pool size: $DOCKER_ADDRESS_POOL_SIZE (must be 16-28)"
|
||||
if [ "$EXISTING_POOL_CONFIGURED" = true ]; then
|
||||
echo "Using existing configuration: $EXISTING_POOL_SIZE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
else
|
||||
echo "Using default configuration: $DOCKER_ADDRESS_POOL_SIZE_DEFAULT"
|
||||
DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE_DEFAULT
|
||||
fi
|
||||
fi
|
||||
|
||||
TOTAL_SPACE=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
|
||||
AVAILABLE_SPACE=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//')
|
||||
REQUIRED_TOTAL_SPACE=30
|
||||
REQUIRED_AVAILABLE_SPACE=20
|
||||
WARNING_SPACE=false
|
||||
|
||||
if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then
|
||||
WARNING_SPACE=true
|
||||
cat <<EOF
|
||||
WARNING: Insufficient total disk space!
|
||||
|
||||
Total disk space: ${TOTAL_SPACE}GB
|
||||
Required disk space: ${REQUIRED_TOTAL_SPACE}GB
|
||||
|
||||
==================
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then
|
||||
cat <<EOF
|
||||
WARNING: Insufficient available disk space!
|
||||
|
||||
Available disk space: ${AVAILABLE_SPACE}GB
|
||||
Required available space: ${REQUIRED_AVAILABLE_SPACE}GB
|
||||
|
||||
==================
|
||||
EOF
|
||||
WARNING_SPACE=true
|
||||
fi
|
||||
|
||||
if [ "$WARNING_SPACE" = true ]; then
|
||||
echo "Sleeping for 5 seconds."
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
|
||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||
mkdir -p /data/coolify/proxy/dynamic
|
||||
|
||||
chown -R 9999:root /data/coolify
|
||||
chmod -R 700 /data/coolify
|
||||
|
||||
INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
|
||||
|
||||
exec > >(tee -a $INSTALLATION_LOG_WITH_DATE) 2>&1
|
||||
|
||||
getAJoke() {
|
||||
JOKES=$(curl -s --max-time 2 "https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit&format=txt&type=single" || true)
|
||||
if [ "$JOKES" != "" ]; then
|
||||
echo -e " - Until then, here's a joke for you:\n"
|
||||
echo -e "$JOKES\n"
|
||||
fi
|
||||
}
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Endeavour OS, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "endeavouros" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||
OS_TYPE="fedora"
|
||||
fi
|
||||
|
||||
# Check if the OS is popOS, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "pop" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
#Check if the OS is zorin, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "zorin" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||
OS_VERSION="rolling"
|
||||
else
|
||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
fi
|
||||
|
||||
# Install xargs on Amazon Linux 2023 - lol
|
||||
if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
dnf install -y findutils >/dev/null
|
||||
fi
|
||||
|
||||
LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',')
|
||||
LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',')
|
||||
LATEST_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',')
|
||||
|
||||
if [ -z "$LATEST_HELPER_VERSION" ]; then
|
||||
LATEST_HELPER_VERSION=latest
|
||||
fi
|
||||
|
||||
if [ -z "$LATEST_REALTIME_VERSION" ]; then
|
||||
LATEST_REALTIME_VERSION=latest
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
# Overwrite LATEST_VERSION if user pass a version number
|
||||
if [ "$1" != "" ]; then
|
||||
LATEST_VERSION=$1
|
||||
LATEST_VERSION="${LATEST_VERSION,,}"
|
||||
LATEST_VERSION="${LATEST_VERSION#v}"
|
||||
fi
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo "| Coolify | $LATEST_VERSION"
|
||||
echo "| Helper | $LATEST_HELPER_VERSION"
|
||||
echo "| Realtime | $LATEST_REALTIME_VERSION"
|
||||
echo "| Docker Pool | $DOCKER_ADDRESS_POOL_BASE (size $DOCKER_ADDRESS_POOL_SIZE)"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||
;;
|
||||
alpine)
|
||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update >/dev/null
|
||||
apk add curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
fi
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
fi
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper refresh >/dev/null
|
||||
zypper install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "2. Check OpenSSH server configuration. "
|
||||
|
||||
# Detect OpenSSH server
|
||||
SSH_DETECTED=false
|
||||
if [ -x "$(command -v systemctl)" ]; then
|
||||
if systemctl status sshd >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
elif systemctl status ssh >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
elif [ -x "$(command -v service)" ]; then
|
||||
if service sshd status >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
elif service ssh status >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$SSH_DETECTED" = "false" ]; then
|
||||
echo " - OpenSSH server not detected. Installing OpenSSH server."
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm openssh >/dev/null
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
alpine)
|
||||
apk add openssh >/dev/null
|
||||
rc-update add sshd default >/dev/null 2>&1
|
||||
service sshd start >/dev/null 2>&1
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y openssh-server >/dev/null
|
||||
systemctl enable ssh >/dev/null 2>&1
|
||||
systemctl start ssh >/dev/null 2>&1
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y openssh-server >/dev/null
|
||||
else
|
||||
dnf install -y openssh-server >/dev/null
|
||||
fi
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper install -y openssh >/dev/null
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
echo "###############################################################################"
|
||||
echo "WARNING: Could not detect and install OpenSSH server - this does not mean that it is not installed or not running, just that we could not detect it."
|
||||
echo -e "Please make sure it is installed and running, otherwise Coolify cannot connect to the host system. \n"
|
||||
echo "###############################################################################"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo " - OpenSSH server installed successfully."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
|
||||
# Detect SSH PermitRootLogin
|
||||
SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true
|
||||
if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then
|
||||
echo " - SSH PermitRootLogin is enabled."
|
||||
else
|
||||
echo " - SSH PermitRootLogin is disabled."
|
||||
echo " If you have problems with SSH, please read this: https://coolify.io/docs/knowledge-base/server/openssh"
|
||||
fi
|
||||
|
||||
# Detect if docker is installed via snap
|
||||
if [ -x "$(command -v snap)" ]; then
|
||||
SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false")
|
||||
if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then
|
||||
echo " - Docker is installed via snap."
|
||||
echo " Please note that Coolify does not support Docker installed via snap."
|
||||
echo " Please remove Docker with snap (snap remove docker) and reexecute this script."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "3. Check Docker Installation. "
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
getAJoke
|
||||
case "$OS_TYPE" in
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"centos" | "fedora" | "rhel")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
|
||||
echo "Please install Docker manually."
|
||||
exit 1
|
||||
fi
|
||||
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
echo " - Docker installed successfully."
|
||||
else
|
||||
echo " - Docker is installed."
|
||||
fi
|
||||
|
||||
echo -e "4. Check Docker Configuration. "
|
||||
|
||||
echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
echo " - To override existing configuration: DOCKER_POOL_FORCE_OVERRIDE=true"
|
||||
|
||||
mkdir -p /etc/docker
|
||||
|
||||
# Backup original daemon.json if it exists
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE"
|
||||
fi
|
||||
|
||||
# Create coolify configuration with or without address pools based on whether they were explicitly provided
|
||||
if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ] || [ "$EXISTING_POOL_CONFIGURED" = false ]; then
|
||||
# First check if the configuration would actually change anything
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
CURRENT_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null)
|
||||
CURRENT_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null)
|
||||
|
||||
if [ "$CURRENT_POOL_BASE" = "$DOCKER_ADDRESS_POOL_BASE" ] && [ "$CURRENT_POOL_SIZE" = "$DOCKER_ADDRESS_POOL_SIZE" ]; then
|
||||
echo " - Network pool configuration unchanged, skipping update"
|
||||
NEED_MERGE=false
|
||||
else
|
||||
# If force override is enabled or no existing configuration exists,
|
||||
# create a new configuration with the specified address pools
|
||||
echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
else
|
||||
# No existing configuration, create new one
|
||||
echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
else
|
||||
# Check if we need to update log settings
|
||||
if [ -f /etc/docker/daemon.json ] && jq -e '.["log-driver"] == "json-file" and .["log-opts"]["max-size"] == "10m" and .["log-opts"]["max-file"] == "3"' /etc/docker/daemon.json >/dev/null 2>&1; then
|
||||
echo " - Log configuration is up to date"
|
||||
NEED_MERGE=false
|
||||
else
|
||||
# Create a configuration without address pools to preserve existing ones
|
||||
cat >/etc/docker/daemon.json.coolify <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
}
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove the duplicate daemon.json creation since we handle it above
|
||||
if ! [ -f /etc/docker/daemon.json ]; then
|
||||
# If no daemon.json exists, create it with default settings
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=false
|
||||
fi
|
||||
|
||||
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
|
||||
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE") || true)
|
||||
if [ "$DIFF" != "" ]; then
|
||||
echo " - Checking configuration changes..."
|
||||
|
||||
# Check if address pools were changed
|
||||
if echo "$DIFF" | grep -q "default-address-pools"; then
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ] || [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
echo " - Network pool updated per user request"
|
||||
else
|
||||
echo " - Warning: Network pool modified without explicit request"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove this redundant restart since we already restarted when writing the config
|
||||
echo " - Configuration changes confirmed"
|
||||
if [ "$NEED_MERGE" = true ]; then
|
||||
echo " - Configuration updated - restarting Docker daemon..."
|
||||
restart_docker_service
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
else
|
||||
if [ "$NEED_MERGE" = true ]; then
|
||||
echo " - Configuration updated - restarting Docker daemon..."
|
||||
restart_docker_service
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "5. Download required files from CDN. "
|
||||
curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
||||
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh
|
||||
|
||||
echo -e "6. Make backup of .env to .env-$DATE"
|
||||
|
||||
# Copy .env.example if .env does not exist
|
||||
if [ -f $ENV_FILE ]; then
|
||||
cp $ENV_FILE $ENV_FILE-$DATE
|
||||
else
|
||||
echo " - File does not exist: $ENV_FILE"
|
||||
echo " - Copying .env.production to .env-$DATE"
|
||||
cp /data/coolify/source/.env.production $ENV_FILE-$DATE
|
||||
# Generate a secure APP_ID and APP_KEY
|
||||
sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Postgres DB username and password
|
||||
# Causes issues: database "random-user" does not exist
|
||||
# sed -i "s|^DB_USERNAME=.*|DB_USERNAME=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Redis password
|
||||
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate secure Pusher credentials
|
||||
sed -i "s|^PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
|
||||
# Add default root user credentials from environment variables
|
||||
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
if grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USERNAME=.*|ROOT_USERNAME=$ROOT_USERNAME|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_EMAIL=.*|ROOT_USER_EMAIL=$ROOT_USER_EMAIL|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_PASSWORD=.*|ROOT_USER_PASSWORD=$ROOT_USER_PASSWORD|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Merge .env and .env.production. New values will be added to .env
|
||||
echo -e "7. Propagating .env with new values - if necessary."
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production >$ENV_FILE
|
||||
|
||||
if [ "$AUTOUPDATE" = "false" ]; then
|
||||
if ! grep -q "AUTOUPDATE=" /data/coolify/source/.env; then
|
||||
echo "AUTOUPDATE=false" >>/data/coolify/source/.env
|
||||
else
|
||||
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save Docker address pool configuration to .env file
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE" >>/data/coolify/source/.env
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_BASE=.*|DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE" >>/data/coolify/source/.env
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_SIZE=.*|DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "8. Checking for SSH key for localhost access."
|
||||
if [ ! -f ~/.ssh/authorized_keys ]; then
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
touch ~/.ssh/authorized_keys
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
set +e
|
||||
IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l)
|
||||
set -e
|
||||
|
||||
if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
|
||||
echo " - Generating SSH key."
|
||||
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify
|
||||
chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal
|
||||
sed -i "/coolify/d" ~/.ssh/authorized_keys
|
||||
cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >>~/.ssh/authorized_keys
|
||||
rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub
|
||||
fi
|
||||
|
||||
chown -R 9999:root /data/coolify
|
||||
chmod -R 700 /data/coolify
|
||||
|
||||
echo -e "9. Installing Coolify ($LATEST_VERSION)"
|
||||
echo -e " - It could take a while based on your server's performance, network speed, stars, etc."
|
||||
echo -e " - Please wait."
|
||||
getAJoke
|
||||
|
||||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
|
||||
echo " - Coolify installed successfully."
|
||||
rm -f $ENV_FILE-$DATE
|
||||
|
||||
echo " - Waiting for 20 seconds for Coolify (database migrations) to be ready."
|
||||
getAJoke
|
||||
|
||||
sleep 20
|
||||
echo -e "\033[0;35m
|
||||
____ _ _ _ _ _
|
||||
/ ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| |
|
||||
| | / _ \| '_ \ / _\` | '__/ _\` | __| | | | |/ _\` | __| |/ _ \| '_ \/ __| |
|
||||
| |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_|
|
||||
\____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_)
|
||||
|___/
|
||||
\033[0m"
|
||||
echo -e "\nYour instance is ready to use!\n"
|
||||
echo -e "You can access Coolify through your Public IP: http://$(curl -4s https://ifconfig.io):8000"
|
||||
|
||||
set +e
|
||||
DEFAULT_PRIVATE_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
|
||||
PRIVATE_IPS=$(hostname -I 2>/dev/null || ip -o addr show scope global | awk '{print $4}' | cut -d/ -f1)
|
||||
set -e
|
||||
|
||||
if [ -n "$PRIVATE_IPS" ]; then
|
||||
echo -e "\nIf your Public IP is not accessible, you can use the following Private IPs:\n"
|
||||
for IP in $PRIVATE_IPS; do
|
||||
if [ "$IP" != "$DEFAULT_PRIVATE_IP" ]; then
|
||||
echo -e "http://$IP:8000"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n"
|
||||
cp /data/coolify/source/.env /data/coolify/source/.env.backup
|
@@ -1,6 +1,15 @@
|
||||
#!/bin/bash
|
||||
## Do not modify this file. You will lose the ability to install and auto-update!
|
||||
|
||||
## Environment variables that can be set:
|
||||
## ROOT_USERNAME - Predefined root username
|
||||
## ROOT_USER_EMAIL - Predefined root user email
|
||||
## ROOT_USER_PASSWORD - Predefined root user password
|
||||
## DOCKER_ADDRESS_POOL_BASE - Custom Docker address pool base (default: 10.0.0.0/8)
|
||||
## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24)
|
||||
## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false)
|
||||
## AUTOUPDATE - Set to "false" to disable auto-updates
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
## $1 could be empty, so we need to disable this check
|
||||
#set -u # Treat unset variables as an error and exit
|
||||
@@ -8,7 +17,7 @@ set -o pipefail # Cause a pipeline to return the status of the last command that
|
||||
CDN="https://cdn.coollabs.io/coolify"
|
||||
DATE=$(date +"%Y%m%d-%H%M%S")
|
||||
|
||||
VERSION="1.7"
|
||||
VERSION="1.8"
|
||||
DOCKER_VERSION="27.0"
|
||||
# TODO: Ask for a user
|
||||
CURRENT_USER=$USER
|
||||
@@ -27,6 +36,144 @@ ROOT_USERNAME=${ROOT_USERNAME:-}
|
||||
ROOT_USER_EMAIL=${ROOT_USER_EMAIL:-}
|
||||
ROOT_USER_PASSWORD=${ROOT_USER_PASSWORD:-}
|
||||
|
||||
# Docker address pool configuration defaults
|
||||
DOCKER_ADDRESS_POOL_BASE_DEFAULT="10.0.0.0/8"
|
||||
DOCKER_ADDRESS_POOL_SIZE_DEFAULT=24
|
||||
|
||||
# Check if environment variables were explicitly provided
|
||||
DOCKER_POOL_BASE_PROVIDED=false
|
||||
DOCKER_POOL_SIZE_PROVIDED=false
|
||||
DOCKER_POOL_FORCE_OVERRIDE=${DOCKER_POOL_FORCE_OVERRIDE:-false}
|
||||
|
||||
if [ -n "${DOCKER_ADDRESS_POOL_BASE+x}" ]; then
|
||||
DOCKER_POOL_BASE_PROVIDED=true
|
||||
fi
|
||||
|
||||
if [ -n "${DOCKER_ADDRESS_POOL_SIZE+x}" ]; then
|
||||
DOCKER_POOL_SIZE_PROVIDED=true
|
||||
fi
|
||||
|
||||
restart_docker_service() {
|
||||
# Check if systemctl is available
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl restart docker
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker daemon restarted successfully"
|
||||
else
|
||||
echo " - Failed to restart Docker daemon"
|
||||
return 1
|
||||
fi
|
||||
# Check if service command is available
|
||||
elif command -v service >/dev/null 2>&1; then
|
||||
service docker restart
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker daemon restarted successfully"
|
||||
else
|
||||
echo " - Failed to restart Docker daemon"
|
||||
return 1
|
||||
fi
|
||||
# If neither systemctl nor service is available
|
||||
else
|
||||
echo " - Error: No service management system found"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to compare address pools
|
||||
compare_address_pools() {
|
||||
local base1="$1"
|
||||
local size1="$2"
|
||||
local base2="$3"
|
||||
local size2="$4"
|
||||
|
||||
# Normalize CIDR notation for comparison
|
||||
local ip1=$(echo "$base1" | cut -d'/' -f1)
|
||||
local prefix1=$(echo "$base1" | cut -d'/' -f2)
|
||||
local ip2=$(echo "$base2" | cut -d'/' -f1)
|
||||
local prefix2=$(echo "$base2" | cut -d'/' -f2)
|
||||
|
||||
# Compare IPs and prefixes
|
||||
if [ "$ip1" = "$ip2" ] && [ "$prefix1" = "$prefix2" ] && [ "$size1" = "$size2" ]; then
|
||||
return 0 # Pools are the same
|
||||
else
|
||||
return 1 # Pools are different
|
||||
fi
|
||||
}
|
||||
|
||||
# Docker address pool configuration
|
||||
DOCKER_ADDRESS_POOL_BASE=${DOCKER_ADDRESS_POOL_BASE:-"$DOCKER_ADDRESS_POOL_BASE_DEFAULT"}
|
||||
DOCKER_ADDRESS_POOL_SIZE=${DOCKER_ADDRESS_POOL_SIZE:-$DOCKER_ADDRESS_POOL_SIZE_DEFAULT}
|
||||
|
||||
# Load Docker address pool configuration from .env file if it exists and environment variables were not provided
|
||||
if [ -f "/data/coolify/source/.env" ] && [ "$DOCKER_POOL_BASE_PROVIDED" = false ] && [ "$DOCKER_POOL_SIZE_PROVIDED" = false ]; then
|
||||
ENV_DOCKER_ADDRESS_POOL_BASE=$(grep -E "^DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env | cut -d '=' -f2 || true)
|
||||
ENV_DOCKER_ADDRESS_POOL_SIZE=$(grep -E "^DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env | cut -d '=' -f2 || true)
|
||||
|
||||
if [ -n "$ENV_DOCKER_ADDRESS_POOL_BASE" ]; then
|
||||
DOCKER_ADDRESS_POOL_BASE="$ENV_DOCKER_ADDRESS_POOL_BASE"
|
||||
fi
|
||||
|
||||
if [ -n "$ENV_DOCKER_ADDRESS_POOL_SIZE" ]; then
|
||||
DOCKER_ADDRESS_POOL_SIZE="$ENV_DOCKER_ADDRESS_POOL_SIZE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if daemon.json exists and extract existing address pool configuration
|
||||
EXISTING_POOL_CONFIGURED=false
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
if jq -e '.["default-address-pools"]' /etc/docker/daemon.json >/dev/null 2>&1; then
|
||||
EXISTING_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null || true)
|
||||
EXISTING_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null || true)
|
||||
|
||||
if [ -n "$EXISTING_POOL_BASE" ] && [ -n "$EXISTING_POOL_SIZE" ] && [ "$EXISTING_POOL_BASE" != "null" ] && [ "$EXISTING_POOL_SIZE" != "null" ]; then
|
||||
echo "Found existing Docker network pool: $EXISTING_POOL_BASE/$EXISTING_POOL_SIZE"
|
||||
EXISTING_POOL_CONFIGURED=true
|
||||
|
||||
# Check if environment variables were explicitly provided
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = false ] && [ "$DOCKER_POOL_SIZE_PROVIDED" = false ]; then
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
else
|
||||
# Check if force override is enabled
|
||||
if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ]; then
|
||||
echo "Force override enabled - network pool will be updated with $DOCKER_ADDRESS_POOL_BASE/$DOCKER_ADDRESS_POOL_SIZE."
|
||||
else
|
||||
echo "Custom pool provided but force override not enabled - using existing configuration."
|
||||
echo "To force override, set DOCKER_POOL_FORCE_OVERRIDE=true"
|
||||
echo "This won't change the existing docker networks, only the pool configuration for the newly created networks."
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
DOCKER_POOL_BASE_PROVIDED=false
|
||||
DOCKER_POOL_SIZE_PROVIDED=false
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate Docker address pool configuration
|
||||
if ! [[ $DOCKER_ADDRESS_POOL_BASE =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]]; then
|
||||
echo "Warning: Invalid network pool base format: $DOCKER_ADDRESS_POOL_BASE"
|
||||
if [ "$EXISTING_POOL_CONFIGURED" = true ]; then
|
||||
echo "Using existing configuration: $EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
else
|
||||
echo "Using default configuration: $DOCKER_ADDRESS_POOL_BASE_DEFAULT"
|
||||
DOCKER_ADDRESS_POOL_BASE="$DOCKER_ADDRESS_POOL_BASE_DEFAULT"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! [[ $DOCKER_ADDRESS_POOL_SIZE =~ ^[0-9]+$ ]] || [ "$DOCKER_ADDRESS_POOL_SIZE" -lt 16 ] || [ "$DOCKER_ADDRESS_POOL_SIZE" -gt 28 ]; then
|
||||
echo "Warning: Invalid network pool size: $DOCKER_ADDRESS_POOL_SIZE (must be 16-28)"
|
||||
if [ "$EXISTING_POOL_CONFIGURED" = true ]; then
|
||||
echo "Using existing configuration: $EXISTING_POOL_SIZE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
else
|
||||
echo "Using default configuration: $DOCKER_ADDRESS_POOL_SIZE_DEFAULT"
|
||||
DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE_DEFAULT
|
||||
fi
|
||||
fi
|
||||
|
||||
TOTAL_SPACE=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
|
||||
AVAILABLE_SPACE=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//')
|
||||
REQUIRED_TOTAL_SPACE=30
|
||||
@@ -35,7 +182,7 @@ WARNING_SPACE=false
|
||||
|
||||
if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then
|
||||
WARNING_SPACE=true
|
||||
cat << EOF
|
||||
cat <<EOF
|
||||
WARNING: Insufficient total disk space!
|
||||
|
||||
Total disk space: ${TOTAL_SPACE}GB
|
||||
@@ -46,7 +193,7 @@ EOF
|
||||
fi
|
||||
|
||||
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then
|
||||
cat << EOF
|
||||
cat <<EOF
|
||||
WARNING: Insufficient available disk space!
|
||||
|
||||
Available disk space: ${AVAILABLE_SPACE}GB
|
||||
@@ -54,7 +201,7 @@ Required available space: ${REQUIRED_AVAILABLE_SPACE}GB
|
||||
|
||||
==================
|
||||
EOF
|
||||
WARNING_SPACE=true
|
||||
WARNING_SPACE=true
|
||||
fi
|
||||
|
||||
if [ "$WARNING_SPACE" = true ]; then
|
||||
@@ -62,7 +209,7 @@ if [ "$WARNING_SPACE" = true ]; then
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,ssl,webhooks-during-maintenance,sentinel}
|
||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||
mkdir -p /data/coolify/proxy/dynamic
|
||||
|
||||
@@ -136,7 +283,6 @@ if [ -z "$LATEST_REALTIME_VERSION" ]; then
|
||||
LATEST_REALTIME_VERSION=latest
|
||||
fi
|
||||
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
*)
|
||||
@@ -152,14 +298,13 @@ if [ "$1" != "" ]; then
|
||||
LATEST_VERSION="${LATEST_VERSION#v}"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo "| Coolify | $LATEST_VERSION"
|
||||
echo "| Helper | $LATEST_HELPER_VERSION"
|
||||
echo "| Realtime | $LATEST_REALTIME_VERSION"
|
||||
echo "| Docker Pool | $DOCKER_ADDRESS_POOL_BASE (size $DOCKER_ADDRESS_POOL_SIZE)"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
@@ -199,7 +344,6 @@ sles | opensuse-leap | opensuse-tumbleweed)
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
echo -e "2. Check OpenSSH server configuration. "
|
||||
|
||||
# Detect OpenSSH server
|
||||
@@ -222,7 +366,6 @@ elif [ -x "$(command -v service)" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
if [ "$SSH_DETECTED" = "false" ]; then
|
||||
echo " - OpenSSH server not detected. Installing OpenSSH server."
|
||||
case "$OS_TYPE" in
|
||||
@@ -288,86 +431,112 @@ if [ -x "$(command -v snap)" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
install_docker() {
|
||||
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
echo -e "3. Check Docker Installation. "
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
getAJoke
|
||||
case "$OS_TYPE" in
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"centos" | "fedora" | "rhel")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"ubuntu" | "debian" | "raspbian")
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
echo " - Installing Docker for Ubuntu 24.10..."
|
||||
apt-get update >/dev/null
|
||||
apt-get install -y ca-certificates curl >/dev/null
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
# Add the repository to Apt sources
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" |
|
||||
tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||||
apt-get update >/dev/null
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null
|
||||
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
echo " - Docker installation failed."
|
||||
echo " Please visit https://docs.docker.com/engine/install/ubuntu/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"centos" | "fedora" | "rhel")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
|
||||
echo "Please install Docker manually."
|
||||
exit 1
|
||||
fi
|
||||
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo " - Docker installed successfully for Ubuntu 24.10."
|
||||
else
|
||||
install_docker
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
install_docker
|
||||
;;
|
||||
esac
|
||||
echo " - Docker installed successfully."
|
||||
else
|
||||
@@ -375,82 +544,132 @@ else
|
||||
fi
|
||||
|
||||
echo -e "4. Check Docker Configuration. "
|
||||
|
||||
echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
echo " - To override existing configuration: DOCKER_POOL_FORCE_OVERRIDE=true"
|
||||
|
||||
mkdir -p /etc/docker
|
||||
# shellcheck disable=SC2015
|
||||
test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" || cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"10.0.0.0/8","size":24}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
cat >/etc/docker/daemon.json.coolify <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"10.0.0.0/8","size":24}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
TEMP_FILE=$(mktemp)
|
||||
if ! jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify >"$TEMP_FILE"; then
|
||||
echo "Error merging JSON files"
|
||||
exit 1
|
||||
|
||||
# Backup original daemon.json if it exists
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE"
|
||||
fi
|
||||
mv "$TEMP_FILE" /etc/docker/daemon.json
|
||||
|
||||
restart_docker_service() {
|
||||
# Check if systemctl is available
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
echo " - Using systemctl to restart Docker."
|
||||
systemctl restart docker
|
||||
# Create coolify configuration with or without address pools based on whether they were explicitly provided
|
||||
if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ] || [ "$EXISTING_POOL_CONFIGURED" = false ]; then
|
||||
# First check if the configuration would actually change anything
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
CURRENT_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null)
|
||||
CURRENT_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null)
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker restarted successfully using systemctl."
|
||||
if [ "$CURRENT_POOL_BASE" = "$DOCKER_ADDRESS_POOL_BASE" ] && [ "$CURRENT_POOL_SIZE" = "$DOCKER_ADDRESS_POOL_SIZE" ]; then
|
||||
echo " - Network pool configuration unchanged, skipping update"
|
||||
NEED_MERGE=false
|
||||
else
|
||||
echo " - Failed to restart Docker using systemctl."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if service command is available
|
||||
elif command -v service >/dev/null 2>&1; then
|
||||
echo " - Using service command to restart Docker."
|
||||
service docker restart
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker restarted successfully using service."
|
||||
else
|
||||
echo " - Failed to restart Docker using service."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If neither systemctl nor service is available
|
||||
else
|
||||
echo " - Neither systemctl nor service command is available on this system."
|
||||
return 1
|
||||
fi
|
||||
# If force override is enabled or no existing configuration exists,
|
||||
# create a new configuration with the specified address pools
|
||||
echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
|
||||
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
|
||||
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE"))
|
||||
if [ "$DIFF" != "" ]; then
|
||||
echo " - Docker configuration updated, restart docker daemon..."
|
||||
restart_docker_service
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
else
|
||||
echo " - Docker configuration is up to date."
|
||||
# No existing configuration, create new one
|
||||
echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
else
|
||||
echo " - Docker configuration updated, restart docker daemon..."
|
||||
restart_docker_service
|
||||
# Check if we need to update log settings
|
||||
if [ -f /etc/docker/daemon.json ] && jq -e '.["log-driver"] == "json-file" and .["log-opts"]["max-size"] == "10m" and .["log-opts"]["max-file"] == "3"' /etc/docker/daemon.json >/dev/null 2>&1; then
|
||||
echo " - Log configuration is up to date"
|
||||
NEED_MERGE=false
|
||||
else
|
||||
# Create a configuration without address pools to preserve existing ones
|
||||
cat >/etc/docker/daemon.json.coolify <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
}
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove the duplicate daemon.json creation since we handle it above
|
||||
if ! [ -f /etc/docker/daemon.json ]; then
|
||||
# If no daemon.json exists, create it with default settings
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=false
|
||||
fi
|
||||
|
||||
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
|
||||
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE") || true)
|
||||
if [ "$DIFF" != "" ]; then
|
||||
echo " - Checking configuration changes..."
|
||||
|
||||
# Check if address pools were changed
|
||||
if echo "$DIFF" | grep -q "default-address-pools"; then
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ] || [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
echo " - Network pool updated per user request"
|
||||
else
|
||||
echo " - Warning: Network pool modified without explicit request"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove this redundant restart since we already restarted when writing the config
|
||||
echo " - Configuration changes confirmed"
|
||||
if [ "$NEED_MERGE" = true ]; then
|
||||
echo " - Configuration updated - restarting Docker daemon..."
|
||||
restart_docker_service
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
else
|
||||
if [ "$NEED_MERGE" = true ]; then
|
||||
echo " - Configuration updated - restarting Docker daemon..."
|
||||
restart_docker_service
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "5. Download required files from CDN. "
|
||||
@@ -501,7 +720,7 @@ fi
|
||||
|
||||
# Merge .env and .env.production. New values will be added to .env
|
||||
echo -e "7. Propagating .env with new values - if necessary."
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production >$ENV_FILE
|
||||
|
||||
if [ "$AUTOUPDATE" = "false" ]; then
|
||||
if ! grep -q "AUTOUPDATE=" /data/coolify/source/.env; then
|
||||
@@ -510,6 +729,26 @@ if [ "$AUTOUPDATE" = "false" ]; then
|
||||
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save Docker address pool configuration to .env file
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE" >>/data/coolify/source/.env
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_BASE=.*|DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE" >>/data/coolify/source/.env
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_SIZE=.*|DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "8. Checking for SSH key for localhost access."
|
||||
if [ ! -f ~/.ssh/authorized_keys ]; then
|
||||
mkdir -p ~/.ssh
|
||||
@@ -527,7 +766,7 @@ if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
|
||||
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify
|
||||
chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal
|
||||
sed -i "/coolify/d" ~/.ssh/authorized_keys
|
||||
cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >> ~/.ssh/authorized_keys
|
||||
cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >>~/.ssh/authorized_keys
|
||||
rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub
|
||||
fi
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user