Compare commits

...

116 Commits

Author SHA1 Message Date
Andras Bacsai
dc7f56b7aa Merge pull request #1980 from coollabsio/next
v4.0.0-beta.258
2024-04-12 23:14:31 +02:00
Andras Bacsai
2295ddddee Merge pull request #1979 from w3cj/fix-livewire-snapshot-missing
fix #1975: add wire:key to edit-domain
2024-04-12 23:13:31 +02:00
Andras Bacsai
3328d20b2d Refactor domain editing in service configuration view 2024-04-12 23:13:17 +02:00
Andras Bacsai
d444211c68 Update version numbers and fix domain editing 2024-04-12 23:12:15 +02:00
w3cj
2de8cd1d6f fix #1975: add wire:key to edit-domain 2024-04-12 14:49:26 -06:00
Andras Bacsai
115813ac44 Merge pull request #1977 from coollabsio/next
v4.0.0-beta.257
2024-04-12 21:49:02 +02:00
Andras Bacsai
7c0e8d9421 Refactor pricing plans component and update button styles 2024-04-12 21:45:36 +02:00
Andras Bacsai
1c349cf55c Merge pull request #1972 from w3cj/allow-tab-in-textarea
bug: fix allowTab logic
2024-04-12 21:43:05 +02:00
Andras Bacsai
7b4d8a8f05 wip: new pricing 2024-04-12 15:48:56 +02:00
w3cj
6e7e2de490 bug: use 2 space instead of tab 2024-04-12 07:23:48 -06:00
w3cj
0a9a92ee7d bug: fix allowTab logic 2024-04-12 06:36:23 -06:00
Andras Bacsai
3bfac7ef3b Merge pull request #1962 from coollabsio/next
v4.0.0-beta.256
2024-04-12 14:31:49 +02:00
Andras Bacsai
1581e0e439 Merge pull request #1971 from w3cj/allow-tab-in-textarea
feat: allow tab in textarea
2024-04-12 14:14:57 +02:00
Andras Bacsai
6773848874 Add dispatch call to check_status method in Heading.php 2024-04-12 14:12:06 +02:00
Andras Bacsai
3e9219173c Refactor deployment logic in ApplicationDeploymentJob.php 2024-04-12 14:03:29 +02:00
w3cj
d46a565e6c feat: allow tab in textarea 2024-04-12 05:33:56 -06:00
Andras Bacsai
6267804d38 fix: redeploy if image not found in restart only mode 2024-04-12 13:24:42 +02:00
Andras Bacsai
25c4d282ce Update delete configuration label in danger.blade.php 2024-04-12 13:19:47 +02:00
Andras Bacsai
45281360d5 fix: service config hash update 2024-04-12 13:15:24 +02:00
Andras Bacsai
7a32b8d1d2 feat: configuration checker for all resources 2024-04-12 12:44:49 +02:00
Andras Bacsai
7b0018b661 Add new popup-small component and update general.blade.php 2024-04-12 11:11:55 +02:00
Andras Bacsai
9032879e20 feat: able to delete configuration from server 2024-04-12 10:54:25 +02:00
Andras Bacsai
8b668cf8b7 Refactor deployment logic in ApplicationDeploymentJob.php 2024-04-12 10:54:09 +02:00
Andras Bacsai
3323533fd0 Update Dropzone library version and add async attribute to script tag in import.blade.php 2024-04-12 10:48:43 +02:00
Andras Bacsai
50e96baea1 Refactor Livewire project new select blade template 2024-04-12 10:28:40 +02:00
Andras Bacsai
2ac92df9d3 Refactor Livewire project new select blade template 2024-04-12 09:46:52 +02:00
Andras Bacsai
63e64cea60 Fix is_multiline default value in EnvironmentVariable.php 2024-04-12 09:34:56 +02:00
Andras Bacsai
0267c76de2 Update password reset views and fix layout in ForcePasswordReset.php and reset-password.blade.php 2024-04-12 09:26:07 +02:00
Andras Bacsai
2382a10bba Refactor environment variable saving logic in ApplicationDeploymentJob.php and escapeEnvVariables function in docker.php 2024-04-12 09:26:04 +02:00
Andras Bacsai
a8db40e99a feat: edit domains easier for compose 2024-04-11 15:42:37 +02:00
Andras Bacsai
8553fffffe Update startup commands for Horizon and Scheduler Worker services 2024-04-11 15:07:41 +02:00
Andras Bacsai
d9d38a27ff Fix empty ray() function call in StartClickhouse.php 2024-04-11 13:46:34 +02:00
Andras Bacsai
1e7a5562ab Update Clickhouse database credentials and variables 2024-04-11 13:45:02 +02:00
Andras Bacsai
e71032a8fc Fix variable assignment in Import.php and ApplicationDeploymentJob.php 2024-04-11 13:20:46 +02:00
Andras Bacsai
484a3dbe5c Add scheduled task to clear uploads every two minutes 2024-04-11 12:56:24 +02:00
Andras Bacsai
9c7f40e4fe feat: upload large backups 2024-04-11 12:13:11 +02:00
Andras Bacsai
f35b7ab6f4 Update labels for database accessibility in blade files 2024-04-11 08:55:11 +02:00
Andras Bacsai
a38dd3d5c8 Merge pull request #1964 from therumbler/patch-1
Changed wording to make more sense
2024-04-11 08:36:24 +02:00
Benjamin Rumble
00ad35fc3e Changed wording to make more sense
Previous wording is incorrect English
2024-04-10 17:57:20 -04:00
Andras Bacsai
0d78b44c80 Fix formatting in deployment show.blade.php file 2024-04-10 21:15:23 +02:00
Andras Bacsai
fb614637a9 Fix download_file function in backup-executions.blade.php 2024-04-10 21:12:50 +02:00
Andras Bacsai
d0482d66c2 Refactor code for displaying command output in deployment show view 2024-04-10 21:11:41 +02:00
Andras Bacsai
ef178dfd24 Remove unnecessary steps in GitHub App selection and repository selection 2024-04-10 21:02:30 +02:00
Andras Bacsai
6cdc6e0780 Refactor download.backup route to use authentication middleware in web.php 2024-04-10 15:34:22 +02:00
Andras Bacsai
c961e6d9c7 Add authentication middleware to download.backup route in web.php 2024-04-10 15:29:45 +02:00
Andras Bacsai
c6844ff47a Add keydb, dragonfly and clickhouse 2024-04-10 15:00:46 +02:00
Andras Bacsai
605a630411 Update download_file method name in backup-executions.blade.php 2024-04-10 14:36:57 +02:00
Andras Bacsai
2b75cf1508 Update sponsor logos in README.md 2024-04-10 08:57:44 +02:00
Andras Bacsai
007054301b Update version numbers to 4.0.0-beta.256 2024-04-10 08:57:42 +02:00
Andras Bacsai
cf01d32237 Merge pull request #1960 from coollabsio/next
v4.0.0-beta.255
2024-04-10 08:36:45 +02:00
Andras Bacsai
5ec553f68d Reset default labels when application FQDN is updated 2024-04-10 08:21:06 +02:00
Andras Bacsai
d95e5a169d Update version numbers to 4.0.0-beta.255 2024-04-10 08:17:31 +02:00
Andras Bacsai
8debefddad Merge pull request #1955 from coollabsio/next
v4.0.0-beta.254
2024-04-09 13:17:02 +02:00
Andras Bacsai
bd25860ccf Add check for enabled tasks in scheduled tasks loop 2024-04-09 13:03:06 +02:00
Andras Bacsai
b21cb5c0e9 Update job schedules to run every two minutes instead of every minute 2024-04-09 13:02:22 +02:00
Andras Bacsai
ff79a2d3f4 Commented out ray() function call in remoteProcess.php 2024-04-09 13:02:16 +02:00
Andras Bacsai
9ae3743a58 Update job schedules and version numbers 2024-04-09 12:48:58 +02:00
Andras Bacsai
ffcdbcc802 Merge branch 'main' into next 2024-04-09 12:28:43 +02:00
Andras Bacsai
3ff78a3a47 Update version to 1.3.1 in install.sh 2024-04-09 12:28:11 +02:00
Andras Bacsai
4d78ac4789 Remove unnecessary sed command in install.sh script 2024-04-09 12:28:05 +02:00
Andras Bacsai
be24f2d520 Create SECURITY.md 2024-04-09 12:27:35 +02:00
Andras Bacsai
ff7fccb6a2 Merge pull request #1954 from coollabsio/next
v4.0.0-beta.253
2024-04-09 12:22:32 +02:00
Andras Bacsai
ee5a2b3c38 Add migration to make custom docker commands longer 2024-04-09 11:58:14 +02:00
Andras Bacsai
72b9001447 Fix error message in generate_github_installation_token function 2024-04-09 11:54:15 +02:00
Andras Bacsai
353245bb7d fix: hashed random delimeter in ssh commands + make sure to remove the delimeter from the command 2024-04-09 11:48:57 +02:00
Andras Bacsai
f9411bf0ed Remove debug statement in generateGitImportCommands function 2024-04-09 11:48:26 +02:00
Andras Bacsai
0eedbd2aa1 Update version numbers to 4.0.0-beta.253 2024-04-09 11:09:16 +02:00
Andras Bacsai
5e36c37838 Add coolify database and handle exceptions 2024-04-09 11:09:13 +02:00
Andras Bacsai
35c76c8e2a Merge pull request #1946 from coollabsio/next
v4.0.0-beta.252
2024-04-09 09:28:29 +02:00
Andras Bacsai
90ad46b7c5 Update SUPPORTED_OS with new OS: amzn 2024-04-09 09:25:55 +02:00
Andras Bacsai
1c06e1e2a4 Update version numbers to 4.0.0-beta.252 2024-04-09 09:03:53 +02:00
Andras Bacsai
15d2c0e436 Update font style for logs in deployment show view 2024-04-09 08:46:06 +02:00
Andras Bacsai
9984aea42f Add ContainerStatusJob to Revived notification 2024-04-09 08:46:03 +02:00
Andras Bacsai
a9cc5cc351 Fix server functionality check and cleanup SSH keys 2024-04-09 08:46:00 +02:00
Andras Bacsai
a6cbabfba5 Add server connection validation in CheckProxy.php 2024-04-09 08:45:37 +02:00
Andras Bacsai
1ef85242ec update supabase 2024-04-08 15:16:52 +02:00
Andras Bacsai
ffe75b0856 Merge pull request #1943 from gionatamettifogo/main
Update supabase.yaml - JWT_SERCET -> should be JWT_SECRET
2024-04-08 15:16:15 +02:00
Andras Bacsai
25e3c4fcdc feat: add amazon linux 2023 2024-04-08 14:33:07 +02:00
Andras Bacsai
a922497f5d Merge branch 'next' of github.com:coollabsio/coolify into next 2024-04-08 14:09:40 +02:00
Andras Bacsai
acb100908b Merge pull request #1895 from moinulmoin/refactor/ui
fix: unintended left padding on sidebar
2024-04-08 14:09:14 +02:00
Andras Bacsai
7fa2cbc746 Merge branch 'main' into refactor/ui 2024-04-08 14:08:16 +02:00
Andras Bacsai
b36491af8e Update Stirling PDF healthcheck 2024-04-08 14:07:18 +02:00
Andras Bacsai
ea4fe81cb2 fix: git submodule update 2024-04-08 14:07:07 +02:00
Andras Bacsai
2191f1b826 Refactor source.blade.php layout 2024-04-08 14:06:40 +02:00
Andras Bacsai
08f6367752 Fix conditional statement in getServiceTemplates function 2024-04-08 14:06:31 +02:00
Andras Bacsai
e1b09f4844 Merge pull request #1913 from loganspark/application-update-submodules-after-git-checkout
feat(application): update submodules after git checkout
2024-04-08 13:43:43 +02:00
Andras Bacsai
884c46b054 Merge branch 'main' into application-update-submodules-after-git-checkout 2024-04-08 13:40:16 +02:00
Andras Bacsai
d1cde123dc Merge pull request #1933 from gionatamettifogo/patch-1
Update stirling-pdf.yaml
2024-04-08 13:32:55 +02:00
Andras Bacsai
5eaded7e3a Update package-lock.json and package.json with latest versions of dependencies 2024-04-08 13:23:44 +02:00
Andras Bacsai
4ae4e88800 Update instance update process 2024-04-08 13:04:38 +02:00
Andras Bacsai
85fb27a631 Refactor project links and add click event handlers 2024-04-08 13:04:33 +02:00
Andras Bacsai
8bf0561009 Refactor code for domain usage and validation 2024-04-08 12:15:44 +02:00
Andras Bacsai
ddfded048c fix: how to update docker-compose, environment variables and fqdns 2024-04-08 11:17:00 +02:00
Andras Bacsai
a2e889587e Refactor environment variable sorting by key 2024-04-08 11:16:42 +02:00
Andras Bacsai
b63aaad645 Update warning message for multiple domains in ServiceApplicationView.php 2024-04-08 11:16:25 +02:00
Andras Bacsai
2c41b5d4fb Fix null pointer exception in Index.php 2024-04-08 11:16:20 +02:00
Andras Bacsai
34aa4c6412 fix: storage layout 2024-04-08 09:51:14 +02:00
Andras Bacsai
82372ea252 Refactor upgrade method in Upgrade.php 2024-04-08 09:36:24 +02:00
Andras Bacsai
1f4440bcf9 Refactor image name generation in ApplicationDeploymentJob.php 2024-04-08 09:36:21 +02:00
Andras Bacsai
31fcff4afc Update OAuth button width in login page 2024-04-08 09:16:16 +02:00
Gionata Mettifogo
082f17f940 Update supabase.yaml - JWT_SERCET -> should be JWT_SECRET
There is a spelling error in supabase.yml that prevents the auth component from working. 

JWT_SERCET -> should be JWT_SECRET
2024-04-07 11:56:46 +02:00
Andras Bacsai
aa2ac3865c Merge pull request #1938 from coollabsio/next
v4.0.0-beta.251
2024-04-05 18:50:18 +02:00
Andras Bacsai
db8ffe50ac fix: isMember 2024-04-05 18:47:07 +02:00
Andras Bacsai
8fe658bacc fix: members cannot manage subscriptions 2024-04-05 18:34:33 +02:00
Andras Bacsai
c71d5e2dfb Merge pull request #1936 from coollabsio/next
v4.0.0-beta.250
2024-04-05 16:50:08 +02:00
Andras Bacsai
36c31dcd67 Add role-based authorization for updating teams 2024-04-05 16:48:06 +02:00
Andras Bacsai
c223408c3c Add updatedApplicationFqdn method to ServiceApplicationView 2024-04-05 15:59:59 +02:00
Andras Bacsai
3cdccb49ef Fix deploy button alignment in destination.blade.php 2024-04-05 15:42:31 +02:00
Andras Bacsai
0844645a8b version++ 2024-04-05 15:33:33 +02:00
Andras Bacsai
30bfad455c ui: multiple server view 2024-04-05 15:33:29 +02:00
Andras Bacsai
d84fdc3cd5 fix: make sure if envs are changed, rebuild is needed 2024-04-05 15:33:19 +02:00
Andras Bacsai
2b64b9de63 fix: do not rebuild dockerfile based apps twice 2024-04-05 15:33:11 +02:00
Gionata Mettifogo
cdb3d863db Update stirling-pdf.yaml
The service works fine now but shows up as Running (unhealthy). Adding a check on its main page on port 8080 makes it healthy.
2024-04-04 08:38:53 +02:00
Darek Wróbel
9616d858cf feat(application): update submodules after git checkout 2024-03-28 09:53:09 +01:00
Moinul Moin
d9389c91ee Merge branch 'coollabsio:main' into refactor/ui 2024-03-26 19:05:04 +06:00
Moinul Moin
647376ab3f fix: unintended left padding on sidebar 2024-03-26 11:49:52 +00:00
168 changed files with 4448 additions and 762 deletions

View File

@@ -33,12 +33,13 @@ Special thanks to our biggest sponsor, [CCCareers](https://cccareers.org/)!
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="cccareers logo" width="200"/></a>
## Github Sponsors ($40+)
<a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a>
<a href="https://typebot.io/?utm_source=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a>
<a href="https://americancloud.com/?utm_source=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a>
<a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>
<a href="https://typebot.io/?utm_source=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a>
<a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a>
<a href="https://www.uxwizz.com/?utm_source=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a>
<a href="https://github.com/automazeio"><img src="https://github.com/automazeio.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/Flowko"><img src="https://barrad.me/_ipx/f_webp&s_300x300/younes.jpg" width="60px" alt="Younes Barrad" /></a>
<a href="https://github.com/automazeio"><img src="https://github.com/automazeio.png" width="60px" alt="Automaze" /></a>
<a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/Niki2k1"><img src="https://github.com/Niki2k1.png" width="60px" alt="Niklas Lausch" /></a>
<a href="https://github.com/pixelinfinito"><img src="https://github.com/pixelinfinito.png" width="60px" alt="Pixel Infinito" /></a>

16
SECURITY.md Normal file
View File

@@ -0,0 +1,16 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| > 4 | :white_check_mark: |
| 3 | :x: |
## Reporting a Vulnerability
If you have any vulnerability please report at hi@coollabs.io

View File

@@ -0,0 +1,159 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneClickhouse;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartClickhouse
{
use AsAction;
public StandaloneClickhouse $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneClickhouse $database)
{
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'ulimits' => [
'nofile' => [
'soft' => 262144,
'hard' => 262144,
],
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (float) $this->database->limits_cpus,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
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'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
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($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$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";
$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 generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) {
$environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) {
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}");
}
return $environment_variables->all();
}
}

View File

@@ -3,6 +3,9 @@
namespace App\Actions\Database;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -15,7 +18,7 @@ class StartDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{
$internalPort = null;
$type = $database->getMorphClass();
@@ -50,6 +53,18 @@ class StartDatabaseProxy
$type = 'App\Models\StandaloneRedis';
$containerName = "redis-{$database->service->uuid}";
break;
case 'standalone-keydb':
$type = 'App\Models\StandaloneKeydb';
$containerName = "keydb-{$database->service->uuid}";
break;
case 'standalone-dragonfly':
$type = 'App\Models\StandaloneDragonfly';
$containerName = "dragonfly-{$database->service->uuid}";
break;
case 'standalone-clickhouse':
$type = 'App\Models\StandaloneClickhouse';
$containerName = "clickhouse-{$database->service->uuid}";
break;
}
}
if ($type === 'App\Models\StandaloneRedis') {
@@ -62,6 +77,12 @@ class StartDatabaseProxy
$internalPort = 3306;
} else if ($type === 'App\Models\StandaloneMariadb') {
$internalPort = 3306;
} else if ($type === 'App\Models\StandaloneKeydb') {
$internalPort = 6379;
} else if ($type === 'App\Models\StandaloneDragonfly') {
$internalPort = 6379;
} else if ($type === 'App\Models\StandaloneClickhouse') {
$internalPort = 9000;
}
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF

View File

@@ -0,0 +1,156 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneDragonfly;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartDragonfly
{
use AsAction;
public StandaloneDragonfly $database;
public array $commands = [];
public string $configuration_dir;
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->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'command' => $startCommand,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'ulimits' => [
'memlock'=> '-1'
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => "redis-cli -a {$this->database->dragonfly_password} ping",
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (float) $this->database->limits_cpus,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
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'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
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($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$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";
$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 generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
}
return $environment_variables->all();
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneKeydb;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartKeydb
{
use AsAction;
public StandaloneKeydb $database;
public array $commands = [];
public string $configuration_dir;
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->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_keydb();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'command' => $startCommand,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => "keydb-cli --pass {$this->database->keydb_password} ping",
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (float) $this->database->limits_cpus,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
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'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
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($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($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 = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$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";
$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 generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
}
return $environment_variables->all();
}
private function add_custom_keydb()
{
if (is_null($this->database->keydb_conf)) {
return;
}
$filename = 'keydb.conf';
Storage::disk('local')->put("tmp/keydb.conf_{$this->database->uuid}", $this->database->keydb_conf);
$path = Storage::path("tmp/keydb.conf_{$this->database->uuid}");
instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
Storage::disk('local')->delete("tmp/keydb.conf_{$this->database->uuid}");
}
}

View File

@@ -114,8 +114,12 @@ class StartMariadb
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
return $local_persistent_volumes;
}

View File

@@ -130,8 +130,12 @@ class StartMongodb
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
return $local_persistent_volumes;
}

View File

@@ -114,8 +114,12 @@ class StartMysql
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
return $local_persistent_volumes;
}

View File

@@ -136,8 +136,12 @@ class StartPostgresql
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
return $local_persistent_volumes;
}

View File

@@ -125,8 +125,12 @@ class StartRedis
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
return $local_persistent_volumes;
}

View File

@@ -2,7 +2,9 @@
namespace App\Actions\Database;
use App\Events\DatabaseStatusChanged;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -14,7 +16,7 @@ class StopDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = $database->destination->server;
if (!$server->isFunctional()) {

View File

@@ -3,6 +3,9 @@
namespace App\Actions\Database;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -14,7 +17,7 @@ class StopDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = data_get($database, 'destination.server');
$uuid = $database->uuid;

View File

@@ -13,6 +13,9 @@ class CheckProxy
if ($server->proxyType() === 'NONE') {
return false;
}
if (!$server->validateConnection()) {
throw new \Exception("Server Connection Error");
}
if (!$server->isProxyShouldRun()) {
if ($fromUI) {
throw new \Exception("Proxy should not run. You selected the Custom Proxy.");

View File

@@ -45,7 +45,6 @@ class UpdateCoolify
}
$this->update();
}
send_internal_notification("Instance updated from {$this->currentVersion} -> {$this->latestVersion}");
} catch (\Throwable $e) {
ray('InstanceAutoUpdateJob failed');
ray($e->getMessage());
@@ -83,6 +82,7 @@ class UpdateCoolify
"bash /data/coolify/source/upgrade.sh $this->latestVersion"
], $this->server);
}
send_internal_notification("Instance updated from {$this->currentVersion} -> {$this->latestVersion}");
return;
}
}

View File

@@ -7,6 +7,9 @@ use App\Models\ScheduledTask;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -55,6 +58,33 @@ class CleanupStuckedResources extends Command
} catch (\Throwable $e) {
echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
}
try {
$keydbs = StandaloneKeydb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($keydbs as $keydb) {
echo "Deleting stuck keydb: {$keydb->name}\n";
$redis->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck keydb: {$e->getMessage()}\n";
}
try {
$dragonflies = StandaloneDragonfly::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($dragonflies as $dragonfly) {
echo "Deleting stuck dragonfly: {$dragonfly->name}\n";
$redis->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck dragonfly: {$e->getMessage()}\n";
}
try {
$clickhouses = StandaloneClickhouse::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($clickhouses as $clickhouse) {
echo "Deleting stuck clickhouse: {$clickhouse->name}\n";
$redis->forceDelete();
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck clickhouse: {$e->getMessage()}\n";
}
try {
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mongodbs as $mongodb) {

View File

@@ -33,6 +33,7 @@ class Kernel extends ConsoleKernel
$this->check_scheduled_backups($schedule);
$this->pull_helper_image($schedule);
$this->check_scheduled_tasks($schedule);
$schedule->command('uploads:clear')->everyTwoMinutes();
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
@@ -49,6 +50,7 @@ class Kernel extends ConsoleKernel
$this->check_scheduled_tasks($schedule);
$schedule->command('cleanup:database --yes')->daily();
$schedule->command('uploads:clear')->everyTwoMinutes();
}
}
private function pull_helper_image($schedule)
@@ -121,6 +123,9 @@ class Kernel extends ConsoleKernel
return;
}
foreach ($scheduled_tasks as $scheduled_task) {
if ($scheduled_task->enabled === false) {
continue;
}
$service = $scheduled_task->service;
$application = $scheduled_task->application;

View File

@@ -2,6 +2,9 @@
namespace App\Http\Controllers\Api;
use App\Actions\Database\StartClickhouse;
use App\Actions\Database\StartDragonfly;
use App\Actions\Database\StartKeydb;
use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql;
@@ -157,6 +160,24 @@ class Deploy extends Controller
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneKeydb') {
StartKeydb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneDragonfly') {
StartDragonfly::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneClickhouse') {
StartClickhouse::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneMongodb') {
StartMongodb::run($resource);
$resource->update([

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Http\JsonResponse;
use Pion\Laravel\ChunkUpload\Exceptions\UploadFailedException;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException;
use Pion\Laravel\ChunkUpload\Handler\AbstractHandler;
use Pion\Laravel\ChunkUpload\Handler\HandlerFactory;
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
class UploadController extends BaseController
{
public function upload(Request $request)
{
$resource = getResourceByUuid(request()->route('databaseUuid'), data_get(auth()->user()->currentTeam(), 'id'));
if (is_null($resource)) {
return response()->json(['error' => 'You do not have permission for this database'], 500);
}
$receiver = new FileReceiver("file", $request, HandlerFactory::classFromRequest($request));
if ($receiver->isUploaded() === false) {
throw new UploadMissingFileException();
}
$save = $receiver->receive();
if ($save->isFinished()) {
return $this->saveFile($save->getFile(), $resource);
}
$handler = $save->handler();
return response()->json([
"done" => $handler->getPercentageDone(),
'status' => true
]);
}
// protected function saveFileToS3($file)
// {
// $fileName = $this->createFilename($file);
// $disk = Storage::disk('s3');
// // It's better to use streaming Streaming (laravel 5.4+)
// $disk->putFileAs('photos', $file, $fileName);
// // for older laravel
// // $disk->put($fileName, file_get_contents($file), 'public');
// $mime = str_replace('/', '-', $file->getMimeType());
// // We need to delete the file when uploaded to s3
// unlink($file->getPathname());
// return response()->json([
// 'path' => $disk->url($fileName),
// 'name' => $fileName,
// 'mime_type' => $mime
// ]);
// }
protected function saveFile(UploadedFile $file, $resource)
{
$mime = str_replace('/', '-', $file->getMimeType());
$filePath = "upload/{$resource->uuid}";
$finalPath = storage_path("app/" . $filePath);
$file->move($finalPath, 'restore');
return response()->json([
'mime_type' => $mime
]);
}
protected function createFilename(UploadedFile $file)
{
$extension = $file->getClientOriginalExtension();
$filename = str_replace("." . $extension, "", $file->getClientOriginalName()); // Filename without extension
$filename .= "_" . md5(time()) . "." . $extension;
return $filename;
}
}

View File

@@ -159,8 +159,12 @@ class Stripe extends Controller
$feedback = data_get($data, 'cancellation_details.feedback');
$comment = data_get($data, 'cancellation_details.comment');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
if (str($lookup_key)->contains('ultimate')) {
$quantity = data_get($data, 'items.data.0.quantity', 10);
if (str($lookup_key)->contains('ultimate') || str($lookup_key)->contains('dynamic')) {
if (str($lookup_key)->contains('dynamic')) {
$quantity = data_get($data, 'items.data.0.quantity', 2);
} else {
$quantity = data_get($data, 'items.data.0.quantity', 10);
}
$team = data_get($subscription, 'team');
$team->update([
'custom_server_limit' => $quantity,

View File

@@ -83,6 +83,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $env_nixpacks_args;
private $docker_compose;
private $docker_compose_base64;
private ?string $env_filename = null;
private ?string $nixpacks_plan = null;
private ?string $nixpacks_type = null;
private string $dockerfile_location = '/Dockerfile';
@@ -179,6 +180,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void
{
if (!$this->server->isFunctional()) {
$this->application_deployment_queue->addLogEntry("Server is not functional.");
$this->fail("Server is not functional.");
return;
}
try {
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
@@ -234,50 +240,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_server = $this->server;
$this->original_server = $this->server;
}
if ($this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile') {
$this->just_restart();
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(false);
$this->run_post_deployment_command();
return;
} else if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockercompose') {
$this->deploy_docker_compose_buildpack();
} else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile_buildpack();
} else if ($this->application->build_pack === 'static') {
$this->deploy_static_buildpack();
} else {
$this->deploy_nixpacks_buildpack();
}
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
// Otherwise built image needs to be pushed before from the build server.
// ray($this->use_build_server);
// if (!$this->use_build_server) {
// if ($this->application->additional_servers->count() > 0) {
// $this->push_to_docker_registry(forceFail: true);
// } else {
// $this->push_to_docker_registry();
// }
// }
$this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) {
if ($this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED);
}
}
$this->run_post_deployment_command();
$this->application->isConfigurationChanged(true);
$this->decide_what_to_do();
} catch (Exception $e) {
if ($this->pull_request_id !== 0 && $this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::ERROR);
@@ -312,6 +275,43 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
}
}
private function decide_what_to_do()
{
if ($this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile') {
$this->just_restart();
return;
} else if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockercompose') {
$this->deploy_docker_compose_buildpack();
} else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile_buildpack();
} else if ($this->application->build_pack === 'static') {
$this->deploy_static_buildpack();
} else {
$this->deploy_nixpacks_buildpack();
}
$this->post_deployment();
}
private function post_deployment()
{
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) {
if ($this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED);
}
}
$this->run_post_deployment_command();
$this->application->isConfigurationChanged(true);
}
private function deploy_simple_dockerfile()
{
if ($this->use_build_server) {
@@ -331,7 +331,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// if (!$this->force_rebuild) {
// $this->check_image_locally_or_remotely();
// if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
// $this->create_workdir();
// $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
// $this->generate_compose_file();
// $this->push_to_docker_registry();
@@ -469,17 +468,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->set_base_dir();
$this->generate_image_names();
$this->clone_repository();
// if (!$this->force_rebuild) {
// $this->check_image_locally_or_remotely();
// if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
// $this->create_workdir();
// $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
// $this->generate_compose_file();
// $this->push_to_docker_registry();
// $this->rolling_update();
// return;
// }
// }
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
$this->push_to_docker_registry();
$this->rolling_update();
return;
}
}
$this->cleanup_git();
$this->generate_compose_file();
$this->generate_build_env_variables();
@@ -501,7 +499,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
ray('pushing to docker registry');
@@ -535,7 +532,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
$this->push_to_docker_registry();
@@ -646,21 +642,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
if ($this->application->dockerfile) {
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest");
$this->build_image_name = "{$this->application->docker_registry_image_name}:build";
$this->production_image_name = "{$this->application->docker_registry_image_name}:latest";
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
$this->build_image_name = "{$this->application->uuid}:build";
$this->production_image_name = "{$this->application->uuid}:latest";
}
} else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
$this->production_image_name = "{$this->dockerImage}:{$this->dockerImageTag}";
} else if ($this->pull_request_id !== 0) {
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}");
$this->build_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build";
$this->production_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}";
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
$this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build";
$this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}";
}
} else {
$this->dockerImageTag = str($this->commit)->substr(0, 128);
@@ -668,17 +664,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->dockerImageTag = $this->application->docker_registry_image_tag;
}
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}");
$this->build_image_name = "{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build";
$this->production_image_name = "{$this->application->docker_registry_image_name}:{$this->dockerImageTag}";
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}");
$this->build_image_name = "{$this->application->uuid}:{$this->dockerImageTag}-build";
$this->production_image_name = "{$this->application->uuid}:{$this->dockerImageTag}";
}
}
}
private function just_restart()
{
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->application_deployment_queue->addLogEntry("Restarting {$this->customRepository}:{$this->application->git_branch} on {$this->server->name}.");
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
@@ -686,12 +682,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.");
$this->create_workdir();
$this->generate_compose_file();
$this->rolling_update();
return;
$this->post_deployment();
} else {
$this->application_deployment_queue->addLogEntry("Image not found ({$this->production_image_name}). Redeploying the application.");
$this->restart_only = false;
$this->decide_what_to_do();
}
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
}
private function check_image_locally_or_remotely()
{
@@ -711,19 +709,35 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
$envs = collect([]);
if ($this->pull_request_id !== 0) {
$this->env_filename = ".env-pr-$this->pull_request_id";
foreach ($this->application->environment_variables_preview as $env) {
$envs->push($env->key . '=' . $env->real_value);
}
} else {
$this->env_filename = ".env";
foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->real_value);
}
}
if ($envs->isEmpty()) {
$this->env_filename = null;
$this->execute_remote_command(
[
"command" => "rm -f $this->configuration_dir/{$this->env_filename}",
"hidden" => true,
"ignore_errors" => true
]
);
return;
}
$envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
],
[
"echo '$envs_base64' | base64 -d > $this->configuration_dir/{$this->env_filename}"
]
);
}
@@ -863,6 +877,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}")
],
[
"command" => "mkdir -p {$this->configuration_dir}"
],
);
}
private function prepare_builder_image()
@@ -885,6 +902,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
$this->execute_remote_command(
[
"command" => "docker rm -f {$this->deployment_uuid}",
"ignore_errors" => true,
"hidden" => true
],
[
$runCommand,
"hidden" => true,
@@ -988,6 +1010,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$importCommands, "hidden" => true
]
);
$this->create_workdir();
}
private function generate_git_import_commands()
@@ -1001,11 +1024,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return $commands;
}
private function set_git_import_settings($git_clone_command)
{
return $this->application->setGitImportSettings($this->deployment_uuid, $git_clone_command);
}
private function cleanup_git()
{
$this->execute_remote_command(
@@ -1098,6 +1116,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_compose_file()
{
$this->create_workdir();
$ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array;
$onlyPort = null;
if (count($ports) > 0) {
@@ -1181,6 +1200,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
]
];
if ($this->env_filename) {
$docker_compose['services'][$this->container_name]['env_file'] = [
$this->env_filename
];
}
if (!$this->custom_healthcheck_found) {
$docker_compose['services'][$this->container_name]['healthcheck'] = [
'test' => [
@@ -1334,13 +1358,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
$this->save_environment_variables();
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->application->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$volume_name = $persistentStorage->host_path;
} else {
$volume_name = $persistentStorage->name;
}
if ($this->pull_request_id !== 0) {
$volume_name = $volume_name . '-pr-' . $this->pull_request_id;
}
@@ -1814,7 +1843,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void
{
$this->next(ApplicationDeploymentStatus::FAILED->value);
$this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
if (str($exception->getMessage())->isNotEmpty()) {

View File

@@ -8,6 +8,9 @@ use App\Actions\Service\DeleteService;
use App\Actions\Service\StopService;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -25,7 +28,7 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource)
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false)
{
}
@@ -42,6 +45,9 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
case 'standalone-mongodb':
case 'standalone-mysql':
case 'standalone-mariadb':
case 'standalone-keydb':
case 'standalone-dragonfly':
case 'standalone-clickhouse':
StopDatabase::run($this->resource);
break;
case 'service':
@@ -49,6 +55,9 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
DeleteService::run($this->resource);
break;
}
if ($this->deleteConfigurations) {
$this->resource?->delete_configurations();
}
} catch (\Throwable $e) {
ray($e->getMessage());
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());

View File

@@ -24,7 +24,7 @@ class ForcePasswordReset extends Component
}
public function render()
{
return view('livewire.force-password-reset')->layout('layouts.simple');
return view('livewire.force-password-reset');
}
public function submit()
{

View File

@@ -24,7 +24,7 @@ class General extends Component
public $customLabels;
public bool $labelsChanged = false;
public bool $isConfigurationChanged = false;
public bool $initLoadingCompose = false;
public ?string $initialDockerComposeLocation = null;
public ?string $initialDockerComposePrLocation = null;
@@ -123,19 +123,22 @@ class General extends Component
$this->application->settings->save();
}
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes;
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->application->isConfigurationChanged(true);
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
$this->customLabels = $this->application->parseContainerLabels();
$this->customLabels = $this->application->parseContainerLabels();
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
}
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
if ($this->application->build_pack === 'dockercompose' && !$this->application->docker_compose_raw) {
$this->initLoadingCompose = true;
$this->dispatch('info', 'Loading docker compose file...');
}
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->dispatch('configurationChanged');
}
}
public function instantSave()
{
@@ -154,31 +157,36 @@ class General extends Component
}
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
$this->dispatch('success', 'Docker compose file loaded.');
$this->dispatch('compose_loaded');
} catch (\Throwable $e) {
$this->application->docker_compose_location = $this->initialDockerComposeLocation;
$this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation;
$this->application->save();
return handleError($e, $this);
} finally {
$this->initLoadingCompose = false;
}
}
public function generateDomain(string $serviceName)
{
$domain = $this->parsedServiceDomains[$serviceName]['domain'] ?? null;
if (!$domain) {
$uuid = new Cuid2(7);
$domain = generateFqdn($this->application->destination->server, $uuid);
$this->parsedServiceDomains[$serviceName]['domain'] = $domain;
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->application->save();
}
$uuid = new Cuid2(7);
$domain = generateFqdn($this->application->destination->server, $uuid);
$this->parsedServiceDomains[$serviceName]['domain'] = $domain;
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->application->save();
$this->dispatch('success', 'Domain generated.');
return $domain;
}
public function updatedApplicationBaseDirectory() {
raY('asdf');
public function updatedApplicationBaseDirectory()
{
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
}
public function updatedApplicationFqdn() {
$this->resetDefaultLabels();
}
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
@@ -206,30 +214,49 @@ class General extends Component
$fqdn = generateFqdn($server, $this->application->uuid);
$this->application->fqdn = $fqdn;
$this->application->save();
$this->updatedApplicationFqdn();
$this->resetDefaultLabels();
$this->dispatch('success', 'Wildcard domain generated.');
}
}
public function resetDefaultLabels($showToaster = true)
public function resetDefaultLabels()
{
ray('resetDefaultLabels');
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->ports_exposes = $this->application->ports_exposes;
$this->submit($showToaster);
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
}
public function updatedApplicationFqdn()
public function checkFqdns($showToaster = true)
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
$this->resetDefaultLabels(false);
if (data_get($this->application, 'fqdn')) {
$domains = str($this->application->fqdn)->trim()->explode(',');
if ($this->application->additional_servers->count() === 0) {
foreach ($domains as $domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
}
}
check_domain_usage(resource: $this->application);
$this->application->fqdn = $domains->implode(',');
}
}
public function submit($showToaster = true)
{
try {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->checkFqdns();
$this->application->save();
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->application->custom_labels = base64_encode($this->customLabels);
@@ -241,25 +268,14 @@ class General extends Component
}
$this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false);
$this->resetDefaultLabels();
}
if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([
'application.docker_registry_image_name' => 'required',
]);
}
if (data_get($this->application, 'fqdn')) {
$domains = str($this->application->fqdn)->trim()->explode(',');
if ($this->application->additional_servers->count() === 0) {
foreach ($domains as $domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
}
}
check_fqdn_usage($this->application);
$this->application->fqdn = $domains->implode(',');
}
if (data_get($this->application, 'custom_docker_run_options')) {
$this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim();
}
@@ -277,6 +293,15 @@ class General extends Component
}
if ($this->application->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
foreach ($this->parsedServiceDomains as $serviceName => $service) {
$domain = data_get($service, 'domain');
if ($domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
check_domain_usage(resource: $this->application);
}
}
if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->parseRawCompose();
} else {
@@ -289,7 +314,7 @@ class General extends Component
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
$this->dispatch('configurationChanged');
}
}
}

View File

@@ -22,6 +22,7 @@ class Heading extends Component
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'check_status',
"compose_loaded" => '$refresh',
];
}
public function mount()
@@ -38,6 +39,7 @@ class Heading extends Component
}
if ($showNotification) $this->dispatch('success', "Success", "Application status updated.");
$this->dispatch('configurationChanged');
}
public function force_deploy_without_cache()

View File

@@ -8,7 +8,8 @@ class Index extends Component
{
public $database;
public $s3s;
public function mount() {
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
@@ -21,8 +22,13 @@ class Index extends Component
if (!$database) {
return redirect()->route('dashboard');
}
// No backups for redis
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
// No backups
if (
$database->getMorphClass() === 'App\Models\StandaloneRedis' ||
$database->getMorphClass() === 'App\Models\StandaloneKeydb' ||
$database->getMorphClass() === 'App\Models\StandaloneDragonfly'||
$database->getMorphClass() === 'App\Models\StandaloneClickhouse'
) {
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Symfony\Component\HttpFoundation\StreamedResponse;
class BackupExecutions extends Component
{
@@ -36,32 +37,9 @@ class BackupExecutions extends Component
$this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions();
}
public function download($exeuctionId)
public function download_file($exeuctionId)
{
try {
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
if (is_null($execution)) {
$this->dispatch('error', 'Backup execution not found.');
return;
}
$filename = data_get($execution, 'filename');
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = $execution->scheduledDatabaseBackup->database->service->destination->server;
} else {
$server = $execution->scheduledDatabaseBackup->database->destination->server;
}
$privateKeyLocation = savePrivateKeyToFs($server);
$disk = Storage::build([
'driver' => 'sftp',
'host' => $server->ip,
'port' => $server->port,
'username' => $server->user,
'privateKey' => $privateKeyLocation,
]);
return $disk->download($filename);
} catch (\Throwable $e) {
return handleError($e, $this);
}
return redirect()->route('download.backup', $exeuctionId);
}
public function refreshBackupExecutions(): void
{

View File

@@ -0,0 +1,115 @@
<?php
namespace App\Livewire\Project\Database\Clickhouse;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneClickhouse;
use Exception;
use Livewire\Component;
class General extends Component
{
public StandaloneClickhouse $database;
public ?string $db_url = null;
public ?string $db_url_public = null;
protected $listeners = ['refresh'];
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.clickhouse_admin_user' => 'required',
'database.clickhouse_admin_password' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.clickhouse_admin_user' => 'Postgres User',
'database.clickhouse_admin_password' => 'Postgres Password',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function submit()
{
try {
if (str($this->database->public_port)->isEmpty()) {
$this->database->public_port = null;
}
$this->validate();
$this->database->save();
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
}

View File

@@ -7,7 +7,8 @@ use Livewire\Component;
class Configuration extends Component
{
public $database;
public function mount() {
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
@@ -21,6 +22,10 @@ class Configuration extends Component
return redirect()->route('dashboard');
}
$this->database = $database;
if (str($this->database->status)->startsWith('running') && is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
}
}
public function render()
{

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Livewire\Project\Database\Dragonfly;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneDragonfly;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneDragonfly $database;
public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.dragonfly_password' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.dragonfly_password' => 'Redis Password',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$this->validate();
$this->database->save();
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function render()
{
return view('livewire.project.database.dragonfly.general');
}
}

View File

@@ -2,13 +2,15 @@
namespace App\Livewire\Project\Database;
use App\Actions\Database\StartClickhouse;
use App\Actions\Database\StartDragonfly;
use App\Actions\Database\StartKeydb;
use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase;
use App\Events\DatabaseStatusChanged;
use App\Jobs\ContainerStatusJob;
use Livewire\Component;
@@ -32,6 +34,12 @@ class Heading extends Component
]);
$this->dispatch('refresh');
$this->check_status();
if (is_null($this->database->config_hash) || $this->database->isConfigurationChanged()) {
$this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
} else {
$this->dispatch('configurationChanged');
}
}
public function check_status($showNotification = false)
@@ -71,6 +79,15 @@ class Heading extends Component
} else if ($this->database->type() === 'standalone-mariadb') {
$activity = StartMariadb::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-keydb') {
$activity = StartKeydb::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-dragonfly') {
$activity = StartDragonfly::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-clickhouse') {
$activity = StartClickhouse::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
}
}
}

View File

@@ -2,29 +2,25 @@
namespace App\Livewire\Project\Database;
use Exception;
use Livewire\Component;
use Livewire\WithFileUploads;
use App\Models\Server;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Storage;
class Import extends Component
{
use WithFileUploads;
public $file;
public bool $unsupported = false;
public $resource;
public $parameters;
public $containers;
public bool $validated = true;
public bool $scpInProgress = false;
public bool $importRunning = false;
public string $validationMsg = '';
public ?string $filename = null;
public ?string $filesize = null;
public bool $isUploading = false;
public int $progress = 0;
public bool $error = false;
public Server $server;
public string $container;
public array $importCommands = [];
@@ -51,22 +47,9 @@ class Import extends Component
if (!data_get($this->parameters, 'database_uuid')) {
abort(404);
}
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
}
abort(404);
}
$this->resource = $resource;
$this->server = $this->resource->destination->server;
@@ -75,38 +58,34 @@ class Import extends Component
$this->containers->push($this->container);
}
if ($this->containers->count() > 1) {
$this->validated = false;
$this->validationMsg = 'The database service has more than one container running. Cannot import.';
}
if (
$this->resource->getMorphClass() == 'App\Models\StandaloneRedis'
|| $this->resource->getMorphClass() == 'App\Models\StandaloneMongodb'
$this->resource->getMorphClass() == 'App\Models\StandaloneRedis' ||
$this->resource->getMorphClass() == 'App\Models\StandaloneKeydb' ||
$this->resource->getMorphClass() == 'App\Models\StandaloneDragonfly' ||
$this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse' ||
$this->resource->getMorphClass() == 'App\Models\StandaloneMongodb'
) {
$this->validated = false;
$this->validationMsg = 'This database type is not currently supported.';
$this->unsupported = true;
}
}
public function runImport()
{
$this->validate([
'file' => 'required|file|max:102400'
]);
$this->importRunning = true;
$this->scpInProgress = true;
if ($this->filename == '') {
$this->dispatch('error', 'Please select a file to import.');
return;
}
try {
$uploadedFilename = $this->file->store('backup-import');
$uploadedFilename = "upload/{$this->resource->uuid}/restore";
$path = Storage::path($uploadedFilename);
if (!Storage::exists($uploadedFilename)) {
$this->dispatch('error', 'The file does not exist or has been deleted.');
return;
}
$tmpPath = '/tmp/' . basename($uploadedFilename);
// SCP the backup file to the server.
instant_scp($path, $tmpPath, $this->server);
$this->scpInProgress = false;
Storage::delete($uploadedFilename);
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
switch ($this->resource->getMorphClass()) {
@@ -132,8 +111,7 @@ class Import extends Component
$this->dispatch('activityMonitor', $activity->id);
}
} catch (\Throwable $e) {
$this->validated = false;
$this->validationMsg = $e->getMessage();
return handleError($e, $this);
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Livewire\Project\Database\Keydb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneKeydb;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneKeydb $database;
public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.keydb_conf' => 'nullable',
'database.keydb_password' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.keydb_conf' => 'Redis Configuration',
'database.keydb_password' => 'Redis Password',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$this->validate();
if ($this->database->keydb_conf === "") {
$this->database->keydb_conf = null;
}
$this->database->save();
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function render()
{
return view('livewire.project.database.keydb.general');
}
}

View File

@@ -76,6 +76,12 @@ class General extends Component
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
public function instantSave()

View File

@@ -78,6 +78,12 @@ class General extends Component
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
public function instantSave()

View File

@@ -77,6 +77,12 @@ class General extends Component
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
public function instantSave()

View File

@@ -58,7 +58,8 @@ class General extends Component
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced() {
public function instantSaveAdvanced()
{
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
@@ -164,6 +165,12 @@ class General extends Component
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
}

View File

@@ -120,6 +120,9 @@ class Select extends Component
case 'mysql':
case 'mariadb':
case 'redis':
case 'keydb':
case 'dragonfly':
case 'clickhouse':
case 'mongodb':
$this->isDatabase = true;
$this->includeSwarm = false;

View File

@@ -36,6 +36,12 @@ class Create extends Component
$database = create_standalone_mysql($environment->id, $destination_uuid);
} else if ($type->value() === 'mariadb') {
$database = create_standalone_mariadb($environment->id, $destination_uuid);
} else if ($type->value() === 'keydb') {
$database = create_standalone_keydb($environment->id, $destination_uuid);
} else if ($type->value() === 'dragonfly') {
$database = create_standalone_dragonfly($environment->id, $destination_uuid);
}else if ($type->value() === 'clickhouse') {
$database = create_standalone_clickhouse($environment->id, $destination_uuid);
}
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,

View File

@@ -16,6 +16,9 @@ class Index extends Component
public $mongodbs = [];
public $mysqls = [];
public $mariadbs = [];
public $keydbs = [];
public $dragonflies = [];
public $clickhouses = [];
public $services = [];
public function mount()
{
@@ -96,6 +99,39 @@ class Index extends Component
}
return $mariadb;
});
$this->keydbs = $this->environment->keydbs->load(['tags'])->sortBy('name');
$this->keydbs = $this->keydbs->map(function ($keydb) {
if (data_get($keydb, 'environment.project.uuid')) {
$keydb->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($keydb, 'environment.project.uuid'),
'environment_name' => data_get($keydb, 'environment.name'),
'database_uuid' => data_get($keydb, 'uuid')
]);
}
return $keydb;
});
$this->dragonflies = $this->environment->dragonflies->load(['tags'])->sortBy('name');
$this->dragonflies = $this->dragonflies->map(function ($dragonfly) {
if (data_get($dragonfly, 'environment.project.uuid')) {
$dragonfly->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($dragonfly, 'environment.project.uuid'),
'environment_name' => data_get($dragonfly, 'environment.name'),
'database_uuid' => data_get($dragonfly, 'uuid')
]);
}
return $dragonfly;
});
$this->clickhouses = $this->environment->clickhouses->load(['tags'])->sortBy('name');
$this->clickhouses = $this->clickhouses->map(function ($clickhouse) {
if (data_get($clickhouse, 'environment.project.uuid')) {
$clickhouse->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($clickhouse, 'environment.project.uuid'),
'environment_name' => data_get($clickhouse, 'environment.name'),
'database_uuid' => data_get($clickhouse, 'uuid')
]);
}
return $clickhouse;
});
$this->services = $this->environment->services->load(['tags'])->sortBy('name');
$this->services = $this->services->map(function ($service) {
if (data_get($service, 'environment.project.uuid')) {

View File

@@ -18,7 +18,8 @@ class Configuration extends Component
$userId = auth()->user()->id;
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
"check_status"
"check_status",
"refresh" => '$refresh',
];
}
public function render()
@@ -65,7 +66,7 @@ class Configuration extends Component
try {
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->dispatch('refresh')->self();
$this->dispatch('serviceStatusChanged');
$this->dispatch('updateStatus');
} catch (\Exception $e) {
return handleError($e, $this);
}

View File

@@ -18,7 +18,7 @@ class EditCompose extends Component
}
public function saveEditedCompose() {
$this->dispatch('warning', "Saving new docker compose...");
$this->dispatch('info', "Saving new docker compose...");
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
}
public function render()

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
class EditDomain extends Component
{
public $applicationId;
public ServiceApplication $application;
protected $rules = [
'application.fqdn' => 'nullable',
'application.required_fqdn' => 'required|boolean',
];
public function mount() {
$this->application = ServiceApplication::find($this->applicationId);
}
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
}
public function submit()
{
try {
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
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 {
$this->dispatch('success', 'Service saved.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('generateDockerCompose');
$this->dispatch('refresh');
$this->dispatch('configurationChanged');
}
}
public function render()
{
return view('livewire.project.service.edit-domain');
}
}

View File

@@ -5,13 +5,14 @@ namespace App\Livewire\Project\Service;
use App\Models\LocalFileVolume;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use Livewire\Component;
use Illuminate\Support\Str;
class FileStorage extends Component
{
public LocalFileVolume $fileStorage;
public ServiceApplication|ServiceDatabase $service;
public ServiceApplication|ServiceDatabase|StandaloneClickhouse $resource;
public string $fs_path;
public ?string $workdir = null;
@@ -23,14 +24,14 @@ class FileStorage extends Component
];
public function mount()
{
$this->service = $this->fileStorage->service;
if (Str::of($this->fileStorage->fs_path)->startsWith('.')) {
$this->workdir = $this->service->service->workdir();
$this->resource = $this->fileStorage->service;
if (Str::of($this->fileStorage->fs_path)->startsWith('.')) {
$this->workdir = $this->resource->service->workdir();
$this->fs_path = Str::of($this->fileStorage->fs_path)->after('.');
} else {
} else {
$this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path;
}
}
}
public function submit()
{

View File

@@ -10,7 +10,7 @@ use Livewire\Component;
class Index extends Component
{
public Service $service;
public ?Service $service = null;
public ?ServiceApplication $serviceApplication = null;
public ?ServiceDatabase $serviceDatabase = null;
public array $parameters;
@@ -26,7 +26,10 @@ class Index extends Component
$this->services = collect([]);
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->first();
if (!$this->service) {
return redirect()->route('dashboard');
}
$service = $this->service->applications()->whereUuid($this->parameters['stack_service_uuid'])->first();
if ($service) {
$this->serviceApplication = $service;

View File

@@ -17,21 +17,33 @@ class Navbar extends Component
public array $query;
public $isDeploymentProgress = false;
public function mount()
{
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
ray('isConfigurationChanged init');
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
}
}
public function getListeners()
{
$userId = auth()->user()->id;
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted',
"serviceStatusChanged"
"updateStatus"=> '$refresh',
];
}
public function serviceStarted() {
$this->dispatch('success', 'Service status changed.');
}
public function serviceStatusChanged()
public function serviceStarted()
{
$this->dispatch('refresh')->self();
$this->dispatch('success', 'Service status changed.');
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
} else {
$this->dispatch('configurationChanged');
}
}
public function check_status()
{
$this->dispatch('check_status');

View File

@@ -24,6 +24,16 @@ class ServiceApplicationView extends Component
{
return view('livewire.project.service.service-application-view');
}
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
}
public function instantSave()
{
$this->submit();
@@ -55,12 +65,12 @@ class ServiceApplicationView extends Component
public function submit()
{
try {
check_fqdn_usage($this->application);
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
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.');
$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 {
$this->dispatch('success', 'Service saved.');
}

View File

@@ -69,6 +69,13 @@ class StackForm extends Component
$this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
if (is_null($this->service->config_hash)) {
ray('asdf');
$this->service->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
public function render()

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Livewire\Component;
class ConfigurationChecker extends Component
{
public bool $isConfigurationChanged = false;
public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
protected $listeners = ['configurationChanged'];
public function mount()
{
$this->configurationChanged();
}
public function render()
{
return view('livewire.project.shared.configuration-checker');
}
public function configurationChanged()
{
$this->isConfigurationChanged = $this->resource->isConfigurationChanged();
}
}

View File

@@ -11,6 +11,7 @@ class Danger extends Component
public $resource;
public $projectUuid;
public $environmentName;
public bool $delete_configurations = true;
public ?string $modalId = null;
public function mount()
@@ -20,12 +21,11 @@ class Danger extends Component
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name');
}
public function delete()
{
try {
$this->resource->delete();
DeleteResourceJob::dispatch($this->resource);
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations);
return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName

View File

@@ -119,6 +119,15 @@ class All extends Component
case 'standalone-mariadb':
$environment->standalone_mariadb_id = $this->resource->id;
break;
case 'standalone-keydb':
$environment->standalone_keydb_id = $this->resource->id;
break;
case 'standalone-dragonfly':
$environment->standalone_dragonfly_id = $this->resource->id;
break;
case 'standalone-clickhouse':
$environment->standalone_clickhouse_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;
@@ -173,6 +182,15 @@ class All extends Component
case 'standalone-mariadb':
$environment->standalone_mariadb_id = $this->resource->id;
break;
case 'standalone-keydb':
$environment->standalone_keydb_id = $this->resource->id;
break;
case 'standalone-dragonfly':
$environment->standalone_dragonfly_id = $this->resource->id;
break;
case 'standalone-clickhouse':
$environment->standalone_clickhouse_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;

View File

@@ -5,11 +5,6 @@ namespace App\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Collection;
use Livewire\Component;
@@ -50,21 +45,9 @@ class ExecuteContainerCommand extends Component
}
} else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database';
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(),'id'));
if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
}
abort(404);
}
$this->resource = $resource;
if ($this->resource->destination->server->isFunctional()) {
@@ -109,7 +92,7 @@ class ExecuteContainerCommand extends Component
];
$this->containers = $this->containers->push($payload);
}
}
}
}
if ($this->containers->count() > 0) {
if (data_get($this->parameters, 'application_uuid')) {

View File

@@ -7,6 +7,9 @@ use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -19,7 +22,7 @@ class GetLogs extends Component
{
public string $outputs = '';
public string $errors = '';
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|null $resource = null;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|null $resource = null;
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
public Server $server;
public ?string $container = null;

View File

@@ -5,6 +5,9 @@ namespace App\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -16,7 +19,7 @@ use Livewire\Component;
class Logs extends Component
{
public ?string $type = null;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
public Collection $servers;
public Collection $containers;
public $container = [];
@@ -67,21 +70,9 @@ class Logs extends Component
}
} else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database';
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
}
abort(404);
}
$this->resource = $resource;
$this->status = $this->resource->status;

View File

@@ -76,7 +76,10 @@ class ResourceOperations extends Component
$this->resource->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneMysql' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneMariadb' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneRedis'
$this->resource->getMorphClass() === 'App\Models\StandaloneRedis' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneKeydb' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneDragonfly' ||
$this->resource->getMorphClass() === 'App\Models\StandaloneClickhouse'
) {
$uuid = (string)new Cuid2(7);
$new_resource = $this->resource->replicate()->fill([

View File

@@ -2,7 +2,6 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume;
use Livewire\Component;
class All extends Component

View File

@@ -75,6 +75,7 @@ class Form extends Component
}
public function checkLocalhostConnection()
{
$this->submit();
$uptime = $this->server->validateConnection();
if ($uptime) {
$this->dispatch('success', 'Server is reachable.');

View File

@@ -41,35 +41,39 @@ class Backup extends Component
}
public function add_coolify_database()
{
$server = Server::find(0);
$out = instant_remote_process(['docker inspect coolify-db'], $server);
$envs = format_docker_envs_to_json($out);
$postgres_password = $envs['POSTGRES_PASSWORD'];
$postgres_user = $envs['POSTGRES_USER'];
$postgres_db = $envs['POSTGRES_DB'];
$this->database = StandalonePostgresql::create([
'id' => 0,
'name' => 'coolify-db',
'description' => 'Coolify database',
'postgres_user' => $postgres_user,
'postgres_password' => $postgres_password,
'postgres_db' => $postgres_db,
'status' => 'running',
'destination_type' => 'App\Models\StandaloneDocker',
'destination_id' => 0,
]);
$this->backup = ScheduledDatabaseBackup::create([
'id' => 0,
'enabled' => true,
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $this->database->id,
'database_type' => 'App\Models\StandalonePostgresql',
'team_id' => currentTeam()->id,
]);
$this->database->refresh();
$this->backup->refresh();
$this->s3s = S3Storage::whereTeamId(0)->get();
try {
$server = Server::findOrFail(0);
$out = instant_remote_process(['docker inspect coolify-db'], $server);
$envs = format_docker_envs_to_json($out);
$postgres_password = $envs['POSTGRES_PASSWORD'];
$postgres_user = $envs['POSTGRES_USER'];
$postgres_db = $envs['POSTGRES_DB'];
$this->database = StandalonePostgresql::create([
'id' => 0,
'name' => 'coolify-db',
'description' => 'Coolify database',
'postgres_user' => $postgres_user,
'postgres_password' => $postgres_password,
'postgres_db' => $postgres_db,
'status' => 'running',
'destination_type' => 'App\Models\StandaloneDocker',
'destination_id' => 0,
]);
$this->backup = ScheduledDatabaseBackup::create([
'id' => 0,
'enabled' => true,
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $this->database->id,
'database_type' => 'App\Models\StandalonePostgresql',
'team_id' => currentTeam()->id,
]);
$this->database->refresh();
$this->backup->refresh();
$this->s3s = S3Storage::whereTeamId(0)->get();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function backup_now()

View File

@@ -59,23 +59,37 @@ class Configuration extends Component
public function submit()
{
$this->resetErrorBag();
if ($this->settings->public_port_min > $this->settings->public_port_max) {
$this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.');
return;
try {
$error_show = false;
$this->server = Server::findOrFail(0);
$this->resetErrorBag();
if ($this->settings->public_port_min > $this->settings->public_port_max) {
$this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.');
return;
}
$this->validate();
if ($this->settings->is_dns_validation_enabled) {
if (!validate_dns_entry($this->settings->fqdn, $this->server)) {
$this->dispatch('error', "Validating DNS ({$this->settings->fqdn}) failed.<br><br>Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$error_show = true;
}
}
check_domain_usage(domain: $this->settings->fqdn);
$this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim();
$this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) {
return str($dns)->trim()->lower();
});
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique();
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(',');
$this->settings->save();
$this->server->setupDynamicProxyConfiguration();
if (!$error_show) {
$this->dispatch('success', 'Instance settings updated successfully!');
}
} catch (\Exception $e) {
return handleError($e, $this);
}
$this->validate();
$this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim();
$this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) {
return str($dns)->trim()->lower();
});
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique();
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(',');
$this->settings->save();
$this->server = Server::findOrFail(0);
$this->server->setupDynamicProxyConfiguration();
$this->dispatch('success', 'Instance settings updated successfully!');
}
}

View File

@@ -15,6 +15,9 @@ class Index extends Component
if (!isCloud()) {
return redirect(RouteServiceProvider::HOME);
}
if (auth()->user()?->isMember()) {
return redirect()->route('dashboard');
}
if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) {
return redirect()->route('subscription.show');
}

View File

@@ -39,6 +39,12 @@ class PricingPlans extends Component
case 'ultimate-yearly':
$priceId = config('subscription.stripe_price_id_ultimate_yearly');
break;
case 'dynamic-monthly':
$priceId = config('subscription.stripe_price_id_dynamic_monthly');
break;
case 'dynamic-yearly':
$priceId = config('subscription.stripe_price_id_dynamic_yearly');
break;
default:
$priceId = config('subscription.stripe_price_id_basic_monthly');
break;
@@ -72,6 +78,13 @@ class PricingPlans extends Component
];
$payload['line_items'][0]['quantity'] = 10;
}
if (str($type)->contains('dynamic')) {
$payload['line_items'][0]['adjustable_quantity'] = [
'enabled' => true,
'minimum' => 2,
];
$payload['line_items'][0]['quantity'] = 2;
}
if (!data_get($team, 'subscription.stripe_trial_already_ended')) {
if (config('constants.limits.trial_period') > 0) {

View File

@@ -11,6 +11,9 @@ class Show extends Component
if (!isCloud()) {
return redirect()->route('dashboard');
}
if (auth()->user()?->isMember()) {
return redirect()->route('dashboard');
}
if (!data_get(currentTeam(), 'subscription')) {
return redirect()->route('subscription.index');
}

View File

@@ -32,10 +32,10 @@ class Upgrade extends Component
public function upgrade()
{
try {
$this->rateLimit(1, 30);
if ($this->showProgress) {
return;
}
$this->rateLimit(1, 30);
$this->showProgress = true;
UpdateCoolify::run(force: true, async: true);
$this->dispatch('success', "Updating Coolify to {$this->latestVersion} version...");

View File

@@ -57,6 +57,15 @@ class Application extends BaseModel
});
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
ray('Deleting workdir');
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
@@ -500,11 +509,11 @@ class Application extends BaseModel
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->ports_exposes . $this->ports_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels . $this->custom_docker_run_options . $this->dockerfile_target_build;
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables());
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
} else {
$newConfigHash .= json_encode($this->environment_variables_preview->all());
$newConfigHash .= json_encode($this->environment_variables_preview->get('updated_at'));
}
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
@@ -598,19 +607,27 @@ class Application extends BaseModel
} else {
$github_access_token = generate_github_installation_token($this->source);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}"));
$git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
} else {
$commands->push("{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}");
$git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
}
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
}
if ($pull_request_id !== 0) {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
$git_checkout_command = $this->buildGitCheckoutCommand($pr_branch_name);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name"));
$commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command"));
} else {
$commands->push("cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name");
$commands->push("cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command");
}
}
return [
@@ -654,7 +671,7 @@ class Application extends BaseModel
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
@@ -662,14 +679,14 @@ class Application extends BaseModel
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git checkout $commit";
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
}
}
@@ -697,7 +714,7 @@ class Application extends BaseModel
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
@@ -705,14 +722,14 @@ class Application extends BaseModel
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git checkout $commit";
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
}
}
@@ -904,6 +921,16 @@ class Application extends BaseModel
: explode(',', $this->fqdn),
);
}
protected function buildGitCheckoutCommand($target): string
{
$command = "git checkout $target";
if ($this->settings->is_git_submodules_enabled) {
$command .= " && git submodule update --init --recursive";
}
return $command;
}
public function watchPaths(): Attribute
{
return Attribute::make(

View File

@@ -46,7 +46,18 @@ class Environment extends Model
{
return $this->hasMany(StandaloneMariadb::class);
}
public function keydbs()
{
return $this->hasMany(StandaloneKeydb::class);
}
public function dragonflies()
{
return $this->hasMany(StandaloneDragonfly::class);
}
public function clickhouses()
{
return $this->hasMany(StandaloneClickhouse::class);
}
public function databases()
{
$postgresqls = $this->postgresqls;
@@ -54,7 +65,10 @@ class Environment extends Model
$mongodbs = $this->mongodbs;
$mysqls = $this->mysqls;
$mariadbs = $this->mariadbs;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
$keydbs = $this->keydbs;
$dragonflies = $this->dragonflies;
$clickhouses = $this->clickhouses;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
}
public function project()

View File

@@ -32,7 +32,7 @@ class EnvironmentVariable extends Model
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'is_multiline' => $environment_variable->is_multiline,
'is_multiline' => $environment_variable->is_multiline ?? false,
'application_id' => $environment_variable->application_id,
'is_preview' => true
]);
@@ -63,19 +63,7 @@ class EnvironmentVariable extends Model
} else if ($this->service_id) {
$resource = Service::find($this->service_id);
} else if ($this->database_id) {
$resource = StandalonePostgresql::find($this->database_id);
if (!$resource) {
$resource = StandaloneMysql::find($this->database_id);
if (!$resource) {
$resource = StandaloneRedis::find($this->database_id);
if (!$resource) {
$resource = StandaloneMongodb::find($this->database_id);
if (!$resource) {
$resource = StandaloneMariadb::find($this->database_id);
}
}
}
}
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
}
return $resource;
}

View File

@@ -22,8 +22,14 @@ class LocalFileVolume extends BaseModel
}
public function saveStorageOnServer()
{
$workdir = $this->resource->service->workdir();
$server = $this->resource->service->server;
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();
$server = $this->resource->service->server;
} else {
$workdir = $this->resource->workdir();
$server = $this->resource->destination->server;
}
$commands = collect([
"mkdir -p $workdir > /dev/null 2>&1 || true",
"cd $workdir"
@@ -55,8 +61,18 @@ class LocalFileVolume extends BaseModel
if (!$fileVolume->is_directory && $isDir == 'NOK') {
if ($content) {
$content = base64_encode($content);
$chmod = $fileVolume->chmod;
$chown = $fileVolume->chown;
ray($content, $path, $chmod, $chown);
$commands->push("echo '$content' | base64 -d > $path");
$commands->push("chmod +x $path");
if ($chown) {
$commands->push("chown $chown $path");
}
if ($chmod) {
$commands->push("chmod $chmod $path");
}
}
} else if ($isDir == 'NOK' && $fileVolume->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");

View File

@@ -63,6 +63,18 @@ class Project extends BaseModel
{
return $this->hasManyThrough(StandaloneRedis::class, Environment::class);
}
public function keydbs()
{
return $this->hasManyThrough(StandaloneKeydb::class, Environment::class);
}
public function dragonflies()
{
return $this->hasManyThrough(StandaloneDragonfly::class, Environment::class);
}
public function clickhouses()
{
return $this->hasManyThrough(StandaloneClickhouse::class, Environment::class);
}
public function mongodbs()
{
return $this->hasManyThrough(StandaloneMongodb::class, Environment::class);
@@ -77,6 +89,6 @@ class Project extends BaseModel
}
public function resource_count()
{
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count();
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count();
}
}

View File

@@ -550,21 +550,21 @@ $schema://$host {
}
public function loadUnmanagedContainers()
{
if ($this->isFunctional()) {
$containers = instant_remote_process(["docker ps -a --format '{{json .}}' "], $this);
$containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) {
$labels = data_get($container, 'Labels');
if (!str($labels)->contains("coolify.managed")) {
return $container;
}
return null;
});
$containers = $containers->filter();
return collect($containers);
} else {
return collect([]);
}
if ($this->isFunctional()) {
$containers = instant_remote_process(["docker ps -a --format '{{json .}}' "], $this);
$containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) {
$labels = data_get($container, 'Labels');
if (!str($labels)->contains("coolify.managed")) {
return $container;
}
return null;
});
$containers = $containers->filter();
return collect($containers);
} else {
return collect([]);
}
}
public function hasDefinedResources()
{
@@ -585,7 +585,10 @@ $schema://$host {
$mongodbs = data_get($standaloneDocker, 'mongodbs', collect([]));
$mysqls = data_get($standaloneDocker, 'mysqls', collect([]));
$mariadbs = data_get($standaloneDocker, 'mariadbs', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
$keydbs = data_get($standaloneDocker, 'keydbs', collect([]));
$dragonflies = data_get($standaloneDocker, 'dragonflies', collect([]));
$clickhouses = data_get($standaloneDocker, 'clickhouses', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
})->filter(function ($item) {
return data_get($item, 'name') !== 'coolify-db';
})->flatten();
@@ -690,7 +693,13 @@ $schema://$host {
}
public function isFunctional()
{
return $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled;
$isFunctional = $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled;
['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this);
if (!$isFunctional) {
Storage::disk('ssh-keys')->delete($private_key_filename);
Storage::disk('ssh-mux')->delete($mux_filename);
}
return $isFunctional;
}
public function isLogDrainEnabled()
{

View File

@@ -11,6 +11,46 @@ class Service extends BaseModel
{
use HasFactory, SoftDeletes;
protected $guarded = [];
public function isConfigurationChanged(bool $save = false)
{
$domains = $this->applications()->get()->pluck('fqdn')->toArray();
$domains = implode(',', $domains);
$applicationImages = $this->applications()->get()->pluck('image');
$databaseImages = $this->databases()->get()->pluck('image');
$images = $applicationImages->merge($databaseImages);
$images = implode(',', $images->toArray());
$applicationStorages = $this->applications()->get()->pluck('persistentStorages')->flatten();
$databaseStorages = $this->databases()->get()->pluck('persistentStorages')->flatten();
$storages = $applicationStorages->merge($databaseStorages)->implode('updated_at');
$newConfigHash = $images . $domains . $images . $storages;
$newConfigHash .= json_encode($this->environment_variables()->get('value'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status())->contains('exited');
}
public function type()
{
return 'service';
@@ -27,6 +67,14 @@ class Service extends BaseModel
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function delete_configurations()
{
$server = data_get($this, 'server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function status()
{
$applications = $this->applications;

View File

@@ -0,0 +1,210 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneClickhouse extends BaseModel
{
use HasFactory, SoftDeletes;
protected $guarded = [];
protected $casts = [
'clickhouse_password' => 'encrypted',
];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'clickhouse-data-' . $database->uuid,
'mount_path' => '/bitnami/clickhouse',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project()
{
return data_get($this, 'environment.project');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function type(): string
{
return 'standalone-clickhouse';
}
public function get_db_url(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
} else {
return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}";
}
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -32,6 +32,18 @@ class StandaloneDocker extends BaseModel
{
return $this->morphMany(StandaloneMariadb::class, 'destination');
}
public function keydbs()
{
return $this->morphMany(StandaloneKeydb::class, 'destination');
}
public function dragonflies()
{
return $this->morphMany(StandaloneDragonfly::class, 'destination');
}
public function clickhouses()
{
return $this->morphMany(StandaloneClickhouse::class, 'destination');
}
public function server()
{

View File

@@ -0,0 +1,210 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneDragonfly extends BaseModel
{
use HasFactory, SoftDeletes;
protected $guarded = [];
protected $casts = [
'dragonfly_password' => 'encrypted',
];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'dragonfly-data-' . $database->uuid,
'mount_path' => '/data',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project()
{
return data_get($this, 'environment.project');
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function type(): string
{
return 'standalone-dragonfly';
}
public function get_db_url(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "redis://{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://{$this->dragonfly_password}@{$this->uuid}:6379/0";
}
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -0,0 +1,212 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneKeydb extends BaseModel
{
use HasFactory, SoftDeletes;
protected $guarded = [];
protected $casts = [
'keydb_password' => 'encrypted',
];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'keydb-data-' . $database->uuid,
'mount_path' => '/data',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->keydb_conf;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
}
public function status(): Attribute
{
return Attribute::make(
set: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
get: function ($value) {
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
} else if (str($value)->contains(':')) {
$status = str($value)->before(':')->trim()->value();
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
} else {
$status = $value;
$health = 'unhealthy';
}
return "$status:$health";
},
);
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project()
{
return data_get($this, 'environment.project');
}
public function team()
{
return data_get($this, 'environment.project.team');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function type(): string
{
return 'standalone-keydb';
}
public function get_db_url(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://{$this->keydb_password}@{$this->uuid}:6379/0";
}
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -43,6 +43,45 @@ class StandaloneMariadb extends BaseModel
$database->tags()->detach();
});
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->mariadb_conf;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');

View File

@@ -46,6 +46,45 @@ class StandaloneMongodb extends BaseModel
$database->tags()->detach();
});
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->mongo_conf;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');

View File

@@ -43,6 +43,45 @@ class StandaloneMysql extends BaseModel
$database->tags()->detach();
});
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->mysql_conf;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');

View File

@@ -43,6 +43,45 @@ class StandalonePostgresql extends BaseModel
$database->tags()->detach();
});
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->postgres_initdb_args . $this->postgres_host_auth_method;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function realStatus()
{
return $this->getRawOriginal('status');

View File

@@ -38,6 +38,45 @@ class StandaloneRedis extends BaseModel
$database->tags()->detach();
});
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image . $this->ports_mappings . $this->redis_conf;
$newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
}
public function workdir()
{
return database_configuration_dir() . "/{$this->uuid}";
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(["rm -rf " . $this->workdir()], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');

View File

@@ -20,6 +20,18 @@ class SwarmDocker extends BaseModel
{
return $this->morphMany(StandaloneRedis::class, 'destination');
}
public function keydbs()
{
return $this->morphMany(StandaloneKeydb::class, 'destination');
}
public function dragonflies()
{
return $this->morphMany(StandaloneDragonfly::class, 'destination');
}
public function clickhouses()
{
return $this->morphMany(StandaloneClickhouse::class, 'destination');
}
public function mongodbs()
{
return $this->morphMany(StandaloneMongodb::class, 'destination');
@@ -50,7 +62,10 @@ class SwarmDocker extends BaseModel
$mongodbs = $this->mongodbs;
$mysqls = $this->mysqls;
$mariadbs = $this->mariadbs;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
$keydbs = $this->keydbs;
$dragonflies = $this->dragonflies;
$clickhouses = $this->clickhouses;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
}
public function attachedTo()

View File

@@ -21,9 +21,11 @@ class Team extends Model implements SendsDiscord, SendsEmail
protected static function booted()
{
// static::saved(function () {
// refreshSession();
// });
static::saving(function ($team) {
if (auth()->user()?->isMember()) {
throw new \Exception('You are not allowed to update this team.');
}
});
}
public function routeNotificationForDiscord()

View File

@@ -127,6 +127,10 @@ class User extends Authenticatable implements SendsEmail
{
return $this->role() === 'owner';
}
public function isMember()
{
return $this->role() === 'member';
}
public function isAdminFromSession()
{
if (auth()->user()->id === 0) {

View File

@@ -2,6 +2,7 @@
namespace App\Notifications\Server;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use App\Notifications\Channels\DiscordChannel;
@@ -21,6 +22,7 @@ class Revived extends Notification implements ShouldQueue
if ($this->server->unreachable_notification_sent === false) {
return;
}
dispatch(new ContainerStatusJob($server));
}
public function via(object $notifiable): array

View File

@@ -22,6 +22,7 @@ class Textarea extends Component
public bool $required = false,
public bool $disabled = false,
public bool $readonly = false,
public bool $allowTab = false,
public ?string $helper = null,
public bool $realtimeValidation = false,
public bool $allowToPeak = true,

View File

@@ -1,7 +1,7 @@
<?php
const REDACTED = '<REDACTED>';
const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb'];
const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb', 'keydb', 'dragonfly', 'clickhouse'];
const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *',
'hourly' => '0 * * * *',
@@ -37,7 +37,7 @@ const SPECIFIC_SERVICES = [
// Based on /etc/os-release
const SUPPORTED_OS = [
'ubuntu debian raspbian',
'centos fedora rhel ol rocky',
'centos fedora rhel ol rocky amzn',
'sles opensuse-leap opensuse-tumbleweed'
];

View File

@@ -1,7 +1,10 @@
<?php
use App\Models\Server;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -90,6 +93,49 @@ function create_standalone_mariadb($environment_id, $destination_uuid): Standalo
'destination_type' => $destination->getMorphClass(),
]);
}
function create_standalone_keydb($environment_id, $destination_uuid): StandaloneKeydb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneKeydb::create([
'name' => generate_database_name('keydb'),
'keydb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
function create_standalone_dragonfly($environment_id, $destination_uuid): StandaloneDragonfly
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneDragonfly::create([
'name' => generate_database_name('dragonfly'),
'dragonfly_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
function create_standalone_clickhouse($environment_id, $destination_uuid): StandaloneClickhouse
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneClickhouse::create([
'name' => generate_database_name('clickhouse'),
'clickhouse_admin_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
/**
* Delete file locally on the filesystem.

View File

@@ -582,7 +582,7 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
function escapeEnvVariables($value)
{
$search = array("\\", "\r", "\t", "\x0", '"', "'", "$");
$replace = array("\\\\", "\\r", "\\t", "\\0", '\"', "\'", "$$");
$search = array("\\", "\r", "\t", "\x0", '"', "'");
$replace = array("\\\\", "\\r", "\\t", "\\0", '\"', "\'");
return str_replace($search, $replace, $value);
}

View File

@@ -29,7 +29,7 @@ function generate_github_installation_token(GithubApp $source)
'Accept' => 'application/vnd.github.machine-man-preview+json'
])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
if ($token->failed()) {
throw new RuntimeException("Failed to get access token for " . $source->name . " with error: " . $token->json()['message']);
throw new RuntimeException("Failed to get access token for " . $source->name . " with error: " . data_get($token->json(),'message','no error message found'));
}
return $token->json()['token'];
}

View File

@@ -7,11 +7,10 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
@@ -55,17 +54,30 @@ function remote_process(
),
])();
}
function server_ssh_configuration(Server $server)
{
$uuid = data_get($server, 'uuid');
if (is_null($uuid)) {
throw new \Exception("Server does not have a uuid");
}
$private_key_filename = "id.root@{$server->uuid}";
$location = '/var/www/html/storage/app/ssh/keys/' . $private_key_filename;
$mux_filename = '/var/www/html/storage/app/ssh/mux/' . $server->muxFilename();
return [
'location' => $location,
'mux_filename' => $mux_filename,
'private_key_filename' => $private_key_filename
];
}
function savePrivateKeyToFs(Server $server)
{
if (data_get($server, 'privateKey.private_key') === null) {
throw new \Exception("Server {$server->name} does not have a private key");
}
$sshKeyFileLocation = "id.root@{$server->uuid}";
['location' => $location, 'private_key_filename' => $private_key_filename] = server_ssh_configuration($server);
Storage::disk('ssh-keys')->makeDirectory('.');
Storage::disk('ssh-mux')->makeDirectory('.');
Storage::disk('ssh-keys')->put($sshKeyFileLocation, $server->privateKey->private_key);
$location = '/var/www/html/storage/app/ssh/keys/' . $sshKeyFileLocation;
Storage::disk('ssh-keys')->put($private_key_filename, $server->privateKey->private_key);
return $location;
}
@@ -122,7 +134,6 @@ function generateSshCommand(Server $server, string $command)
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$delimiter = 'EOF-COOLIFY-SSH';
$ssh_command = "timeout $timeout ssh ";
if (config('coolify.mux_enabled') && config('coolify.is_windows_docker_desktop') == false) {
@@ -132,6 +143,9 @@ function generateSshCommand(Server $server, string $command)
$ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command);
$command = str_replace($delimiter, '', $command);
$ssh_command .= "-i {$privateKeyLocation} "
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
. '-o PasswordAuthentication=no '
@@ -145,6 +159,7 @@ function generateSshCommand(Server $server, string $command)
. $command . PHP_EOL
. $delimiter;
// ray($ssh_command);
// ray($delimiter);
return $ssh_command;
}
function instant_remote_process(Collection|array $command, Server $server, $throwError = true)
@@ -223,6 +238,13 @@ function remove_iip($text)
$text = preg_replace('/x-access-token:.*?(?=@)/', "x-access-token:" . REDACTED, $text);
return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
}
function remove_mux_and_private_key(Server $server)
{
$muxFilename = $server->muxFilename();
$privateKeyLocation = savePrivateKeyToFs($server);
Storage::disk('ssh-mux')->delete($muxFilename);
Storage::disk('ssh-keys')->delete($privateKeyLocation);
}
function refresh_server_connection(?PrivateKey $private_key = null)
{
if (is_null($private_key)) {

View File

@@ -88,33 +88,106 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
$dockerCompose = Yaml::parse($dockerComposeRaw);
// Switch Image
$image = data_get($resource, 'image');
data_set($dockerCompose, "services.{$name}.image", $image);
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
$resource->service->docker_compose_raw = $dockerComposeRaw;
$resource->service->save();
if ($resource->fqdn && !str($resource->fqdn)->contains(',')) {
// Update FQDN
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$fqdn = Url::fromString($resource->fqdn);
$fqdn = $fqdn->getScheme() . '://' . $fqdn->getHost();
if ($generatedEnv) {
$generatedEnv->value = $fqdn;
$generatedEnv->save();
}
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper();
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($resource->fqdn);
$url = $url->getHost();
if ($generatedEnv) {
$url = Str::of($resource->fqdn)->after('://');
$generatedEnv->value = $url;
$generatedEnv->save();
$updatedImage = data_get_str($resource, 'image');
$currentImage = data_get_str($dockerCompose, "services.{$name}.image");
if ($currentImage !== $updatedImage) {
data_set($dockerCompose, "services.{$name}.image", $updatedImage->value());
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
$resource->service->docker_compose_raw = $dockerComposeRaw;
$resource->service->save();
$resource->image = $updatedImage;
$resource->save();
}
if ($resource->fqdn) {
$resourceFqdns = str($resource->fqdn)->explode(',');
if ($resourceFqdns->count() === 1) {
$resourceFqdns = $resourceFqdns->first();
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$fqdn = Url::fromString($resourceFqdns);
$port = $fqdn->getPort();
$fqdn = $fqdn->getScheme() . '://' . $fqdn->getHost();
if ($generatedEnv) {
$generatedEnv->value = $fqdn;
$generatedEnv->save();
}
if ($port) {
$variableName = $variableName . "_$port";
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
if ($generatedEnv) {
$generatedEnv->value = $fqdn . ':' . $port;
$generatedEnv->save();
}
}
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($fqdn);
$port = $url->getPort();
$url = $url->getHost();
if ($generatedEnv) {
$url = Str::of($fqdn)->after('://');
$generatedEnv->value = $url;
$generatedEnv->save();
}
if ($port) {
$variableName = $variableName . "_$port";
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
if ($generatedEnv) {
$generatedEnv->value = $url . ':' . $port;
$generatedEnv->save();
}
}
} else if ($resourceFqdns->count() > 1) {
foreach ($resourceFqdns as $fqdn) {
$host = Url::fromString($fqdn);
$port = $host->getPort();
$url = $host->getHost();
$host = $host->getScheme() . '://' . $host->getHost();
if ($port) {
$port_envs = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'like', "SERVICE_FQDN_%_$port")->get();
foreach ($port_envs as $port_env) {
$service_fqdn = str($port_env->key)->beforeLast('_')->after('SERVICE_FQDN_');
$env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_FQDN_' . $service_fqdn)->first();
if ($env) {
$env->value = $host;
$env->save();
}
$port_env->value = $host . ':' . $port;
$port_env->save();
}
$port_envs_url = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'like', "SERVICE_URL_%_$port")->get();
foreach ($port_envs_url as $port_env_url) {
$service_url = str($port_env_url->key)->beforeLast('_')->after('SERVICE_URL_');
$env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_URL_' . $service_url)->first();
if ($env) {
$env->value = $url;
$env->save();
}
$port_env_url->value = $url . ':' . $port;
$port_env_url->save();
}
} else {
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$fqdn = Url::fromString($fqdn);
$fqdn = $fqdn->getScheme() . '://' . $fqdn->getHost();
if ($generatedEnv) {
$generatedEnv->value = $fqdn;
$generatedEnv->save();
}
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($fqdn);
$url = $url->getHost();
if ($generatedEnv) {
$url = Str::of($fqdn)->after('://');
$generatedEnv->value = $url;
$generatedEnv->save();
}
}
}
}
}
} catch (\Throwable $e) {
return handleError($e);
}

View File

@@ -11,6 +11,9 @@ use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -443,7 +446,7 @@ function sslip(Server $server)
function getServiceTemplates()
{
if (!isDev()) {
if (isDev()) {
$services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys();
} else {
@@ -463,15 +466,14 @@ function getServiceTemplates()
function getResourceByUuid(string $uuid, ?int $teamId = null)
{
$resource = queryResourcesByUuid($uuid);
if (!is_null($teamId)) {
if (!is_null($resource) && $resource->environment->project->team_id === $teamId) {
return $resource;
}
if (is_null($teamId)) {
return null;
} else {
}
$resource = queryResourcesByUuid($uuid);
if (!is_null($resource) && $resource->environment->project->team_id === $teamId) {
return $resource;
}
return null;
}
function queryResourcesByUuid(string $uuid)
{
@@ -490,6 +492,12 @@ function queryResourcesByUuid(string $uuid)
if ($mysql) return $mysql;
$mariadb = StandaloneMariadb::whereUuid($uuid)->first();
if ($mariadb) return $mariadb;
$keydb = StandaloneKeydb::whereUuid($uuid)->first();
if ($keydb) return $keydb;
$dragonfly = StandaloneDragonfly::whereUuid($uuid)->first();
if ($dragonfly) return $dragonfly;
$clickhouse = StandaloneClickhouse::whereUuid($uuid)->first();
if ($clickhouse) return $clickhouse;
return $resource;
}
function generatTagDeployWebhook($tag_name)
@@ -1168,6 +1176,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// });
// ray($withoutServiceEnvs);
// data_set($service, 'environment', $withoutServiceEnvs->toArray());
updateCompose($savedService);
return $service;
});
$finalServices = [
@@ -1856,13 +1865,26 @@ function ip_match($ip, $cidrs, &$match = null)
}
return false;
}
function check_fqdn_usage(ServiceApplication|Application $own_resource)
function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
{
$domains = collect($own_resource->fqdns)->map(function ($domain) {
if ($resource) {
if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') {
$domains = data_get(json_decode($resource->docker_compose_domains, true), "*.domain");
ray($domains);
$domains = collect($domains);
} else {
$domains = collect($resource->fqdns);
}
} else if ($domain) {
$domains = collect($domain);
} else {
throw new \RuntimeException("No resource or FQDN provided.");
}
$domains = $domains->map(function ($domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
return str($domain)->replace('http://', '')->replace('https://', '');
return str($domain);
});
$apps = Application::all();
foreach ($apps as $app) {
@@ -1871,10 +1893,15 @@ function check_fqdn_usage(ServiceApplication|Application $own_resource)
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value();
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource:<br> {$app->name}.");
if (data_get($resource, 'uuid')) {
ray($resource->uuid, $app->uuid);
if ($resource->uuid !== $app->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
}
} else if ($domain) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
}
}
}
@@ -1886,12 +1913,29 @@ function check_fqdn_usage(ServiceApplication|Application $own_resource)
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value();
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource.");
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
}
} else if ($domain) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
}
}
}
}
if ($resource) {
$settings = InstanceSettings::get();
if (data_get($settings, 'fqdn')) {
$domain = data_get($settings, 'fqdn');
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance.");
}
}
}
}

View File

@@ -27,6 +27,7 @@
"lorisleiva/laravel-actions": "^2.7",
"nubs/random-name-generator": "^2.2",
"phpseclib/phpseclib": "~3.0",
"pion/laravel-chunk-upload": "^1.5",
"poliander/cron": "^3.0",
"purplepixie/phpdns": "^2.1",
"pusher/pusher-php-server": "^7.2",

68
composer.lock generated
View File

@@ -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": "e095b8a9eb22df2943cbc3e9649ff9e8",
"content-hash": "e6fd1d5c5183226a78df717b52343393",
"packages": [
{
"name": "amphp/amp",
@@ -6370,6 +6370,72 @@
},
"time": "2021-10-28T11:13:42+00:00"
},
{
"name": "pion/laravel-chunk-upload",
"version": "v1.5.4",
"source": {
"type": "git",
"url": "https://github.com/pionl/laravel-chunk-upload.git",
"reference": "cfbc4292ddcace51308a4f2f446d310aa04e6133"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pionl/laravel-chunk-upload/zipball/cfbc4292ddcace51308a4f2f446d310aa04e6133",
"reference": "cfbc4292ddcace51308a4f2f446d310aa04e6133",
"shasum": ""
},
"require": {
"illuminate/console": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
"illuminate/filesystem": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
"illuminate/http": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
"illuminate/support": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16.0 | ^3.52.0",
"mockery/mockery": "^1.1.0 | ^1.3.0 | ^1.6.0",
"overtrue/phplint": "^1.1 | ^2.0 | ^9.1",
"phpunit/phpunit": "5.7 | 6.0 | 7.0 | 7.5 | 8.4 | ^8.5 | ^9.3 | ^10.0 | ^11.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Pion\\Laravel\\ChunkUpload\\Providers\\ChunkUploadServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Pion\\Laravel\\ChunkUpload\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Martin Kluska",
"email": "martin@kluska.cz"
}
],
"description": "Service for chunked upload with several js providers",
"support": {
"issues": "https://github.com/pionl/laravel-chunk-upload/issues",
"source": "https://github.com/pionl/laravel-chunk-upload/tree/v1.5.4"
},
"funding": [
{
"url": "https://revolut.me/martinpv7n",
"type": "custom"
},
{
"url": "https://github.com/pionl",
"type": "github"
}
],
"time": "2024-03-25T15:50:07+00:00"
},
{
"name": "poliander/cron",
"version": "3.1.0",

44
config/chunk-upload.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
/**
* @see https://github.com/pionl/laravel-chunk-upload
*/
return [
/*
* The storage config
*/
'storage' => [
/*
* Returns the folder name of the chunks. The location is in storage/app/{folder_name}
*/
'chunks' => 'chunks',
'disk' => 'local',
],
'clear' => [
/*
* How old chunks we should delete
*/
'timestamp' => '-1 HOURS',
'schedule' => [
'enabled' => false,
'cron' => '25 * * * *', // run every hour on the 25th minute
],
],
'chunk' => [
// setup for the chunk naming setup to ensure same name upload at same time
'name' => [
'use' => [
'session' => true, // should the chunk name use the session id? The uploader must send cookie!,
'browser' => false, // instead of session we can use the ip and browser?
],
],
],
'handlers' => [
// A list of handlers/providers that will be appended to existing list of handlers
'custom' => [],
// Overrides the list of handlers - use only what you really want
'override' => [
// \Pion\Laravel\ChunkUpload\Handler\DropZoneUploadHandler::class
],
],
];

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.249',
'release' => '4.0.0-beta.258',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -20,6 +20,9 @@ return [
'stripe_price_id_ultimate_monthly_old' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD', null),
'stripe_price_id_ultimate_yearly_old' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD', null),
'stripe_price_id_dynamic_monthly' => env('STRIPE_PRICE_ID_DYNAMIC_MONTHLY', null),
'stripe_price_id_dynamic_yearly' => env('STRIPE_PRICE_ID_DYNAMIC_YEARLY', null),
// Paddle
'paddle_vendor_id' => env('PADDLE_VENDOR_ID', null),
'paddle_vendor_auth_code' => env('PADDLE_VENDOR_AUTH_CODE', null),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.249';
return '4.0.0-beta.258';

View File

@@ -0,0 +1,29 @@
<?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('applications', function (Blueprint $table) {
$table->text('custom_docker_run_options')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('custom_docker_run_options')->nullable()->change();
});
}
};

View File

@@ -0,0 +1,64 @@
<?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::create('standalone_keydbs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->text('keydb_password');
$table->longText('keydb_conf')->nullable();
$table->boolean('is_log_drain_enabled')->default(false);
$table->boolean('is_include_timestamps')->default(false);
$table->softDeletes();
$table->string('status')->default('exited');
$table->string('image')->default('eqalpha/keydb:latest');
$table->boolean('is_public')->default(false);
$table->integer('public_port')->nullable();
$table->text('ports_mappings')->nullable();
$table->string('limits_memory')->default("0");
$table->string('limits_memory_swap')->default("0");
$table->integer('limits_memory_swappiness')->default(60);
$table->string('limits_memory_reservation')->default("0");
$table->string('limits_cpus')->default("0");
$table->string('limits_cpuset')->nullable()->default(null);
$table->integer('limits_cpu_shares')->default(1024);
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id')->nullable();
$table->timestamps();
});
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('standalone_keydb_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('standalone_keydbs');
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('standalone_keydb_id');
});
}
};

View File

@@ -0,0 +1,63 @@
<?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::create('standalone_dragonflies', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->text('dragonfly_password');
$table->boolean('is_log_drain_enabled')->default(false);
$table->boolean('is_include_timestamps')->default(false);
$table->softDeletes();
$table->string('status')->default('exited');
$table->string('image')->default('docker.dragonflydb.io/dragonflydb/dragonfly');
$table->boolean('is_public')->default(false);
$table->integer('public_port')->nullable();
$table->text('ports_mappings')->nullable();
$table->string('limits_memory')->default("0");
$table->string('limits_memory_swap')->default("0");
$table->integer('limits_memory_swappiness')->default(60);
$table->string('limits_memory_reservation')->default("0");
$table->string('limits_cpus')->default("0");
$table->string('limits_cpuset')->nullable()->default(null);
$table->integer('limits_cpu_shares')->default(1024);
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id')->nullable();
$table->timestamps();
});
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('standalone_dragonfly_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('standalone_dragonflies');
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('standalone_dragonfly_id');
});
}
};

Some files were not shown because too many files have changed in this diff Show More