Compare commits

...

99 Commits

Author SHA1 Message Date
Andras Bacsai
e260bfae02 Merge pull request #1443 from coollabsio/next
v4.0.0-beta.128
2023-11-13 15:49:51 +01:00
Andras Bacsai
5abd4a6d78 Update version and fix MINIO_BROWSER_REDIRECT_URL
and MINIO_SERVER_URL
2023-11-13 15:49:23 +01:00
Andras Bacsai
9dff1e5631 Merge pull request #1442 from coollabsio/next
v4.0.0-beta.127
2023-11-13 15:42:28 +01:00
Andras Bacsai
02332ade1b Fix URLs and remove unnecessary command in
ApplicationDeploymentJob.php
2023-11-13 15:41:49 +01:00
Andras Bacsai
486de58d5b Update database start commands 2023-11-13 15:27:33 +01:00
Andras Bacsai
606aeb2b61 Merge pull request #1441 from coollabsio/next
v4.0.0-beta.126
2023-11-13 15:21:45 +01:00
Andras Bacsai
3fc264560c Update dependencies and fix minor bugs. 2023-11-13 15:19:49 +01:00
Andras Bacsai
3dd9182281 Add sponsorship notification and disable option,
update dependencies
2023-11-13 14:44:54 +01:00
Andras Bacsai
c838ff7198 Update version numbers to 4.0.0-beta.126 2023-11-13 13:38:50 +01:00
Andras Bacsai
ca6db9c1a9 Merge pull request #1440 from coollabsio/next
v4.0.0-beta.125
2023-11-13 13:21:00 +01:00
Andras Bacsai
f27e00e80e Update version.json to include v4.0.0-beta.125 2023-11-13 13:20:28 +01:00
Andras Bacsai
60cf296f31 Update preview application deployment labels and version 2023-11-13 13:20:12 +01:00
Andras Bacsai
ea64e9d5ad Merge pull request #1439 from coollabsio/next
v4.0.0-beta.124
2023-11-13 13:03:46 +01:00
Andras Bacsai
55846c5635 Fix service retrieval and add error handling 2023-11-13 12:59:59 +01:00
Andras Bacsai
7763594e6e Add pull_latest_image function and update
build_image function to use it. Also add check for
dockerfile existence in start_by_compose_file
function.
2023-11-13 12:30:25 +01:00
Andras Bacsai
6b5339c1c1 Remove ray debug statement and refactor random
name generator
2023-11-13 11:44:13 +01:00
Andras Bacsai
f2980738e4 Fix documentation link in service-templates.json 2023-11-13 11:30:20 +01:00
Andras Bacsai
f0e3ad0461 Merge pull request #1432 from AlejandroAkbal/main
fix(fider template): use the correct docs url
2023-11-13 11:29:39 +01:00
Andras Bacsai
187050e098 Merge pull request #1435 from AshikNesin/main
Fix typo in onboarding page
2023-11-13 11:29:02 +01:00
Andras Bacsai
9e7823795d Fix null check for MINIO_BROWSER_REDIRECT_URL and
MINIO_SERVER_URL in generateServiceSpecificFqdns
function
2023-11-13 11:17:49 +01:00
Andras Bacsai
239459dfa8 Remove commented out code for minio service 2023-11-13 11:13:16 +01:00
Andras Bacsai
ce0f560c44 Add service-specific configuration fields and save
them to the database
2023-11-13 11:09:21 +01:00
Andras Bacsai
95baec99dd Fix typo in General.php component 2023-11-13 09:04:19 +01:00
Andras Bacsai
363e8fc0b5 Update code with bug fixes and improvements 2023-11-13 08:46:43 +01:00
Andras Bacsai
e49caba920 Add STRIPE_EXCLUDED_PLANS to services in
docker-compose.prod.yml
2023-11-13 08:46:17 +01:00
Ashik Nesin
30db2b2a09 Update typo in onboarding screen 2023-11-12 19:30:20 +00:00
Andras Bacsai
285666e181 Merge pull request #1434 from coollabsio/next
v4.0.0-beta.123
2023-11-12 19:11:31 +01:00
Andras Bacsai
003934ee1d disable service confs for now 2023-11-12 19:10:54 +01:00
Andras Bacsai
44c7958aa6 make fqdn super long 2023-11-12 19:09:38 +01:00
Alejandro Akbal
35b1a81dfe fix(fider template): use the correct docs url 2023-11-12 12:10:53 +00:00
Andras Bacsai
e40f397cc7 fix: service updates 2023-11-11 21:32:41 +01:00
Andras Bacsai
9fd8cd7e6c Merge pull request #1430 from coollabsio/next
v4.0.0-beta.122
2023-11-11 10:19:28 +01:00
Andras Bacsai
a94b7ee611 fix: container status jobs for old pr deployments 2023-11-11 10:18:40 +01:00
Andras Bacsai
fc68bf50b5 save 2023-11-10 22:04:04 +01:00
Andras Bacsai
0f99ee787c Merge pull request #1429 from coollabsio/next
v4.0.0-beta.121
2023-11-10 21:30:49 +01:00
Andras Bacsai
95777e978e fix: revert workdir to basedir 2023-11-10 21:02:39 +01:00
Andras Bacsai
fb0b9dbfed Add subscription exclusion for certain plans in
webhook handling
2023-11-10 15:41:44 +01:00
Andras Bacsai
9617000daa Add stripe_excluded_plans config variable and
handle excluded plans in webhook
2023-11-10 15:36:02 +01:00
Andras Bacsai
1818404172 Refactor application configuration blade file to
conditionally display tabs based on build pack
2023-11-10 13:46:14 +01:00
Andras Bacsai
d9a966fd98 Fix broken link to framework specific docs in
general.blade.php
2023-11-10 13:42:17 +01:00
Andras Bacsai
763ce5fc14 Update version numbers and deployment logs styling 2023-11-10 13:38:29 +01:00
Andras Bacsai
df021760a7 Merge pull request #1423 from coollabsio/next
v4.0.0-beta.120
2023-11-10 12:06:55 +01:00
Andras Bacsai
fb2598f2e4 Update UI elements and add new build pack option (static) 2023-11-10 11:33:15 +01:00
Andras Bacsai
7af07b2718 Add logging to DockerCleanupJob 2023-11-10 10:55:23 +01:00
Andras Bacsai
23a94c9378 Refactor DockerCleanupJob and Application model 2023-11-10 10:34:28 +01:00
Andras Bacsai
ed34fc9645 Update defaultClass in Select component 2023-11-10 10:14:46 +01:00
Andras Bacsai
cafd9e0ab2 Convert cpus limits to integer in database and
application classes
2023-11-10 09:54:40 +01:00
Andras Bacsai
e882477e21 Refactor navbar and add help us link 2023-11-10 09:49:47 +01:00
Andras Bacsai
db0e3cfcc4 fix: database proxy for services
version++
tiny css modifications
2023-11-10 09:41:42 +01:00
Andras Bacsai
b3c4429028 Merge pull request #1422 from coollabsio/next
v4.0.0-beta.119
2023-11-09 15:10:56 +01:00
Andras Bacsai
87ab4bd71e fix: local ip address 2023-11-09 15:05:42 +01:00
Andras Bacsai
61e1fdede9 feat: make service databases public 2023-11-09 14:59:38 +01:00
Andras Bacsai
b189919f97 Merge pull request #1421 from coollabsio/next
v4.0.0-beta.118
2023-11-09 12:47:05 +01:00
Andras Bacsai
8f5b084931 Refactor environment variable saving logic. 2023-11-09 12:40:53 +01:00
Andras Bacsai
eb96a5ae7b Update user authentication logic to use bcrypt
hashing algorithm
2023-11-09 12:29:03 +01:00
Andras Bacsai
f0fb9dbb94 Update Sentry and version configs to
4.0.0-beta.118
2023-11-09 12:19:08 +01:00
Andras Bacsai
cb2d4b4a0a Merge pull request #1420 from coollabsio/next
v4.0.0-beta.117
2023-11-09 11:59:01 +01:00
Andras Bacsai
3aace2d4f9 Update email recipient in SendEmail.php 2023-11-09 11:58:12 +01:00
Andras Bacsai
8c2ed75653 Update Docker images and add Directus service with
PostgreSQL.
2023-11-09 11:52:51 +01:00
Andras Bacsai
8c8aafbc65 Update version and fix directory path in
deployment job
2023-11-09 11:33:37 +01:00
Andras Bacsai
a2c39fd07e Merge pull request #1416 from coollabsio/next
v4.0.0-beta.116
2023-11-08 15:42:03 +01:00
Andras Bacsai
9698a051d9 Refactored code for better container management 2023-11-08 15:40:06 +01:00
Andras Bacsai
51423394ba Add deployment logs button to Telegram
notification
2023-11-08 14:37:01 +01:00
Andras Bacsai
8e5e36dd5b Update version numbers to 4.0.0-beta.116 and change docs link 2023-11-08 12:54:13 +01:00
Andras Bacsai
c1dd05dcd8 Merge pull request #1413 from coollabsio/next
Refactored database backup job to handle missing
2023-11-08 12:46:14 +01:00
Andras Bacsai
dd1ce6ee6c Refactor database backup job to simplify code 2023-11-08 12:45:48 +01:00
Andras Bacsai
fe4c6d396c Refactored database backup job to handle missing
POSTGRES_DB environment variable
2023-11-08 12:45:31 +01:00
Andras Bacsai
8db54ec069 Merge pull request #1412 from coollabsio/next
Fix database type check in service show blade file
2023-11-08 12:42:35 +01:00
Andras Bacsai
3abc720926 Fix database type check in service show blade file 2023-11-08 12:42:20 +01:00
Andras Bacsai
64cc0b63f1 Merge pull request #1411 from coollabsio/next
v4.0.0-beta.115
2023-11-08 12:41:25 +01:00
Andras Bacsai
c78068466b Add custom PostgreSQL configuration to
StandalonePostgresql
2023-11-08 12:40:05 +01:00
Andras Bacsai
88e407756d Update version numbers and database URLs 2023-11-08 12:26:57 +01:00
Andras Bacsai
1538116e6e Merge pull request #1410 from coollabsio/next
v4.0.0-beta.114
2023-11-08 11:31:40 +01:00
Andras Bacsai
aba47d58a4 Add customRepository property to
ApplicationDeploymentJob class
Fix weird image names in case of custom git
2023-11-08 11:30:54 +01:00
Andras Bacsai
e7f184dd82 Add conditional check for backups tab in service
show view
2023-11-08 11:07:44 +01:00
Andras Bacsai
2ad8d7812b Refactor database backup job to improve code
readability and maintainability.
2023-11-08 11:05:57 +01:00
Andras Bacsai
8212bb99a1 Update database backup job and version number 2023-11-08 10:47:39 +01:00
Andras Bacsai
5fc382d09d Merge pull request #1406 from coollabsio/next
v4.0.0-beta.113
2023-11-08 10:28:57 +01:00
Andras Bacsai
78a80c46da Add nixpacks environment variables to deployment
job
2023-11-08 10:13:20 +01:00
Andras Bacsai
c365d132af Fix empty public port in database configuration 2023-11-08 09:30:38 +01:00
Andras Bacsai
4dc3db3845 Update versions and fix database replication (init values are changeable) in
CloneProject.php
2023-11-08 09:07:30 +01:00
Andras Bacsai
b9427d2ec1 Merge pull request #1398 from coollabsio/next
v4.0.0-beta.112
2023-11-07 15:01:56 +01:00
Andras Bacsai
332a0b9e04 Remove ANSI colors from console output. 2023-11-07 14:40:58 +01:00
Andras Bacsai
18e98aaf52 Add S3 storage to Livewire components and fix
backup job network issue
2023-11-07 14:09:24 +01:00
Andras Bacsai
a7f9fad627 Add support for Dockerfile target build 2023-11-07 13:49:15 +01:00
Andras Bacsai
b01f6ac414 Fix docker network connection in StartService.php 2023-11-07 13:29:05 +01:00
Andras Bacsai
e1bc2cc406 Fix docker network connection issue in
StartService.php
2023-11-07 13:28:48 +01:00
Andras Bacsai
74830b12f3 Fix Docker network creation command in
StartService.php
2023-11-07 13:28:10 +01:00
Andras Bacsai
56a977c676 update n8n 2023-11-07 12:50:18 +01:00
Andras Bacsai
a0bb5733e6 lol n8n with umami db name 2023-11-07 12:30:37 +01:00
Andras Bacsai
516e10ddf2 feat: service database backups 2023-11-07 12:11:47 +01:00
Andras Bacsai
2976c72e09 fix: ui 2023-11-07 10:18:28 +01:00
Andras Bacsai
7377e9e415 fix: dockercleanupjob should be released back 2023-11-07 09:51:48 +01:00
Andras Bacsai
d77c55148b fix: github source view 2023-11-07 09:47:25 +01:00
Andras Bacsai
ad7aa2eed6 fix: github source view 2023-11-07 09:44:47 +01:00
Andras Bacsai
5cec50efbe update install script 2023-11-06 21:14:32 +01:00
Andras Bacsai
ef8686d4da Merge pull request #1383 from krsilas/fix/check-docker-installation
Check if docker installation was successful
2023-11-06 21:13:29 +01:00
Silas Krause
8ae18f49dc Add missing fi 2023-11-01 22:13:25 +01:00
Silas Krause
4feb99cbe0 Check if docker installation was successful 2023-11-01 21:52:08 +01:00
110 changed files with 2247 additions and 958 deletions

View File

@@ -0,0 +1,22 @@
<?php
use App\Models\User;
$email = 'test@example.com';
$user = User::whereEmail($email)->first();
$teams = $user->teams;
foreach ($teams as $team) {
$servers = $team->servers;
if ($servers->count() > 0) {
foreach ($servers as $server) {
dump($server);
$server->delete();
}
}
dump($team);
$team->delete();
}
if ($user) {
dump($user);
$user->delete();
}

View File

@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Mail;
set_transanctional_email_settings(); set_transanctional_email_settings();
$users = User::whereEmail('andras.bacsai@gmail.com'); $users = User::whereEmail('test@example.com');
foreach ($users as $user) { foreach ($users as $user) {
Mail::send([], [], function ($message) use ($user) { Mail::send([], [], function ($message) use ($user) {
$message $message

View File

@@ -3,7 +3,6 @@
namespace App\Actions\Application; namespace App\Actions\Application;
use App\Models\Application; use App\Models\Application;
use App\Notifications\Application\StatusChanged;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class StopApplication class StopApplication
@@ -12,7 +11,7 @@ class StopApplication
public function handle(Application $application) public function handle(Application $application)
{ {
$server = $application->destination->server; $server = $application->destination->server;
$containers = getCurrentApplicationContainerStatus($server, $application->id); $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
if ($containers->count() > 0) { if ($containers->count() > 0) {
foreach ($containers as $container) { foreach ($containers as $container) {
$containerName = data_get($container, 'Names'); $containerName = data_get($container, 'Names');

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@@ -14,21 +15,53 @@ class StartDatabaseProxy
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
{ {
$internalPort = null; $internalPort = null;
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') { $type = $database->getMorphClass();
$network = data_get($database, 'destination.network');
$server = data_get($database, 'destination.server');
$containerName = data_get($database, 'uuid');
$proxyContainerName = "{$database->uuid}-proxy";
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
$databaseType = $database->databaseType();
$network = data_get($database, 'service.destination.network');
$server = data_get($database, 'service.destination.server');
$proxyContainerName = "{$database->service->uuid}-proxy";
switch ($databaseType) {
case 'standalone-mariadb':
$type = 'App\Models\StandaloneMariadb';
$containerName = "mariadb-{$database->service->uuid}";
break;
case 'standalone-mongodb':
$type = 'App\Models\StandaloneMongodb';
$containerName = "mongodb-{$database->service->uuid}";
break;
case 'standalone-mysql':
$type = 'App\Models\StandaloneMysql';
$containerName = "mysql-{$database->service->uuid}";
break;
case 'standalone-postgresql':
$type = 'App\Models\StandalonePostgresql';
$containerName = "postgresql-{$database->service->uuid}";
break;
case 'standalone-redis':
$type = 'App\Models\StandaloneRedis';
$containerName = "redis-{$database->service->uuid}";
break;
}
}
if ($type === 'App\Models\StandaloneRedis') {
$internalPort = 6379; $internalPort = 6379;
} else if ($database->getMorphClass() === 'App\Models\StandalonePostgresql') { } else if ($type === 'App\Models\StandalonePostgresql') {
$internalPort = 5432; $internalPort = 5432;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMongodb') { } else if ($type === 'App\Models\StandaloneMongodb') {
$internalPort = 27017; $internalPort = 27017;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMysql') { } else if ($type === 'App\Models\StandaloneMysql') {
$internalPort = 3306; $internalPort = 3306;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMariadb') { } else if ($type === 'App\Models\StandaloneMariadb') {
$internalPort = 3306; $internalPort = 3306;
} }
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid); $configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF $nginxconf = <<<EOF
user nginx; user nginx;
@@ -42,7 +75,7 @@ class StartDatabaseProxy
stream { stream {
server { server {
listen $database->public_port; listen $database->public_port;
proxy_pass $database->uuid:$internalPort; proxy_pass $containerName:$internalPort;
} }
} }
EOF; EOF;
@@ -54,19 +87,19 @@ class StartDatabaseProxy
$docker_compose = [ $docker_compose = [
'version' => '3.8', 'version' => '3.8',
'services' => [ 'services' => [
$containerName => [ $proxyContainerName => [
'build' => [ 'build' => [
'context' => $configuration_dir, 'context' => $configuration_dir,
'dockerfile' => 'Dockerfile', 'dockerfile' => 'Dockerfile',
], ],
'image' => "nginx:stable-alpine", 'image' => "nginx:stable-alpine",
'container_name' => $containerName, 'container_name' => $proxyContainerName,
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'ports' => [ 'ports' => [
"$database->public_port:$database->public_port", "$database->public_port:$database->public_port",
], ],
'networks' => [ 'networks' => [
$database->destination->network, $network,
], ],
'healthcheck' => [ 'healthcheck' => [
'test' => [ 'test' => [
@@ -81,9 +114,9 @@ class StartDatabaseProxy
] ]
], ],
'networks' => [ 'networks' => [
$database->destination->network => [ $network => [
'external' => true, 'external' => true,
'name' => $database->destination->network, 'name' => $network,
'attachable' => true, 'attachable' => true,
] ]
] ]
@@ -96,7 +129,8 @@ class StartDatabaseProxy
"echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile", "echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf", "echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml", "echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} pull",
"docker compose --project-directory {$configuration_dir} up --build -d", "docker compose --project-directory {$configuration_dir} up --build -d",
], $database->destination->server); ], $server);
} }
} }

View File

@@ -56,7 +56,7 @@ class StartMariadb
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus, 'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]
@@ -91,6 +91,8 @@ class StartMariadb
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);

View File

@@ -63,7 +63,7 @@ class StartMongodb
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus, 'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]
@@ -107,6 +107,8 @@ class StartMongodb
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);

View File

@@ -56,7 +56,7 @@ class StartMysql
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus, 'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]
@@ -91,6 +91,8 @@ class StartMysql
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);

View File

@@ -32,6 +32,8 @@ class StartPostgresql
$volume_names = $this->generate_local_persistent_volumes_only_volume_names(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables(); $environment_variables = $this->generate_environment_variables();
$this->generate_init_scripts(); $this->generate_init_scripts();
$this->add_custom_conf();
$docker_compose = [ $docker_compose = [
'version' => '3.8', 'version' => '3.8',
'services' => [ 'services' => [
@@ -64,7 +66,7 @@ class StartPostgresql
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus, 'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]
@@ -96,11 +98,26 @@ class StartPostgresql
]; ];
} }
} }
if (!is_null($this->database->postgres_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/custom-postgres.conf',
'target' => '/etc/postgresql/postgresql.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = [
'postgres',
'-c',
'config_file=/etc/postgresql/postgresql.conf',
];
}
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
@@ -171,4 +188,14 @@ class StartPostgresql
$this->init_scripts[] = "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}"; $this->init_scripts[] = "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}";
} }
} }
private function add_custom_conf()
{
if (is_null($this->database->postgres_conf)) {
return;
}
$filename = 'custom-postgres.conf';
$content = $this->database->postgres_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
} }

View File

@@ -65,7 +65,7 @@ class StartRedis
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus, 'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]
@@ -101,6 +101,8 @@ class StartRedis
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@@ -13,9 +14,13 @@ class StopDatabaseProxy
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
{ {
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server); $server = data_get($database, 'destination.server');
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = data_get($database, 'service.server');
}
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $server);
$database->is_public = false; $database->is_public = false;
$database->save(); $database->save();
} }

View File

@@ -16,17 +16,17 @@ class StartService
$commands[] = "cd " . $service->workdir(); $commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'"; $commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo '####### Creating Docker network.'"; $commands[] = "echo '####### Creating Docker network.'";
$commands[] = "docker network create --attachable {$service->uuid} >/dev/null 2>/dev/null || true"; $commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true";
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'"; $commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'"; $commands[] = "echo '####### Pulling images.'";
$commands[] = "docker compose pull"; $commands[] = "docker compose pull";
$commands[] = "echo '####### Starting containers.'"; $commands[] = "echo '####### Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate"; $commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true"; $commands[] = "docker network connect $service->uuid coolify-proxy || true";
$compose = data_get($service,'docker_compose',[]); $compose = data_get($service,'docker_compose',[]);
$serviceNames = data_get(Yaml::parse($compose),'services',[]); $serviceNames = data_get(Yaml::parse($compose),'services',[]);
foreach($serviceNames as $serviceName => $serviceConfig){ foreach($serviceNames as $serviceName => $serviceConfig){
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} 2>/dev/null || true"; $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
} }
$activity = remote_process($commands, $service->server); $activity = remote_process($commands, $service->server);
return $activity; return $activity;

View File

@@ -55,6 +55,7 @@ class General extends Component
'application.docker_registry_image_tag' => 'nullable', 'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable', 'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable', 'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@@ -77,6 +78,7 @@ class General extends Component
'application.docker_registry_image_tag' => 'Docker registry image tag', 'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location', 'application.dockerfile_location' => 'Dockerfile location',
'application.custom_labels' => 'Custom labels', 'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
]; ];
public function mount() public function mount()

View File

@@ -104,6 +104,8 @@ class CloneProject extends Component
$uuid = (string)new Cuid2(7); $uuid = (string)new Cuid2(7);
$newDatabase = $database->replicate()->fill([ $newDatabase = $database->replicate()->fill([
'uuid' => $uuid, 'uuid' => $uuid,
'status' => 'exited',
'started_at' => null,
'environment_id' => $newEnvironment->id, 'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer, 'destination_id' => $this->selectedServer,
]); ]);
@@ -111,15 +113,15 @@ class CloneProject extends Component
$environmentVaribles = $database->environment_variables()->get(); $environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) { foreach ($environmentVaribles as $environmentVarible) {
$payload = []; $payload = [];
if ($database->type() === 'standalone-postgres') { if ($database->type() === 'standalone-postgresql') {
$payload['standalone_postgresql_id'] = $newDatabase->id; $payload['standalone_postgresql_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_redis') { } else if ($database->type() === 'standalone-redis') {
$payload['standalone_redis_id'] = $newDatabase->id; $payload['standalone_redis_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_mongodb') { } else if ($database->type() === 'standalone-mongodb') {
$payload['standalone_mongodb_id'] = $newDatabase->id; $payload['standalone_mongodb_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_mysql') { } else if ($database->type() === 'standalone-mysql') {
$payload['standalone_mysql_id'] = $newDatabase->id; $payload['standalone_mysql_id'] = $newDatabase->id;
}else if ($database->type() === 'standalone_mariadb') { } else if ($database->type() === 'standalone-mariadb') {
$payload['standalone_mariadb_id'] = $newDatabase->id; $payload['standalone_mariadb_id'] = $newDatabase->id;
} }
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
@@ -134,6 +136,16 @@ class CloneProject extends Component
'destination_id' => $this->selectedServer, 'destination_id' => $this->selectedServer,
]); ]);
$newService->save(); $newService->save();
foreach ($newService->applications() as $application) {
$application->update([
'status' => 'exited',
]);
}
foreach ($newService->databases() as $database) {
$database->update([
'status' => 'exited',
]);
}
$newService->parse(); $newService->parse();
} }
return redirect()->route('project.resources', [ return redirect()->route('project.resources', [

View File

@@ -3,6 +3,7 @@
namespace App\Http\Livewire\Project\Database; namespace App\Http\Livewire\Project\Database;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url;
class BackupEdit extends Component class BackupEdit extends Component
{ {
@@ -43,14 +44,23 @@ class BackupEdit extends Component
{ {
// TODO: Delete backup from server and add a confirmation modal // TODO: Delete backup from server and add a confirmation modal
$this->backup->delete(); $this->backup->delete();
redirect()->route('project.database.backups.all', $this->parameters); if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$previousUrl = url()->previous();
$url = Url::fromString($previousUrl);
$url = $url->withoutQueryParameter('selectedBackupId');
$url = $url->withFragment('backups');
$url = $url->getPath() . "#{$url->getFragment()}";
return redirect()->to($url);
} else {
redirect()->route('project.database.backups.all', $this->parameters);
}
} }
public function instantSave() public function instantSave()
{ {
try { try {
$this->custom_validate(); $this->custom_validate();
$this->backup->save(); $this->backup->save();
$this->backup->refresh(); $this->backup->refresh();
$this->emit('success', 'Backup updated successfully'); $this->emit('success', 'Backup updated successfully');

View File

@@ -19,7 +19,11 @@ class BackupExecutions extends Component
$this->emit('error', 'Backup execution not found.'); $this->emit('error', 'Backup execution not found.');
return; return;
} }
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
$execution->delete(); $execution->delete();
$this->emit('success', 'Backup deleted successfully.'); $this->emit('success', 'Backup deleted successfully.');
$this->emit('refreshBackupExecutions'); $this->emit('refreshBackupExecutions');
@@ -33,7 +37,11 @@ class BackupExecutions extends Component
return; return;
} }
$filename = data_get($execution, 'filename'); $filename = data_get($execution, 'filename');
$server = $execution->scheduledDatabaseBackup->database->destination->server; 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); $privateKeyLocation = savePrivateKeyToFs($server);
$disk = Storage::build([ $disk = Storage::build([
'driver' => 'sftp', 'driver' => 'sftp',

View File

@@ -22,7 +22,8 @@ class CreateScheduledBackup extends Component
'frequency' => 'Backup Frequency', 'frequency' => 'Backup Frequency',
'save_s3' => 'Save to S3', 'save_s3' => 'Save to S3',
]; ];
public function mount() { public function mount()
{
if ($this->s3s->count() > 0) { if ($this->s3s->count() > 0) {
$this->s3_storage_id = $this->s3s->first()->id; $this->s3_storage_id = $this->s3s->first()->id;
} }
@@ -50,11 +51,16 @@ class CreateScheduledBackup extends Component
$payload['databases_to_backup'] = $this->database->postgres_db; $payload['databases_to_backup'] = $this->database->postgres_db;
} else if ($this->database->type() === 'standalone-mysql') { } else if ($this->database->type() === 'standalone-mysql') {
$payload['databases_to_backup'] = $this->database->mysql_database; $payload['databases_to_backup'] = $this->database->mysql_database;
}else if ($this->database->type() === 'standalone-mariadb') { } else if ($this->database->type() === 'standalone-mariadb') {
$payload['databases_to_backup'] = $this->database->mariadb_database; $payload['databases_to_backup'] = $this->database->mariadb_database;
} }
ScheduledDatabaseBackup::create($payload);
$this->emit('refreshScheduledBackups'); $databaseBackup = ScheduledDatabaseBackup::create($payload);
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$this->emit('refreshScheduledBackups', $databaseBackup->id);
} else {
$this->emit('refreshScheduledBackups');
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
handleError($e, $this); handleError($e, $this);
} finally { } finally {

View File

@@ -13,7 +13,8 @@ class General extends Component
protected $listeners = ['refresh']; protected $listeners = ['refresh'];
public StandaloneMariadb $database; public StandaloneMariadb $database;
public string $db_url; public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [ protected $rules = [
'database.name' => 'required', 'database.name' => 'required',
@@ -41,9 +42,20 @@ class General extends Component
'database.is_public' => 'Is Public', 'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port', 'database.public_port' => 'Public Port',
]; ];
public function mount()
{
$this->db_url = $this->database->getDbUrl(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl();
}
}
public function submit() public function submit()
{ {
try { try {
if (str($this->database->public_port)->isEmpty()) {
$this->database->public_port = null;
}
$this->validate(); $this->validate();
$this->database->save(); $this->database->save();
$this->emit('success', 'Database updated successfully.'); $this->emit('success', 'Database updated successfully.');
@@ -66,12 +78,13 @@ class General extends Component
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl();
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->emit('success', 'Database is no longer publicly accessible.'); $this->emit('success', 'Database is no longer publicly accessible.');
} }
$this->db_url = $this->database->getDbUrl();
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public; $this->database->is_public = !$this->database->is_public;
@@ -83,11 +96,6 @@ class General extends Component
$this->database->refresh(); $this->database->refresh();
} }
public function mount()
{
$this->db_url = $this->database->getDbUrl();
}
public function render() public function render()
{ {
return view('livewire.project.database.mariadb.general'); return view('livewire.project.database.mariadb.general');

View File

@@ -13,7 +13,8 @@ class General extends Component
protected $listeners = ['refresh']; protected $listeners = ['refresh'];
public StandaloneMongodb $database; public StandaloneMongodb $database;
public string $db_url; public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [ protected $rules = [
'database.name' => 'required', 'database.name' => 'required',
@@ -39,13 +40,25 @@ class General extends Component
'database.is_public' => 'Is Public', 'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port', 'database.public_port' => 'Public Port',
]; ];
public function mount()
{
$this->db_url = $this->database->getDbUrl(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl();
}
}
public function submit() public function submit()
{ {
try { try {
$this->validate(); if (str($this->database->public_port)->isEmpty()) {
if ($this->database->mongo_conf === "") { $this->database->public_port = null;
}
if (str($this->database->mongo_conf)->isEmpty()) {
$this->database->mongo_conf = null; $this->database->mongo_conf = null;
} }
$this->validate();
$this->database->save(); $this->database->save();
$this->emit('success', 'Database updated successfully.'); $this->emit('success', 'Database updated successfully.');
} catch (Exception $e) { } catch (Exception $e) {
@@ -67,12 +80,13 @@ class General extends Component
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl();
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->emit('success', 'Database is no longer publicly accessible.'); $this->emit('success', 'Database is no longer publicly accessible.');
} }
$this->db_url = $this->database->getDbUrl();
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public; $this->database->is_public = !$this->database->is_public;
@@ -84,11 +98,6 @@ class General extends Component
$this->database->refresh(); $this->database->refresh();
} }
public function mount()
{
$this->db_url = $this->database->getDbUrl();
}
public function render() public function render()
{ {
return view('livewire.project.database.mongodb.general'); return view('livewire.project.database.mongodb.general');

View File

@@ -13,7 +13,8 @@ class General extends Component
protected $listeners = ['refresh']; protected $listeners = ['refresh'];
public StandaloneMysql $database; public StandaloneMysql $database;
public string $db_url; public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [ protected $rules = [
'database.name' => 'required', 'database.name' => 'required',
@@ -41,9 +42,20 @@ class General extends Component
'database.is_public' => 'Is Public', 'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port', 'database.public_port' => 'Public Port',
]; ];
public function mount()
{
$this->db_url = $this->database->getDbUrl(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl();
}
}
public function submit() public function submit()
{ {
try { try {
if (str($this->database->public_port)->isEmpty()) {
$this->database->public_port = null;
}
$this->validate(); $this->validate();
$this->database->save(); $this->database->save();
$this->emit('success', 'Database updated successfully.'); $this->emit('success', 'Database updated successfully.');
@@ -66,12 +78,13 @@ class General extends Component
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl();
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->emit('success', 'Database is no longer publicly accessible.'); $this->emit('success', 'Database is no longer publicly accessible.');
} }
$this->db_url = $this->database->getDbUrl();
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public; $this->database->is_public = !$this->database->is_public;
@@ -83,11 +96,6 @@ class General extends Component
$this->database->refresh(); $this->database->refresh();
} }
public function mount()
{
$this->db_url = $this->database->getDbUrl();
}
public function render() public function render()
{ {
return view('livewire.project.database.mysql.general'); return view('livewire.project.database.mysql.general');

View File

@@ -15,7 +15,8 @@ class General extends Component
public StandalonePostgresql $database; public StandalonePostgresql $database;
public string $new_filename; public string $new_filename;
public string $new_content; public string $new_content;
public string $db_url; public ?string $db_url = null;
public ?string $db_url_public = null;
protected $listeners = ['refresh', 'save_init_script', 'delete_init_script']; protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
@@ -27,6 +28,7 @@ class General extends Component
'database.postgres_db' => 'required', 'database.postgres_db' => 'required',
'database.postgres_initdb_args' => 'nullable', 'database.postgres_initdb_args' => 'nullable',
'database.postgres_host_auth_method' => 'nullable', 'database.postgres_host_auth_method' => 'nullable',
'database.postgres_conf' => 'nullable',
'database.init_scripts' => 'nullable', 'database.init_scripts' => 'nullable',
'database.image' => 'required', 'database.image' => 'required',
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
@@ -41,6 +43,7 @@ class General extends Component
'database.postgres_db' => 'Postgres DB', 'database.postgres_db' => 'Postgres DB',
'database.postgres_initdb_args' => 'Postgres Initdb Args', 'database.postgres_initdb_args' => 'Postgres Initdb Args',
'database.postgres_host_auth_method' => 'Postgres Host Auth Method', 'database.postgres_host_auth_method' => 'Postgres Host Auth Method',
'database.postgres_conf' => 'Postgres Configuration',
'database.init_scripts' => 'Init Scripts', 'database.init_scripts' => 'Init Scripts',
'database.image' => 'Image', 'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping', 'database.ports_mappings' => 'Port Mapping',
@@ -49,7 +52,10 @@ class General extends Component
]; ];
public function mount() public function mount()
{ {
$this->db_url = $this->database->getDbUrl(); $this->db_url = $this->database->getDbUrl(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl();
}
} }
public function instantSave() public function instantSave()
{ {
@@ -66,12 +72,13 @@ class General extends Component
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl();
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->emit('success', 'Database is no longer publicly accessible.'); $this->emit('success', 'Database is no longer publicly accessible.');
} }
$this->db_url = $this->database->getDbUrl();
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public; $this->database->is_public = !$this->database->is_public;
@@ -91,7 +98,6 @@ class General extends Component
$collection = collect($this->database->init_scripts); $collection = collect($this->database->init_scripts);
$found = $collection->firstWhere('filename', $script['filename']); $found = $collection->firstWhere('filename', $script['filename']);
if ($found) { if ($found) {
ray($collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray());
$this->database->init_scripts = $collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray(); $this->database->init_scripts = $collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray();
$this->database->save(); $this->database->save();
$this->refresh(); $this->refresh();
@@ -135,6 +141,9 @@ class General extends Component
public function submit() public function submit()
{ {
try { try {
if (str($this->database->public_port)->isEmpty()) {
$this->database->public_port = null;
}
$this->validate(); $this->validate();
$this->database->save(); $this->database->save();
$this->emit('success', 'Database updated successfully.'); $this->emit('success', 'Database updated successfully.');

View File

@@ -13,7 +13,8 @@ class General extends Component
protected $listeners = ['refresh']; protected $listeners = ['refresh'];
public StandaloneRedis $database; public StandaloneRedis $database;
public string $db_url; public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [ protected $rules = [
'database.name' => 'required', 'database.name' => 'required',
@@ -35,6 +36,13 @@ class General extends Component
'database.is_public' => 'Is Public', 'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port', 'database.public_port' => 'Public Port',
]; ];
public function mount()
{
$this->db_url = $this->database->getDbUrl(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl();
}
}
public function submit() public function submit()
{ {
try { try {
@@ -63,12 +71,13 @@ class General extends Component
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl();
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->emit('success', 'Database is no longer publicly accessible.'); $this->emit('success', 'Database is no longer publicly accessible.');
} }
$this->db_url = $this->database->getDbUrl();
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public; $this->database->is_public = !$this->database->is_public;
@@ -80,10 +89,6 @@ class General extends Component
$this->database->refresh(); $this->database->refresh();
} }
public function mount()
{
$this->db_url = $this->database->getDbUrl();
}
public function render() public function render()
{ {
return view('livewire.project.database.redis.general'); return view('livewire.project.database.redis.general');

View File

@@ -8,13 +8,33 @@ class ScheduledBackups extends Component
{ {
public $database; public $database;
public $parameters; public $parameters;
public $type;
public $selectedBackup;
public $selectedBackupId;
public $s3s;
protected $listeners = ['refreshScheduledBackups']; protected $listeners = ['refreshScheduledBackups'];
protected $queryString = ['selectedBackupId'];
public function mount(): void public function mount(): void
{ {
if ($this->selectedBackupId) {
$this->setSelectedBackup($this->selectedBackupId);
}
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$this->type = 'service-database';
} else {
$this->type = 'database';
}
$this->s3s = currentTeam()->s3s;
}
public function setSelectedBackup($backupId) {
$this->selectedBackupId = $backupId;
$this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId);
if (is_null($this->selectedBackup)) {
$this->selectedBackupId = null;
}
} }
public function delete($scheduled_backup_id): void public function delete($scheduled_backup_id): void
{ {
$this->database->scheduledBackups->find($scheduled_backup_id)->delete(); $this->database->scheduledBackups->find($scheduled_backup_id)->delete();
@@ -22,9 +42,11 @@ class ScheduledBackups extends Component
$this->refreshScheduledBackups(); $this->refreshScheduledBackups();
} }
public function refreshScheduledBackups(): void public function refreshScheduledBackups(?int $id = null): void
{ {
ray('refreshScheduledBackups');
$this->database->refresh(); $this->database->refresh();
if ($id) {
$this->setSelectedBackup($id);
}
} }
} }

View File

@@ -2,28 +2,56 @@
namespace App\Http\Livewire\Project\Service; namespace App\Http\Livewire\Project\Service;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use Livewire\Component; use Livewire\Component;
class Database extends Component class Database extends Component
{ {
public ServiceDatabase $database; public ServiceDatabase $database;
public ?string $db_url_public = null;
public $fileStorages; public $fileStorages;
protected $listeners = ["refreshFileStorages"]; protected $listeners = ["refreshFileStorages"];
protected $rules = [ protected $rules = [
'database.human_name' => 'nullable', 'database.human_name' => 'nullable',
'database.description' => 'nullable', 'database.description' => 'nullable',
'database.image' => 'required', 'database.image' => 'required',
'database.exclude_from_status' => 'required|boolean', 'database.exclude_from_status' => 'required|boolean',
'database.public_port' => 'nullable|integer',
'database.is_public' => 'required|boolean',
]; ];
public function render() public function render()
{ {
return view('livewire.project.service.database'); return view('livewire.project.service.database');
} }
public function mount() { public function mount() {
if ($this->database->is_public) {
$this->db_url_public = $this->database->getServiceDatabaseUrl();
}
$this->refreshFileStorages(); $this->refreshFileStorages();
} }
public function instantSave() { public function instantSave() {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->emit('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->getServiceDatabaseUrl();
$this->emit('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->submit(); $this->submit();
} }
public function refreshFileStorages() public function refreshFileStorages()

View File

@@ -27,11 +27,15 @@ class Navbar extends Component
$activity = StartService::run($this->service); $activity = StartService::run($this->service);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} }
public function stop() public function stop(bool $forceCleanup = false)
{ {
StopService::run($this->service); StopService::run($this->service);
$this->service->refresh(); $this->service->refresh();
$this->emit('success', 'Service stopped successfully.'); if ($forceCleanup) {
$this->emit('success', 'Force cleanup service successfully.');
} else {
$this->emit('success', 'Service stopped successfully.');
}
$this->emit('checkStatus'); $this->emit('checkStatus');
} }
} }

View File

@@ -16,6 +16,8 @@ class Show extends Component
public array $parameters; public array $parameters;
public array $query; public array $query;
public Collection $services; public Collection $services;
public $s3s;
protected $listeners = ['generateDockerCompose']; protected $listeners = ['generateDockerCompose'];
public function mount() public function mount()
@@ -33,6 +35,7 @@ class Show extends Component
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first(); $this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
$this->serviceDatabase->getFilesFromServer(); $this->serviceDatabase->getFilesFromServer();
} }
$this->s3s = currentTeam()->s3s;
} catch(\Throwable $e) { } catch(\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -7,15 +7,39 @@ use Livewire\Component;
class StackForm extends Component class StackForm extends Component
{ {
public $service; public $service;
public $fields = [];
protected $listeners = ["saveCompose"]; protected $listeners = ["saveCompose"];
protected $rules = [ public $rules = [
'service.docker_compose_raw' => 'required', 'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required', 'service.docker_compose' => 'required',
'service.name' => 'required', 'service.name' => 'required',
'service.description' => 'nullable', 'service.description' => 'nullable',
]; ];
public $validationAttributes = [];
public function mount()
{
$extraFields = $this->service->extraFields();
foreach ($extraFields as $serviceName => $fields) {
foreach ($fields as $fieldKey => $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$rules = data_get($field, 'rules');
$isPassword = data_get($field, 'isPassword');
$this->fields[$key] = [
"serviceName" => $serviceName,
"key" => $key,
"name" => $fieldKey,
"value" => $value,
"isPassword" => $isPassword,
];
$this->rules["fields.$key.value"] = $rules;
$this->validationAttributes["fields.$key.value"] = $fieldKey;
}
}
}
public function saveCompose($raw) public function saveCompose($raw)
{ {
$this->service->docker_compose_raw = $raw; $this->service->docker_compose_raw = $raw;
$this->submit(); $this->submit();
} }
@@ -25,6 +49,7 @@ class StackForm extends Component
try { try {
$this->validate(); $this->validate();
$this->service->save(); $this->service->save();
$this->service->saveExtraFields($this->fields);
$this->service->parse(); $this->service->parse();
$this->service->refresh(); $this->service->refresh();
$this->service->saveComposeConfigs(); $this->service->saveComposeConfigs();

View File

@@ -55,12 +55,17 @@ class All extends Component
{ {
if ($isPreview) { if ($isPreview) {
$variables = parseEnvFormatToArray($this->variablesPreview); $variables = parseEnvFormatToArray($this->variablesPreview);
$this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
} else { } else {
$variables = parseEnvFormatToArray($this->variables); $variables = parseEnvFormatToArray($this->variables);
$this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
} }
foreach ($variables as $key => $variable) { foreach ($variables as $key => $variable) {
$found = $this->resource->environment_variables()->where('key', $key)->first(); if ($isPreview) {
$foundPreview = $this->resource->environment_variables_preview()->where('key', $key)->first(); $found = $this->resource->environment_variables_preview()->where('key', $key)->first();
} else {
$found = $this->resource->environment_variables()->where('key', $key)->first();
}
if ($found) { if ($found) {
if ($found->is_shown_once) { if ($found->is_shown_once) {
continue; continue;
@@ -68,14 +73,6 @@ class All extends Component
$found->value = $variable; $found->value = $variable;
$found->save(); $found->save();
continue; continue;
}
if ($foundPreview) {
if ($foundPreview->is_shown_once) {
continue;
}
$foundPreview->value = $variable;
$foundPreview->save();
continue;
} else { } else {
$environment = new EnvironmentVariable(); $environment = new EnvironmentVariable();
$environment->key = $key; $environment->key = $key;

View File

@@ -17,7 +17,7 @@ class GetLogs extends Component
public int $numberOfLines = 100; public int $numberOfLines = 100;
public function doSomethingWithThisChunkOfOutput($output) public function doSomethingWithThisChunkOfOutput($output)
{ {
$this->outputs .= $output; $this->outputs .= removeAnsiColors($output);
} }
public function instantSave() public function instantSave()
{ {

View File

@@ -31,7 +31,7 @@ class Logs extends Component
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail(); $this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->status = $this->resource->status; $this->status = $this->resource->status;
$this->server = $this->resource->destination->server; $this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id); $containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($containers->count() > 0) { if ($containers->count() > 0) {
$this->container = data_get($containers[0], 'Names'); $this->container = data_get($containers[0], 'Names');
} }

View File

@@ -69,7 +69,6 @@ class Backup extends Component
]); ]);
$this->database->refresh(); $this->database->refresh();
$this->backup->refresh(); $this->backup->refresh();
ray($this->backup);
$this->s3s = S3Storage::whereTeamId(0)->get(); $this->s3s = S3Storage::whereTeamId(0)->get();
} }

View File

@@ -3,17 +3,18 @@
namespace App\Http\Livewire\Source\Github; namespace App\Http\Livewire\Source\Github;
use App\Models\GithubApp; use App\Models\GithubApp;
use App\Models\InstanceSettings;
use Livewire\Component; use Livewire\Component;
class Change extends Component class Change extends Component
{ {
public string $webhook_endpoint; public string $webhook_endpoint;
public string|null $ipv4; public ?string $ipv4;
public string|null $ipv6; public ?string $ipv6;
public string|null $fqdn; public ?string $fqdn;
public bool|null $default_permissions = true; public ?bool $default_permissions = true;
public bool|null $preview_deployment_permissions = true; public ?bool $preview_deployment_permissions = true;
public $parameters; public $parameters;
public GithubApp $github_app; public GithubApp $github_app;
@@ -28,29 +29,68 @@ class Change extends Component
'github_app.custom_user' => 'required|string', 'github_app.custom_user' => 'required|string',
'github_app.custom_port' => 'required|int', 'github_app.custom_port' => 'required|int',
'github_app.app_id' => 'required|int', 'github_app.app_id' => 'required|int',
'github_app.installation_id' => 'nullable', 'github_app.installation_id' => 'required|int',
'github_app.client_id' => 'nullable', 'github_app.client_id' => 'required|string',
'github_app.client_secret' => 'nullable', 'github_app.client_secret' => 'required|string',
'github_app.webhook_secret' => 'nullable', 'github_app.webhook_secret' => 'required|string',
'github_app.is_system_wide' => 'required|bool', 'github_app.is_system_wide' => 'required|bool',
]; ];
public function mount() public function mount()
{ {
$github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::where('uuid', $github_app_uuid)->first();
if (!$this->github_app) {
return redirect()->route('source.all');
}
$settings = InstanceSettings::get();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab();
$this->fqdn = $settings->fqdn;
if ($settings->public_ipv4) {
$this->ipv4 = 'http://' . $settings->public_ipv4 . ':' . config('app.port');
}
if ($settings->public_ipv6) {
$this->ipv6 = 'http://' . $settings->public_ipv6 . ':' . config('app.port');
}
if ($this->github_app->installation_id && session('from')) {
$source_id = data_get(session('from'), 'source_id');
if (!$source_id || $this->github_app->id !== $source_id) {
session()->forget('from');
} else {
$parameters = data_get(session('from'), 'parameters');
$back = data_get(session('from'), 'back');
$environment_name = data_get($parameters, 'environment_name');
$project_uuid = data_get($parameters, 'project_uuid');
$type = data_get($parameters, 'type');
$destination = data_get($parameters, 'destination');
session()->forget('from');
return redirect()->route($back, [
'environment_name' => $environment_name,
'project_uuid' => $project_uuid,
'type' => $type,
'destination' => $destination,
]);
}
}
$this->parameters = get_route_parameters();
if (isCloud() && !isDev()) { if (isCloud() && !isDev()) {
$this->webhook_endpoint = config('app.url'); $this->webhook_endpoint = config('app.url');
} else { } else {
$this->webhook_endpoint = $this->ipv4; $this->webhook_endpoint = $this->ipv4;
$this->is_system_wide = $this->github_app->is_system_wide; $this->is_system_wide = $this->github_app->is_system_wide;
} }
$this->parameters = get_route_parameters();
} }
public function submit() public function submit()
{ {
try { try {
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->validate(); $this->validate();
$this->github_app->save(); $this->github_app->save();
$this->emit('success', 'Github App updated successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -58,6 +98,7 @@ class Change extends Component
public function instantSave() public function instantSave()
{ {
$this->submit();
} }
public function delete() public function delete()

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
class Sponsorship extends Component
{
public function disable()
{
auth()->user()->update(['is_notification_sponsorship_enabled' => false]);
}
public function render()
{
return view('livewire.sponsorship');
}
}

View File

@@ -52,7 +52,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private GithubApp|GitlabApp|string $source = 'other'; private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination; private StandaloneDocker|SwarmDocker $destination;
private Server $server; private Server $server;
private ApplicationPreview|null $preview = null; private ?ApplicationPreview $preview = null;
private string $container_name; private string $container_name;
private ?string $currently_running_container_name = null; private ?string $currently_running_container_name = null;
@@ -69,6 +69,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose_base64; private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile'; private string $dockerfile_location = '/Dockerfile';
private ?string $addHosts = null; private ?string $addHosts = null;
private ?string $buildTarget = null;
private $log_model; private $log_model;
private Collection $saved_outputs; private Collection $saved_outputs;
@@ -77,6 +78,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private string $dockerConfigFileExists = 'NOK'; private string $dockerConfigFileExists = 'NOK';
private int $customPort = 22; private int $customPort = 22;
private ?string $customRepository = null;
private ?string $fullRepoUrl = null; private ?string $fullRepoUrl = null;
private ?string $branch = null; private ?string $branch = null;
@@ -117,11 +119,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
if ($this->application->fqdn) { if ($this->application->fqdn) {
if (data_get($this->preview, 'fqdn')) { if (str($this->application->fqdn)->contains(',')) {
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn')); $url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
} else {
$url = Url::fromString($this->application->fqdn);
if (data_get($this->preview, 'fqdn')) {
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
}
} }
$template = $this->application->preview_url_template; $template = $this->application->preview_url_template;
$url = Url::fromString($this->application->fqdn);
$host = $url->getHost(); $host = $url->getHost();
$schema = $url->getScheme(); $schema = $url->getScheme();
$random = new Cuid2(7); $random = new Cuid2(7);
@@ -137,21 +144,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
// ray()->measure();
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($containers->count() === 1) {
$this->currently_running_container_name = data_get($containers[0], 'Names');
} else {
$foundContainer = $containers->filter(function ($container) {
return !str(data_get($container, 'Names'))->startsWith("{$this->application->uuid}-pr-");
})->first();
if ($foundContainer) {
$this->currently_running_container_name = data_get($foundContainer, 'Names');
}
}
if ($this->pull_request_id !== 0 && $this->pull_request_id !== null) {
$this->currently_running_container_name = $this->container_name;
}
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]); ]);
@@ -178,6 +170,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return "--add-host $name:$ip"; return "--add-host $name:$ip";
})->implode(' '); })->implode(' ');
if ($this->application->dockerfile_target_build) {
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
}
// Get user home directory // Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server); $this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
@@ -188,7 +184,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->customPort = $matches[0]; $this->customPort = $matches[0];
$gitHost = str($this->application->git_repository)->before(':'); $gitHost = str($this->application->git_repository)->before(':');
$gitRepo = str($this->application->git_repository)->after('/'); $gitRepo = str($this->application->git_repository)->after('/');
$this->application->git_repository = "$gitHost:$gitRepo"; $this->customRepository = "$gitHost:$gitRepo";
} else {
$this->customRepository = $this->application->git_repository;
} }
try { try {
if ($this->restart_only) { if ($this->restart_only) {
@@ -199,6 +197,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->deploy_dockerimage_buildpack(); $this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') { } else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile_buildpack(); $this->deploy_dockerfile_buildpack();
} else if ($this->application->build_pack === 'static') {
$this->deploy_static_buildpack();
} else { } else {
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->deploy_pull_request(); $this->deploy_pull_request();
@@ -234,6 +234,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[ [
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1", "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
"hidden" => true, "hidden" => true,
"ignore_errors" => true,
]
);
$this->execute_remote_command(
[
"docker image prune -f >/dev/null 2>&1",
"hidden" => true,
"ignore_errors" => true,
] ]
); );
} }
@@ -253,7 +261,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml") // executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml")
// ], // ],
// ); // );
// $this->build_image_name = Str::lower("{$this->application->git_repository}:build"); // $this->build_image_name = Str::lower("{$this->customRepository}:build");
// $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); // $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
// $this->save_environment_variables(); // $this->save_environment_variables();
// $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id); // $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
@@ -278,7 +286,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_image_names() private function generate_image_names()
{ {
if ($this->application->dockerfile) { if ($this->application->dockerfile) {
$this->build_image_name = Str::lower("{$this->application->git_repository}:build"); $this->build_image_name = Str::lower("{$this->application->uuid}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest"); $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
} else if ($this->application->build_pack === 'dockerimage') { } else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
@@ -290,7 +298,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (strlen($tag) > 128) { if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128); $tag = $tag->substr(0, 128);
} }
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build"); $this->build_image_name = Str::lower("{$this->application->uuid}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}"); $this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
} }
} }
@@ -298,7 +306,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'" "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
], ],
); );
$this->prepare_builder_image(); $this->prepare_builder_image();
@@ -375,7 +383,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'" "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
], ],
); );
$this->prepare_builder_image(); $this->prepare_builder_image();
@@ -394,7 +402,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'" "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
], ],
); );
$this->prepare_builder_image(); $this->prepare_builder_image();
@@ -428,6 +436,23 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_image(); $this->build_image();
$this->rolling_update(); $this->rolling_update();
} }
private function deploy_static_buildpack()
{
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->clone_repository();
$this->cleanup_git();
$this->build_image();
$this->generate_compose_file();
$this->rolling_update();
}
private function rolling_update() private function rolling_update()
{ {
@@ -494,9 +519,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_pull_request() private function deploy_pull_request()
{ {
$this->newVersionIsHealthy = true;
$this->generate_image_names(); $this->generate_image_names();
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'", "echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.'",
]); ]);
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->clone_repository(); $this->clone_repository();
@@ -510,12 +536,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// $this->generate_build_env_variables(); // $this->generate_build_env_variables();
// $this->add_build_env_variables_to_dockerfile(); // $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
if ($this->currently_running_container_name) { $this->stop_running_container();
$this->execute_remote_command(
["echo -n 'Removing old version of your application.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
}
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Starting preview deployment.'"], ["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
@@ -594,7 +615,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$importCommands = $this->generate_git_import_commands(); $importCommands = $this->generate_git_import_commands();
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '" "echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
], ],
[ [
$importCommands, "hidden" => true $importCommands, "hidden" => true
@@ -619,15 +640,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->source->getMorphClass() == 'App\Models\GithubApp') { if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) { if ($this->source->is_public) {
$this->fullRepoUrl = "{$this->source->html_url}/{$this->application->git_repository}"; $this->fullRepoUrl = "{$this->source->html_url}/{$this->customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->basedir}"; $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command); $git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
} else { } else {
$github_access_token = generate_github_installation_token($this->source); $github_access_token = generate_github_installation_token($this->source);
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->basedir}")); $commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git {$this->basedir}"));
$this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git"; $this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git";
} }
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name"; $this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
@@ -637,13 +658,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
if ($this->application->deploymentType() === 'deploy_key') { if ($this->application->deploymentType() === 'deploy_key') {
$this->fullRepoUrl = $this->application->git_repository; $this->fullRepoUrl = $this->customRepository;
$private_key = data_get($this->application, 'private_key.private_key'); $private_key = data_get($this->application, 'private_key.private_key');
if (is_null($private_key)) { if (is_null($private_key)) {
throw new Exception('Private key not found. Please add a private key to the application and try again.'); throw new Exception('Private key not found. Please add a private key to the application and try again.');
} }
$private_key = base64_encode($private_key); $private_key = base64_encode($private_key);
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_repository} {$this->basedir}"; $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command); $git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands = collect([ $commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"), executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
@@ -654,8 +675,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return $commands->implode(' && '); return $commands->implode(' && ');
} }
if ($this->application->deploymentType() === 'other') { if ($this->application->deploymentType() === 'other') {
$this->fullRepoUrl = $this->application->git_repository; $this->fullRepoUrl = $this->customRepository;
$git_clone_command = "{$git_clone_command} {$this->application->git_repository} {$this->basedir}"; $git_clone_command = "{$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command); $git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
return $commands->implode(' && '); return $commands->implode(' && ');
@@ -754,21 +775,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$labels = collect(generateLabelsApplication($this->application, $this->preview)); $labels = collect(generateLabelsApplication($this->application, $this->preview));
} }
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$newLabels = collect(generateLabelsApplication($this->application, $this->preview)); $labels = collect(generateLabelsApplication($this->application, $this->preview));
$newHostLabel = $newLabels->filter(function ($label) {
return str($label)->contains('Host');
});
$labels = $labels->reject(function ($label) {
return str($label)->contains('Host');
});
$labels = $labels->map(function ($label) { // $newHostLabel = $newLabels->filter(function ($label) {
$pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/'; // return str($label)->contains('Host');
$replacement = "$1-pr-{$this->pull_request_id}-$2-$3"; // });
$newLabel = preg_replace($pattern, $replacement, $label); // $labels = $labels->reject(function ($label) {
return $newLabel; // return str($label)->contains('Host');
}); // });
$labels = $labels->merge($newHostLabel); // ray($labels,$newLabels);
// $labels = $labels->map(function ($label) {
// $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
// $replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
// $newLabel = preg_replace($pattern, $replacement, $label);
// return $newLabel;
// });
// $labels = $labels->merge($newHostLabel);
} }
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray(); $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
$docker_compose = [ $docker_compose = [
@@ -798,7 +820,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'memswap_limit' => $this->application->limits_memory_swap, 'memswap_limit' => $this->application->limits_memory_swap,
'mem_swappiness' => $this->application->limits_memory_swappiness, 'mem_swappiness' => $this->application->limits_memory_swappiness,
'mem_reservation' => $this->application->limits_memory_reservation, 'mem_reservation' => $this->application->limits_memory_reservation,
'cpus' => $this->application->limits_cpus, 'cpus' => (int) $this->application->limits_cpus,
'cpuset' => $this->application->limits_cpuset, 'cpuset' => $this->application->limits_cpuset,
'cpu_shares' => $this->application->limits_cpu_shares, 'cpu_shares' => $this->application->limits_cpu_shares,
] ]
@@ -877,11 +899,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
foreach ($this->application->runtime_environment_variables as $env) { foreach ($this->application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value"); $environment_variables->push("$env->key=$env->value");
} }
foreach ($this->application->nixpacks_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
} else { } else {
// ray($this->application->runtime_environment_variables_preview)->green(); // ray($this->application->runtime_environment_variables_preview)->green();
foreach ($this->application->runtime_environment_variables_preview as $env) { foreach ($this->application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value"); $environment_variables->push("$env->key=$env->value");
} }
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
} }
// Add PORT if not exists, use the first port as default // Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) { if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
@@ -912,25 +940,41 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
return implode(' ', $generated_healthchecks_commands); return implode(' ', $generated_healthchecks_commands);
} }
private function pull_latest_image($image)
{
$this->execute_remote_command(
["echo -n 'Pulling latest image ($image) from the registry.'"],
[
executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true
]
);
}
private function build_image() private function build_image()
{ {
$this->execute_remote_command([ if ($this->application->build_pack === 'static') {
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]);
if ($this->application->settings->is_static) {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true "echo -n 'Static deployment. Copying static assets to the image.'",
]); ]);
} else {
$this->execute_remote_command([
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]);
}
$dockerfile = base64_encode("FROM {$this->application->static_image} if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
if ($this->application->static_image) {
$this->pull_latest_image($this->application->static_image);
}
if ($this->application->build_pack === 'static') {
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/ WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid} LABEL coolify.deploymentId={$this->deployment_uuid}
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} . COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile
COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$nginx_config = base64_encode("server {
$nginx_config = base64_encode("server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name localhost; server_name localhost;
@@ -946,43 +990,95 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
root /usr/share/nginx/html; root /usr/share/nginx/html;
} }
}"); }");
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]);
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid}
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$nginx_config = base64_encode("server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}");
}
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile-prod") executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile")
], ],
[ [
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf") executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
], ],
[ [
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
] ]
); );
} else { } else {
// Pure Dockerfile based deployment
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true executeInDocker($this->deployment_uuid, "docker build --pull $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]); ]);
} }
} }
private function stop_running_container(bool $force = false) private function stop_running_container(bool $force = false)
{ {
if ($this->currently_running_container_name) { $this->execute_remote_command(["echo -n 'Removing old version of your application.'"]);
if ($this->newVersionIsHealthy || $force) {
$this->execute_remote_command( if ($this->newVersionIsHealthy || $force) {
["echo -n 'Removing old version of your application.'"], $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
[executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], if ($this->pull_request_id !== 0) {
); $containers = $containers->filter(function ($container) {
return data_get($container, 'Names') === $this->container_name;
});
} else { } else {
$this->execute_remote_command( $containers = $containers->filter(function ($container) {
["echo -n 'New version is not healthy, rolling back to the old version.'"], return data_get($container, 'Names') !== $this->container_name;
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], });
);
} }
$containers->each(function ($container) {
$containerName = data_get($container, 'Names');
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
});
} else {
$this->execute_remote_command(
["echo -n 'New version is not healthy, rolling back to the old version.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
} }
} }
private function start_by_compose_file() private function start_by_compose_file()
{ {
if (
!$this->application->dockerfile &&
(
$this->application->build_pack === 'dockerimage' ||
$this->application->build_pack === 'dockerfile')
) {
$this->execute_remote_command(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir}"), "hidden" => true],
);
}
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"], ["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
@@ -1043,8 +1139,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
{ {
$this->execute_remote_command( $this->execute_remote_command(
["echo 'Oops something is not okay, are you okay? 😢'"], ["echo 'Oops something is not okay, are you okay? 😢'"],
["echo '{$exception->getMessage()}'"] ["echo '{$exception->getMessage()}'"],
["echo -n 'Deployment failed. Removing the new version of your application.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
); );
$this->next(ApplicationDeploymentStatus::FAILED->value); $this->next(ApplicationDeploymentStatus::FAILED->value);
} }
} }

View File

@@ -159,6 +159,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($applicationId) { if ($applicationId) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId'); $pullRequestId = data_get($labels, 'coolify.pullRequestId');
if ($pullRequestId) { if ($pullRequestId) {
if (str($applicationId)->contains('-')) {
$applicationId = str($applicationId)->before('-');
}
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) { if ($preview) {
$foundApplicationPreviews[] = $preview->id; $foundApplicationPreviews[] = $preview->id;
@@ -252,6 +255,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($projectUuid && $serviceUuid && $environmentName) { if ($projectUuid && $serviceUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid; $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
} else {
$url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']); $exitedService->update(['status' => 'exited']);
@@ -276,6 +281,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($projectUuid && $applicationUuid && $environment) { if ($projectUuid && $applicationUuid && $environment) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid; $url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
} else {
$url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
@@ -299,6 +306,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($projectUuid && $applicationUuid && $environmentName) { if ($projectUuid && $applicationUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid; $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid;
} else {
$url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
@@ -322,6 +331,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($projectUuid && $databaseUuid && $environmentName) { if ($projectUuid && $databaseUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid; $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
} else {
$url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
} }

View File

@@ -7,6 +7,7 @@ use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution; use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server; use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@@ -32,9 +33,10 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public ?Team $team = null; public ?Team $team = null;
public Server $server; public Server $server;
public ScheduledDatabaseBackup $backup; public ScheduledDatabaseBackup $backup;
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database; public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database;
public ?string $container_name = null; public ?string $container_name = null;
public ?string $directory_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null; public ?ScheduledDatabaseBackupExecution $backup_log = null;
public string $backup_status = 'failed'; public string $backup_status = 'failed';
public ?string $backup_location = null; public ?string $backup_location = null;
@@ -48,9 +50,15 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
{ {
$this->backup = $backup; $this->backup = $backup;
$this->team = Team::find($backup->team_id); $this->team = Team::find($backup->team_id);
$this->database = data_get($this->backup, 'database'); if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->server = $this->database->destination->server; $this->database = data_get($this->backup, 'database');
$this->s3 = $this->backup->s3; $this->server = $this->database->service->server;
$this->s3 = $this->backup->s3;
} else {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
}
} }
public function middleware(): array public function middleware(): array
@@ -73,14 +81,109 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$this->database->delete(); $this->database->delete();
return; return;
} }
$status = Str::of(data_get($this->database, 'status')); $status = Str::of(data_get($this->database, 'status'));
if (!$status->startsWith('running') && $this->database->id !== 0) { if (!$status->startsWith('running') && $this->database->id !== 0) {
ray('database not running'); ray('database not running');
return; return;
} }
$databaseType = $this->database->type(); if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$databasesToBackup = data_get($this->backup, 'databases_to_backup'); $databaseType = $this->database->databaseType();
$serviceUuid = $this->database->service->uuid;
$serviceName = str($this->database->service->name)->slug();
if ($databaseType === 'standalone-postgresql') {
$this->container_name = "{$this->database->name}-$serviceUuid";
$this->directory_name = $serviceName . '-' . $this->container_name;
$commands[] = "docker exec $this->container_name env | grep POSTGRES_";
$envs = instant_remote_process($commands, $this->server);
$envs = str($envs)->explode("\n");
$user = $envs->filter(function ($env) {
return str($env)->startsWith('POSTGRES_USER=');
})->first();
if ($user) {
$this->database->postgres_user = str($user)->after('POSTGRES_USER=')->value();
} else {
$this->database->postgres_user = 'postgres';
}
$db = $envs->filter(function ($env) {
return str($env)->startsWith('POSTGRES_DB=');
})->first();
if ($db) {
$databasesToBackup = str($db)->after('POSTGRES_DB=')->value();
} else {
$databasesToBackup = $this->database->postgres_user;
}
} else if ($databaseType === 'standalone-mysql') {
$this->container_name = "{$this->database->name}-$serviceUuid";
$this->directory_name = $serviceName . '-' . $this->container_name;
$commands[] = "docker exec $this->container_name env | grep MYSQL_";
$envs = instant_remote_process($commands, $this->server);
$envs = str($envs)->explode("\n");
$rootPassword = $envs->filter(function ($env) {
return str($env)->startsWith('MYSQL_ROOT_PASSWORD=');
})->first();
if ($rootPassword) {
$this->database->mysql_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
}
$db = $envs->filter(function ($env) {
return str($env)->startsWith('MYSQL_DATABASE=');
})->first();
if ($db) {
$databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
} else {
throw new \Exception('MYSQL_DATABASE not found');
}
} else if ($databaseType === 'standalone-mariadb') {
$this->container_name = "{$this->database->name}-$serviceUuid";
$this->directory_name = $serviceName . '-' . $this->container_name;
$commands[] = "docker exec $this->container_name env | grep MARIADB_";
$envs = instant_remote_process($commands, $this->server);
$envs = str($envs)->explode("\n");
$rootPassword = $envs->filter(function ($env) {
return str($env)->startsWith('MARIADB_ROOT_PASSWORD=');
})->first();
if ($rootPassword) {
$this->database->mysql_root_password = str($rootPassword)->after('MARIADB_ROOT_PASSWORD=')->value();
} else {
$rootPassword = $envs->filter(function ($env) {
return str($env)->startsWith('MYSQL_ROOT_PASSWORD=');
})->first();
if ($rootPassword) {
$this->database->mysql_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
}
}
$db = $envs->filter(function ($env) {
return str($env)->startsWith('MARIADB_DATABASE=');
})->first();
if ($db) {
$databasesToBackup = str($db)->after('MARIADB_DATABASE=')->value();
} else {
$db = $envs->filter(function ($env) {
return str($env)->startsWith('MYSQL_DATABASE=');
})->first();
if ($db) {
$databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
} else {
throw new \Exception('MARIADB_DATABASE or MYSQL_DATABASE not found');
}
}
}
} else {
$databaseName = str($this->database->name)->slug()->value();
$this->container_name = $this->database->uuid;
$this->directory_name = $databaseName . '-' . $this->container_name;
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
}
if (is_null($databasesToBackup)) { if (is_null($databasesToBackup)) {
if ($databaseType === 'standalone-postgresql') { if ($databaseType === 'standalone-postgresql') {
@@ -116,12 +219,11 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
return; return;
} }
} }
$this->container_name = $this->database->uuid; $this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->directory_name;
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
if ($this->database->name === 'coolify-db') { if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify']; $databasesToBackup = ['coolify'];
$this->container_name = "coolify-db"; $this->directory_name = $this->container_name = "coolify-db";
$ip = Str::slug($this->server->ip); $ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
} }
@@ -314,7 +416,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
if ($this->backup->number_of_backups_locally === 0) { if ($this->backup->number_of_backups_locally === 0) {
$deletable = $this->backup->executions()->where('status', 'success'); $deletable = $this->backup->executions()->where('status', 'success');
} else { } else {
$deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally); $deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally - 1);
} }
foreach ($deletable->get() as $execution) { foreach ($deletable->get() as $execution) {
delete_backup_locally($execution->filename, $this->server); delete_backup_locally($execution->filename, $this->server);
@@ -334,8 +436,12 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$bucket = $this->s3->bucket; $bucket = $this->s3->bucket;
$endpoint = $this->s3->endpoint; $endpoint = $this->s3->endpoint;
$this->s3->testConnection(); $this->s3->testConnection();
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1"; if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$network = $this->database->service->destination->network;
} else {
$network = $this->database->destination->network;
}
$commands[] = "docker run --pull=always -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server); instant_remote_process($commands, $this->server);

View File

@@ -2,8 +2,8 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server; use App\Models\Server;
use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@@ -11,18 +11,19 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000; public $timeout = 300;
public ?string $dockerRootFilesystem = null; public ?string $dockerRootFilesystem = null;
public ?int $usageBefore = null; public ?int $usageBefore = null;
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; return [(new WithoutOverlapping($this->server->uuid))];
} }
public function uniqueId(): string public function uniqueId(): string
@@ -34,14 +35,15 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
} }
public function handle(): void public function handle(): void
{ {
$queuedCount = 0; $isInprogress = false;
$this->server->applications()->each(function ($application) use ($queuedCount) { $this->server->applications()->each(function ($application) use (&$isInprogress) {
$count = data_get($application->deployments(), 'count', 0); if ($application->isDeploymentInprogress()) {
$queuedCount += $count; $isInprogress = true;
return;
}
}); });
if ($queuedCount > 0) { if ($isInprogress) {
ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange'); throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
return;
} }
try { try {
if (!$this->server->isFunctional()) { if (!$this->server->isFunctional()) {
@@ -50,23 +52,25 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
$this->dockerRootFilesystem = "/"; $this->dockerRootFilesystem = "/";
$this->usageBefore = $this->getFilesystemUsage(); $this->usageBefore = $this->getFilesystemUsage();
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) { if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name)->color('orange'); ray('Cleaning up ' . $this->server->name);
instant_remote_process(['docker image prune -af'], $this->server); instant_remote_process(['docker image prune -af'], $this->server);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server); instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server);
instant_remote_process(['docker builder prune -af'], $this->server); instant_remote_process(['docker builder prune -af'], $this->server);
$usageAfter = $this->getFilesystemUsage(); $usageAfter = $this->getFilesystemUsage();
if ($usageAfter < $this->usageBefore) { if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name)->color('orange'); ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
Log::info('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
} else { } else {
ray('DockerCleanupJob failed to save disk space on ' . $this->server->name)->color('orange'); Log::info('DockerCleanupJob failed to save disk space on ' . $this->server->name);
} }
} else { } else {
ray('No need to clean up ' . $this->server->name)->color('orange'); ray('No need to clean up ' . $this->server->name);
Log::info('No need to clean up ' . $this->server->name);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage()); send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
ray($e->getMessage())->color('orange'); ray($e->getMessage());
throw $e; throw $e;
} }
} }

View File

@@ -213,6 +213,14 @@ class Application extends BaseModel
return $this->morphTo(); return $this->morphTo();
} }
public function isDeploymentInprogress() {
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
if ($deployments > 0) {
return true;
}
return false;
}
public function deployments(int $skip = 0, int $take = 10) public function deployments(int $skip = 0, int $take = 10)
{ {
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc'); $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc');

View File

@@ -45,7 +45,184 @@ class Service extends BaseModel
{ {
return 'service'; return 'service';
} }
public function extraFields()
{
$fields = collect([]);
$applications = $this->applications()->get();
foreach ($applications as $application) {
$image = str($application->image)->before(':')->value();
switch ($image) {
case str($image)->contains('minio'):
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first();
$fields->put('MinIO', [
'Console URL' => [
'key' => data_get($console_url, 'key'),
'value' => data_get($console_url, 'value'),
'rules' => 'required|url',
],
'S3 API URL' => [
'key' => data_get($s3_api_url, 'key'),
'value' => data_get($s3_api_url, 'value'),
'rules' => 'required|url',
],
'Admin User' => [
'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value'),
'rules' => 'required',
],
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
break;
case str($image)->contains('weblate'):
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
$fields->put('Weblate', [
'Admin Email' => [
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
}
$databases = $this->databases()->get();
foreach ($databases as $database) {
$image = str($database->image)->before(':')->value();
switch ($image) {
case str($image)->contains('postgres'):
$userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
$passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
$dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
$postgres_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$postgres_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$postgres_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$fields->put('PostgreSQL', [
'User' => [
'key' => data_get($postgres_user, 'key'),
'value' => data_get($postgres_user, 'value'),
'rules' => 'required',
],
'Password' => [
'key' => data_get($postgres_password, 'key'),
'value' => data_get($postgres_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
'Database Name' => [
'key' => data_get($postgres_db_name, 'key'),
'value' => data_get($postgres_db_name, 'value'),
'rules' => 'required',
],
]);
break;
case str($image)->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mysql_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
$mysql_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$fields->put('MySQL', [
'User' => [
'key' => data_get($mysql_user, 'key'),
'value' => data_get($mysql_user, 'value'),
'rules' => 'required',
],
'Password' => [
'key' => data_get($mysql_password, 'key'),
'value' => data_get($mysql_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
'Root Password' => [
'key' => data_get($mysql_root_password, 'key'),
'value' => data_get($mysql_root_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
'Database Name' => [
'key' => data_get($mysql_db_name, 'key'),
'value' => data_get($mysql_db_name, 'value'),
'rules' => 'required',
],
]);
break;
case str($image)->contains('mariadb'):
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS'];
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA'];
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
$mariadb_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$fields->put('MariaDB', [
'User' => [
'key' => data_get($mariadb_user, 'key'),
'value' => data_get($mariadb_user, 'value'),
'rules' => 'required',
],
'Password' => [
'key' => data_get($mariadb_password, 'key'),
'value' => data_get($mariadb_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
'Root Password' => [
'key' => data_get($mariadb_root_password, 'key'),
'value' => data_get($mariadb_root_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
'Database Name' => [
'key' => data_get($mariadb_db_name, 'key'),
'value' => data_get($mariadb_db_name, 'value'),
'rules' => data_get($mariadb_db_name, 'value') && 'required',
],
]);
break;
}
}
return $fields;
}
public function saveExtraFields($fields)
{
foreach ($fields as $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$found = $this->environment_variables()->where('key', $key)->first();
if ($found) {
$found->value = $value;
$found->save();
} else {
$this->environment_variables()->create([
'key' => $key,
'value' => $value,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
}
}
public function documentation() public function documentation()
{ {
$services = getServiceTemplates(); $services = getServiceTemplates();
@@ -257,7 +434,7 @@ class Service extends BaseModel
} }
} }
$networks = collect(); $networks = collect();
foreach ($serviceNetworks as $key =>$serviceNetwork) { foreach ($serviceNetworks as $key => $serviceNetwork) {
if (gettype($serviceNetwork) === 'string') { if (gettype($serviceNetwork) === 'string') {
// networks: // networks:
// - appwrite // - appwrite
@@ -268,7 +445,7 @@ class Service extends BaseModel
// ipv4_address: 192.168.203.254 // ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null); // $networks->put($serviceNetwork, null);
ray($key); ray($key);
$networks->put($key,$serviceNetwork); $networks->put($key, $serviceNetwork);
} }
} }
foreach ($definedNetwork as $key => $network) { foreach ($definedNetwork as $key => $network) {
@@ -395,6 +572,7 @@ class Service extends BaseModel
$key = Str::of($variableName); $key = Str::of($variableName);
$value = Str::of($variable); $value = Str::of($variable);
} }
// TODO: here is the problem
if ($key->startsWith('SERVICE_FQDN')) { if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) { if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower(); $name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
@@ -452,15 +630,31 @@ class Service extends BaseModel
'service_id' => $this->id, 'service_id' => $this->id,
])->first(); ])->first();
if ($value->startsWith('SERVICE_')) { if ($value->startsWith('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_'); // Count _ in $value
$forService = $value->afterLast('_'); $count = substr_count($value->value(), '_');
$generatedValue = null; if ($count === 2) {
// SERVICE_FQDN_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
$forService = $value->afterLast('_');
$generatedValue = null;
$port = null;
}
if ($count === 3) {
// SERVICE_FQDN_UMAMI_1000
$command = $value->after('SERVICE_')->before('_');
$forService = $value->after('SERVICE_')->after('_')->before('_');
$generatedValue = null;
$port = $value->afterLast('_');
}
if ($command->value() === 'FQDN' || $command->value() === 'URL') { if ($command->value() === 'FQDN' || $command->value() === 'URL') {
if (Str::lower($forService) === $serviceName) { if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($this->server, $containerName); $fqdn = generateFqdn($this->server, $containerName);
} else { } else {
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid); $fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
} }
if ($port) {
$fqdn = "$fqdn:$port";
}
if ($foundEnv) { if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value'); $fqdn = data_get($foundEnv, 'value');
} else { } else {
@@ -476,7 +670,7 @@ class Service extends BaseModel
]); ]);
} }
if (!$isDatabase) { if (!$isDatabase) {
if ($command->value() === 'FQDN') { if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
$savedService->fqdn = $fqdn; $savedService->fqdn = $fqdn;
$savedService->save(); $savedService->save();
} }
@@ -547,7 +741,11 @@ class Service extends BaseModel
} }
// Add labels to the service // Add labels to the service
$fqdns = collect(data_get($savedService, 'fqdns')); if ($savedService->serviceType()) {
$fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
} else {
$fqdns = collect(data_get($savedService, 'fqdns'));
}
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); $defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
$serviceLabels = $serviceLabels->merge($defaultLabels); $serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) { if (!$isDatabase && $fqdns->count() > 0) {

View File

@@ -22,6 +22,16 @@ class ServiceApplication extends BaseModel
{ {
return 'service'; return 'service';
} }
public function serviceType()
{
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
return str($this->image)->before(':')->value() === $service;
})->first());
if ($found->isNotEmpty()) {
return $found;
}
return null;
}
public function service() public function service()
{ {
return $this->belongsTo(Service::class); return $this->belongsTo(Service::class);

View File

@@ -20,6 +20,28 @@ class ServiceDatabase extends BaseModel
{ {
return 'service'; return 'service';
} }
public function serviceType()
{
return null;
}
public function databaseType()
{
$image = str($this->image)->before(':');
if ($image->value() === 'postgres') {
$image = 'postgresql';
}
return "standalone-$image";
}
public function getServiceDatabaseUrl()
{
$port = $this->public_port;
$realIp = $this->service->server->ip;
if ($realIp === 'host.docker.internal' || isDev()) {
$realIp = base_ip();
}
$url = "{$realIp}:{$port}";
return $url;
}
public function service() public function service()
{ {
return $this->belongsTo(Service::class); return $this->belongsTo(Service::class);
@@ -36,4 +58,8 @@ class ServiceDatabase extends BaseModel
{ {
getFilesystemVolumesFromServer($this, $isInit); getFilesystemVolumesFromServer($this, $isInit);
} }
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
} }

View File

@@ -58,8 +58,9 @@ class StandaloneRedis extends BaseModel
{ {
return 'standalone-redis'; return 'standalone-redis';
} }
public function getDbUrl(): string { public function getDbUrl(bool $useInternal = false): string
if ($this->is_public) { {
if ($this->is_public && !$useInternal) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else { } else {
return "redis://:{$this->redis_password}@{$this->uuid}:6379/0"; return "redis://:{$this->redis_password}@{$this->uuid}:6379/0";

View File

@@ -84,11 +84,14 @@ class DeploymentFailed extends Notification implements ShouldQueue
} else { } else {
$message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): '; $message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
} }
$buttons[] = [
"text" => "Deployment logs",
"url" => $this->deployment_url
];
return [ return [
"message" => $message, "message" => $message,
"buttons" => [ "buttons" => [
"text" => "View Deployment Logs", ...$buttons
"url" => $this->deployment_url
], ],
]; ];
} }

View File

@@ -20,7 +20,7 @@ class Input extends Component
public bool $readonly = false, public bool $readonly = false,
public string|null $helper = null, public string|null $helper = null,
public bool $allowToPeak = true, public bool $allowToPeak = true,
public string $defaultClass = "input input-sm bg-coolgray-200 rounded text-white w-full disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50" public string $defaultClass = "input input-sm bg-coolgray-100 rounded text-white w-full disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
) { ) {
} }

View File

@@ -19,7 +19,7 @@ class Select extends Component
public string|null $label = null, public string|null $label = null,
public string|null $helper = null, public string|null $helper = null,
public bool $required = false, public bool $required = false,
public string $defaultClass = "select select-sm w-full rounded text-white text-sm bg-coolgray-200 font-normal disabled:bg-coolgray-200/50 disabled:border-none" public string $defaultClass = "select select-sm w-full rounded text-white text-sm bg-coolgray-100 font-normal disabled:bg-coolgray-200/50 disabled:border-none"
) { ) {
// //
} }

View File

@@ -25,7 +25,7 @@ class Textarea extends Component
public bool $readonly = false, public bool $readonly = false,
public string|null $helper = null, public string|null $helper = null,
public bool $realtimeValidation = false, public bool $realtimeValidation = false,
public string $defaultClass = "textarea leading-normal bg-coolgray-200 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50" public string $defaultClass = "textarea leading-normal bg-coolgray-100 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
) { ) {
// //
} }

View File

@@ -16,22 +16,28 @@ class Links extends Component
{ {
$this->links = collect([]); $this->links = collect([]);
$service->applications()->get()->map(function ($application) { $service->applications()->get()->map(function ($application) {
if ($application->fqdn) { $type = $application->serviceType();
$fqdns = collect(Str::of($application->fqdn)->explode(',')); if ($type) {
$fqdns->map(function ($fqdn) { $links = generateServiceSpecificFqdns($application, false);
$this->links->push(getFqdnWithoutPort($fqdn)); $this->links = $this->links->merge($links);
}); } else {
} if ($application->fqdn) {
if ($application->ports) { $fqdns = collect(Str::of($application->fqdn)->explode(','));
$portsCollection = collect(Str::of($application->ports)->explode(',')); $fqdns->map(function ($fqdn) {
$portsCollection->map(function ($port) { $this->links->push(getFqdnWithoutPort($fqdn));
if (Str::of($port)->contains(':')) { });
$hostPort = Str::of($port)->before(':'); }
} else { if ($application->ports) {
$hostPort = $port; $portsCollection = collect(Str::of($application->ports)->explode(','));
} $portsCollection->map(function ($port) {
$this->links->push(base_url(withPort:false) . ":{$hostPort}"); if (Str::of($port)->contains(':')) {
}); $hostPort = Str::of($port)->before(':');
} else {
$hostPort = $port;
}
$this->links->push(base_url(withPort: false) . ":{$hostPort}");
});
}
} }
}); });
} }

View File

@@ -23,3 +23,6 @@ const DATABASE_DOCKER_IMAGES = [
'influxdb', 'influxdb',
'clickhouse/clickhouse-server' 'clickhouse/clickhouse-server'
]; ];
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
];

View File

@@ -10,15 +10,24 @@ use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection
{ {
if ($pullRequestId) { ray($id, $pullRequestId);
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --filter='label=coolify.pullRequestId={$pullRequestId}' --format '{{json .}}' "], $server); $containers = collect([]);
} else { $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}'"], $server); $containers = format_docker_command_output_to_json($containers);
} $containers = $containers->map(function ($container) use ($pullRequestId) {
if (!$containers) { $labels = data_get($container, 'Labels');
return collect([]); if (!str($labels)->contains("coolify.pullRequestId=")) {
} data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
return format_docker_command_output_to_json($containers); return $container;
}
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
return $container;
}
return null;
});
$containers = $containers->filter();
ray($containers);
return $containers;
} }
function format_docker_command_output_to_json($rawOutput): Collection function format_docker_command_output_to_json($rawOutput): Collection
@@ -128,15 +137,50 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
$labels->push("coolify." . $type . "Id=" . $id); $labels->push("coolify." . $type . "Id=" . $id);
$labels->push("coolify.type=$type"); $labels->push("coolify.type=$type");
$labels->push('coolify.name=' . $name); $labels->push('coolify.name=' . $name);
if ($pull_request_id !== 0) { $labels->push('coolify.pullRequestId=' . $pull_request_id);
$labels->push('coolify.pullRequestId=' . $pull_request_id);
}
if ($type === 'service') { if ($type === 'service') {
$labels->push('coolify.service.subId=' . $subId); $labels->push('coolify.service.subId=' . $subId);
$labels->push('coolify.service.subType=' . $subType); $labels->push('coolify.service.subType=' . $subType);
} }
return $labels; return $labels;
} }
function generateServiceSpecificFqdns($service, $forTraefik = false)
{
$variables = collect($service->service->environment_variables);
$type = $service->serviceType();
$payload = collect([]);
switch ($type) {
case $type->contains('minio'):
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
return $payload;
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
return $payload;
}
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([
"value" => generateFqdn($service->service->server, 'console-' . $service->uuid)
]);
}
if (is_null($MINIO_SERVER_URL?->value)) {
$MINIO_SERVER_URL?->update([
"value" => generateFqdn($service->service->server, 'minio-' . $service->uuid)
]);
}
if ($forTraefik) {
$payload = collect([
$MINIO_BROWSER_REDIRECT_URL->value . ':9001',
$MINIO_SERVER_URL->value . ':9000',
]);
} else {
$payload = collect([
$MINIO_BROWSER_REDIRECT_URL->value,
$MINIO_SERVER_URL->value,
]);
}
}
return $payload;
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null) function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
{ {
$labels = collect([]); $labels = collect([]);

View File

@@ -85,7 +85,6 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
} else { } else {
$fileLocation = $path; $fileLocation = $path;
} }
ray($path,$fileLocation);
// Exists and is a file // Exists and is a file
$isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server); $isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server);
// Exists and is a directory // Exists and is a directory
@@ -135,19 +134,21 @@ function updateCompose($resource)
$image = data_get($resource, 'image'); $image = data_get($resource, 'image');
data_set($dockerCompose, "services.{$name}.image", $image); data_set($dockerCompose, "services.{$name}.image", $image);
// Update FQDN if (!str($resource->fqdn)->contains(',')) {
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper(); // Update FQDN
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
if ($generatedEnv) { $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$generatedEnv->value = $resource->fqdn; if ($generatedEnv) {
$generatedEnv->save(); $generatedEnv->value = $resource->fqdn;
} $generatedEnv->save();
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper(); }
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $variableName = "SERVICE_URL_" . Str::of($resource->name)->upper();
if ($generatedEnv) { $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Str::of($resource->fqdn)->after('://'); if ($generatedEnv) {
$generatedEnv->value = $url; $url = Str::of($resource->fqdn)->after('://');
$generatedEnv->save(); $generatedEnv->value = $url;
$generatedEnv->save();
}
} }
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2); $dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);

View File

@@ -27,7 +27,6 @@ use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Stringable; use Illuminate\Support\Stringable;
use Nubs\RandomNameGenerator\All;
use Poliander\Cron\CronExpression; use Poliander\Cron\CronExpression;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\RSA;
@@ -173,7 +172,11 @@ function get_latest_version_of_coolify(): string
function generate_random_name(?string $cuid = null): string function generate_random_name(?string $cuid = null): string
{ {
$generator = All::create(); $generator = new \Nubs\RandomNameGenerator\All(
[
new \Nubs\RandomNameGenerator\Alliteration(),
]
);
if (is_null($cuid)) { if (is_null($cuid)) {
$cuid = new Cuid2(7); $cuid = new Cuid2(7);
} }
@@ -444,20 +447,25 @@ function getServiceTemplates()
if (isDev()) { if (isDev()) {
$services = File::get(base_path('templates/service-templates.json')); $services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys(); $services = collect(json_decode($services))->sortKeys();
$version = config('version');
$services = $services->map(function ($service) use ($version) {
if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
$service->disabled = true;
}
return $service;
});
} else { } else {
$services = Http::get(config('constants.services.official')); try {
if ($services->failed()) { $response = Http::retry(3, 50)->get(config('constants.services.official'));
throw new \Exception($services->body()); if ($response->failed()) {
return collect([]);
}
$services = $response->json();
$services = collect($services)->sortKeys();
} catch (\Throwable $e) {
$services = collect([]);
} }
$services = collect($services->json())->sortKeys();
} }
// $version = config('version');
// $services = $services->map(function ($service) use ($version) {
// if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
// $service->disabled = true;
// }
// return $service;
// });
return $services; return $services;
} }
@@ -493,7 +501,8 @@ function queryResourcesByUuid(string $uuid)
return $resource; return $resource;
} }
function generateDeployWebhook($resource) { function generateDeployWebhook($resource)
{
$baseUrl = base_url(); $baseUrl = base_url();
$api = Url::fromString($baseUrl) . '/api/v1'; $api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = '/deploy'; $endpoint = '/deploy';
@@ -501,3 +510,7 @@ function generateDeployWebhook($resource) {
$url = $api . $endpoint . "?uuid=$uuid&force=false"; $url = $api . $endpoint . "?uuid=$uuid&force=false";
return $url; return $url;
} }
function removeAnsiColors($text)
{
return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text);
}

743
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -11,6 +11,7 @@ return [
'stripe_price_id_pro_yearly' => env('STRIPE_PRICE_ID_PRO_YEARLY', null), 'stripe_price_id_pro_yearly' => env('STRIPE_PRICE_ID_PRO_YEARLY', null),
'stripe_price_id_ultimate_monthly' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY', null), 'stripe_price_id_ultimate_monthly' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY', null),
'stripe_price_id_ultimate_yearly' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY', null), 'stripe_price_id_ultimate_yearly' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY', null),
'stripe_excluded_plans' => env('STRIPE_EXCLUDED_PLANS', null),
// Paddle // Paddle

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.111'; return '4.0.0-beta.128';

View File

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

View File

@@ -0,0 +1,30 @@
<?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('standalone_postgresqls', function (Blueprint $table) {
$table->longText('postgres_conf')->nullable();
$table->string('image')->default('postgres:16-alpine')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->dropColumn('postgres_conf');
$table->string('image')->default('postgres:15-alpine')->change();
});
}
};

View File

@@ -0,0 +1,30 @@
<?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('service_databases', function (Blueprint $table) {
$table->integer('public_port')->nullable();
$table->boolean('is_public')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('public_port');
$table->dropColumn('is_public');
});
}
};

View File

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

View File

@@ -0,0 +1,28 @@
<?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('users', function (Blueprint $table) {
$table->boolean('is_notification_sponsorship_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_notification_sponsorship_enabled');
});
}
};

View File

@@ -44,6 +44,7 @@ services:
- STRIPE_PRICE_ID_PRO_YEARLY - STRIPE_PRICE_ID_PRO_YEARLY
- STRIPE_PRICE_ID_ULTIMATE_MONTHLY - STRIPE_PRICE_ID_ULTIMATE_MONTHLY
- STRIPE_PRICE_ID_ULTIMATE_YEARLY - STRIPE_PRICE_ID_ULTIMATE_YEARLY
- STRIPE_EXCLUDED_PLANS
- PADDLE_VENDOR_ID - PADDLE_VENDOR_ID
- PADDLE_WEBHOOK_SECRET - PADDLE_WEBHOOK_SECRET
- PADDLE_VENDOR_AUTH_CODE - PADDLE_VENDOR_AUTH_CODE

28
package-lock.json generated
View File

@@ -7,7 +7,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.1", "alpinejs": "3.13.1",
"daisyui": "3.9.2", "daisyui": "4.0.3",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
}, },
"devDependencies": { "devDependencies": {
@@ -896,11 +896,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -952,16 +947,23 @@
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
"dev": true "dev": true
}, },
"node_modules/culori": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/culori/-/culori-3.2.0.tgz",
"integrity": "sha512-HIEbTSP7vs1mPq/2P9In6QyFE0Tkpevh0k9a+FkjhD+cwsYm9WRSbn4uMdW9O0yXlNYC3ppxL3gWWPOcvEl57w==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "3.9.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.9.2.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.0.3.tgz",
"integrity": "sha512-yJZ1QjHUaL+r9BkquTdzNHb7KIgAJVFh0zbOXql2Wu0r7zx5qZNLxclhjN0WLoIpY+o2h/8lqXg7ijj8oTigOw==", "integrity": "sha512-mG6PsdIA6MEHzdJwBlJxc1rqsIAAlcfhj2O8g0ol1uWg5y6C5zTcqfG8vKBqK4y2YfCxGIVgMsMWRTSm32N1Ow==",
"dependencies": { "dependencies": {
"colord": "^2.9",
"css-selector-tokenizer": "^0.8", "css-selector-tokenizer": "^0.8",
"postcss": "^8", "culori": "^3",
"postcss-js": "^4", "picocolors": "^1",
"tailwindcss": "^3.1" "postcss-js": "^4"
}, },
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"

View File

@@ -11,14 +11,14 @@
"axios": "1.5.1", "axios": "1.5.1",
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.31", "postcss": "8.4.31",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.5",
"vite": "4.4.11", "vite": "4.4.11",
"vue": "3.3.4" "vue": "3.3.4"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.1", "alpinejs": "3.13.1",
"daisyui": "3.9.2", "daisyui": "4.0.3",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
} }
} }

View File

@@ -53,10 +53,10 @@ a {
@apply text-white; @apply text-white;
} }
.box { .box {
@apply flex p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem]; @apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
} }
.box-without-bg { .box-without-bg {
@apply flex p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem]; @apply flex p-2 transition-colors h-16 min-h-full hover:text-white hover:no-underline min-h-[4rem];
} }
.description { .description {
@apply pt-2 text-xs font-bold text-neutral-500 group-hover:text-white; @apply pt-2 text-xs font-bold text-neutral-500 group-hover:text-white;
@@ -124,3 +124,6 @@ tr td:first-child {
.fullscreen { .fullscreen {
@apply fixed top-0 left-0 w-full h-full z-[9999] bg-coolgray-100 overflow-y-auto scrollbar pb-4 ; @apply fixed top-0 left-0 w-full h-full z-[9999] bg-coolgray-100 overflow-y-auto scrollbar pb-4 ;
} }
input.input-sm {
@apply pr-10;
}

View File

@@ -1,7 +1,7 @@
<template> <template>
<Transition name="fade"> <Transition name="fade">
<div> <div>
<div class="flex items-center p-1 px-2 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-200" <div class="flex items-center p-1 px-2 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-100"
@click="showCommandPalette = true"> @click="showCommandPalette = true">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24" stroke-width="2" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">

View File

@@ -13,7 +13,7 @@
<div class="relative" x-data> <div class="relative" x-data>
@if ($allowToPeak) @if ($allowToPeak)
<div x-on:click="changePasswordFieldType" <div x-on:click="changePasswordFieldType"
class="absolute inset-y-0 left-0 flex items-center pl-2 cursor-pointer hover:text-white"> class="absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />

View File

@@ -1,6 +1,7 @@
@auth @auth
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar"> <nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 cursor-pointer bg-coolgray-100"><img class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a> <a href="/" class="fixed top-0 z-50 mx-3 mt-3 bg-transparent cursor-pointer"><img
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap"> <ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard"> <li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif> <a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
@@ -11,6 +12,18 @@
</svg> </svg>
</a> </a>
</li> </li>
<li title="Help us!">
<a class="hover:bg-transparent"href="https://coolify.io/sponsorships" target="_blank">
<svg class="icon hover:text-pink-500" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.5 12.572L12 20l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 1 1 7.5 6.572" />
<path
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
</g>
</svg>
</a>
</li>
<li title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs"> <li title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs">
<div class="justify-center" wire:click="help" onclick="help.showModal()"> <div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">

View File

@@ -1,6 +1,7 @@
@auth @auth
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-28 scrollbar"> <nav class="fixed h-full overflow-hidden overflow-y-auto pt-28 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 cursor-pointer bg-coolgray-100"><img class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a> <a href="/" class="fixed top-0 z-50 mx-3 mt-3 bg-transparent cursor-pointer"><img
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap"> <ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard"> <li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif> <a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
@@ -87,6 +88,18 @@
@if (isInstanceAdmin() && !isCloud()) @if (isInstanceAdmin() && !isCloud())
<livewire:upgrade /> <livewire:upgrade />
@endif @endif
<li title="Help us!">
<a class="hover:bg-transparent"href="https://coolify.io/sponsorships" target="_blank">
<svg class="icon hover:text-pink-500" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.5 12.572L12 20l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 1 1 7.5 6.572" />
<path
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
</g>
</svg>
</a>
</li>
<li title="Profile"> <li title="Profile">
<a class="hover:bg-transparent" @if (!request()->is('profile')) href="/profile" @endif> <a class="hover:bg-transparent" @if (!request()->is('profile')) href="/profile" @endif>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"

View File

@@ -3,7 +3,7 @@
href="{{ route('project.service', $parameters) }}"> href="{{ route('project.service', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<x-services.links :service="$service" /> <x-services.links />
<div class="flex-1"></div> <div class="flex-1"></div>
@if (serviceStatus($service) === 'degraded') @if (serviceStatus($service) === 'degraded')
<button wire:click='deploy' onclick="startService.showModal()" <button wire:click='deploy' onclick="startService.showModal()"
@@ -38,13 +38,13 @@
</button> </button>
@endif @endif
@if (serviceStatus($service) === 'exited') @if (serviceStatus($service) === 'exited')
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <button wire:click='stop(true)' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path fill="red" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" /> <path fill="red" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" />
<path fill="red" <path fill="red"
d="M17.003 20a4.895 4.895 0 0 0-2.404-4.173L22 3l-1.73-1l-7.577 13.126a5.699 5.699 0 0 0-5.243 1.503C3.706 20.24 3.996 28.682 4.01 29.04a1 1 0 0 0 1 .96h14.991a1 1 0 0 0 .6-1.8c-3.54-2.656-3.598-8.146-3.598-8.2Zm-5.073-3.003A3.11 3.11 0 0 1 15.004 20c0 .038.002.208.017.469l-5.9-2.624a3.8 3.8 0 0 1 2.809-.848ZM15.45 28A5.2 5.2 0 0 1 14 25h-2a6.5 6.5 0 0 0 .968 3h-2.223A16.617 16.617 0 0 1 10 24H8a17.342 17.342 0 0 0 .665 4H6c.031-1.836.29-5.892 1.803-8.553l7.533 3.35A13.025 13.025 0 0 0 17.596 28Z" /> d="M17.003 20a4.895 4.895 0 0 0-2.404-4.173L22 3l-1.73-1l-7.577 13.126a5.699 5.699 0 0 0-5.243 1.503C3.706 20.24 3.996 28.682 4.01 29.04a1 1 0 0 0 1 .96h14.991a1 1 0 0 0 .6-1.8c-3.54-2.656-3.598-8.146-3.598-8.2Zm-5.073-3.003A3.11 3.11 0 0 1 15.004 20c0 .038.002.208.017.469l-5.9-2.624a3.8 3.8 0 0 1 2.809-.848ZM15.45 28A5.2 5.2 0 0 1 14 25h-2a6.5 6.5 0 0 0 .968 3h-2.223A16.617 16.617 0 0 1 10 24H8a17.342 17.342 0 0 0 .665 4H6c.031-1.836.29-5.892 1.803-8.553l7.533 3.35A13.025 13.025 0 0 0 17.596 28Z" />
</svg> </svg>
Force cleanup containers Force Cleanup Containers
</button> </button>
<button wire:click='deploy' onclick="startService.showModal()" <button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">

View File

@@ -5,7 +5,8 @@
<div class="fixed z-50 top-[4.5rem] left-4" id="vue"> <div class="fixed z-50 top-[4.5rem] left-4" id="vue">
<magic-bar></magic-bar> <magic-bar></magic-bar>
</div> </div>
<main class="main max-w-screen-2xl"> <livewire:sponsorship />
<main class="pb-10 main max-w-screen-2xl">
{{ $slot }} {{ $slot }}
</main> </main>
@endsection @endsection

View File

@@ -25,7 +25,6 @@
@endif @endif
</head> </head>
@section('body') @section('body')
<body> <body>
@livewireScripts @livewireScripts
<dialog id="help" class="modal"> <dialog id="help" class="modal">

View File

@@ -67,7 +67,7 @@
services, called resources. Any CPU intensive process will use the server's CPU where you services, called resources. Any CPU intensive process will use the server's CPU where you
are deploying your resources.</p> are deploying your resources.</p>
<p>Localhost is the server where Coolify is running on. It is not recommended to use one server <p>Localhost is the server where Coolify is running on. It is not recommended to use one server
for everyting.</p> for everything.</p>
<p>Remote Server is a server reachable through SSH. It can be hosted at home, or from any cloud <p>Remote Server is a server reachable through SSH. It can be hosted at home, or from any cloud
provider.</p> provider.</p>
</x-slot:explanation> </x-slot:explanation>

View File

@@ -14,7 +14,7 @@
@endif @endif
<div id="screen" :class="fullscreen ? 'fullscreen' : ''"> <div id="screen" :class="fullscreen ? 'fullscreen' : ''">
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif <div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
class="relative flex flex-col-reverse w-full p-2 px-4 mt-4 overflow-y-auto scrollbar border-coolgray-400" class="relative flex flex-col-reverse w-full p-2 px-4 mt-4 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
:class="fullscreen ? '' : 'max-h-[40rem] border border-dotted rounded'"> :class="fullscreen ? '' : 'max-h-[40rem] border border-dotted rounded'">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg <button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg
class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -48,7 +48,7 @@
@foreach (decode_remote_command_output($application_deployment_queue) as $line) @foreach (decode_remote_command_output($application_deployment_queue) as $line)
<div @class([ <div @class([
'font-mono whitespace-pre-line', 'font-mono whitespace-pre-line',
'text-neutral-400' => $line['type'] == 'stdout', 'text-white' => $line['type'] == 'stdout',
'text-error' => $line['type'] == 'stderr', 'text-error' => $line['type'] == 'stderr',
'text-warning' => $line['hidden'], 'text-warning' => $line['hidden'],
])>[{{ $line['timestamp'] }}] @if ($line['hidden']) ])>[{{ $line['timestamp'] }}] @if ($line['hidden'])

View File

@@ -12,14 +12,14 @@
</form> </form>
@forelse ($deployments as $deployment) @forelse ($deployments as $deployment)
<a @class([ <a @class([
'bg-coolgray-200 p-2 border-l border-dashed transition-colors hover:no-underline', 'bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline',
'cursor-not-allowed hover:bg-coolgray-200' => 'hover:bg-coolgray-200' =>
data_get($deployment, 'status') === 'queued' || data_get($deployment, 'status') === 'queued',
data_get($deployment, 'status') === 'cancelled by system',
'border-warning hover:bg-warning hover:text-black' => 'border-warning hover:bg-warning hover:text-black' =>
data_get($deployment, 'status') === 'in_progress', data_get($deployment, 'status') === 'in_progress' ||
data_get($deployment, 'status') === 'cancelled-by-user',
'border-error hover:bg-error' => 'border-error hover:bg-error' =>
data_get($deployment, 'status') === 'error', data_get($deployment, 'status') === 'failed',
'border-success hover:bg-success' => 'border-success hover:bg-success' =>
data_get($deployment, 'status') === 'finished', data_get($deployment, 'status') === 'finished',
]) href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}" ]) href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}"

View File

@@ -27,10 +27,11 @@
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.select wire:model="application.build_pack" label="Build Pack" required> <x-forms.select wire:model="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option> <option value="nixpacks">Nixpacks</option>
<option value="static">Static</option>
<option value="dockerfile">Dockerfile</option> <option value="dockerfile">Dockerfile</option>
<option value="dockerimage">Docker Image</option> <option value="dockerimage">Docker Image</option>
</x-forms.select> </x-forms.select>
@if ($application->settings->is_static) @if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.select id="application.static_image" label="Static Image" required> <x-forms.select id="application.static_image" label="Static Image" required>
<option value="nginx:alpine">nginx:alpine</option> <option value="nginx:alpine">nginx:alpine</option>
<option disabled value="apache:alpine">apache:alpine</option> <option disabled value="apache:alpine">apache:alpine</option>
@@ -50,9 +51,9 @@
<h3>Build</h3> <h3>Build</h3>
@if ($application->could_set_build_commands()) @if ($application->could_set_build_commands())
@if ($application->build_pack === 'nixpacks') @if ($application->build_pack === 'nixpacks')
<div>Nixpacks will detect your package manager/configurations: <a class="underline" <div>Nixpacks will detect the required configuration automatically.
href="https://nixpacks.com/docs/providers">Nixpacks documentation</a></div> <a class="underline" href="https://coolify.io/docs/frameworks/">Framework Specific Docs</a>
<div class="text-warning">You probably do not need to modify the commands below.</div> </div>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml" <x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml"
id="application.install_command" label="Install Command" /> id="application.install_command" label="Install Command" />
@@ -72,6 +73,8 @@
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location" <x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
label="Dockerfile Location" label="Dockerfile Location"
helper="It is calculated together with the Base Directory: {{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}" /> helper="It is calculated together with the Base Directory: {{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}" />
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target"
helper="Useful if you have multi-staged dockerfile." />
@endif @endif
@if ($application->could_set_build_commands()) @if ($application->could_set_build_commands())
@if ($application->settings->is_static) @if ($application->settings->is_static)
@@ -95,7 +98,7 @@
@endif @endif
<h3>Network</h3> <h3>Network</h3>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static) @if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly /> <x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly />
@else @else
<x-forms.input placeholder="3000,3001" id="application.ports_exposes" label="Ports Exposes" required <x-forms.input placeholder="3000,3001" id="application.ports_exposes" label="Ports Exposes" required

View File

@@ -48,7 +48,7 @@
@endif @endif
</div> </div>
@if ($application->previews->count() > 0) @if ($application->previews->count() > 0)
<h4 class="py-4">Deployed Previews</h4> <div class="pb-4">Previews</div>
<div class="flex gap-6 "> <div class="flex gap-6 ">
@foreach ($application->previews as $preview) @foreach ($application->previews as $preview)
<div class="flex flex-col p-4 bg-coolgray-200"> <div class="flex flex-col p-4 bg-coolgray-200">
@@ -71,19 +71,19 @@
</a> </a>
</div> </div>
<div class="flex items-center gap-2 pt-6"> <div class="flex items-center gap-2 pt-6">
<x-forms.button wire:click="deploy({{ data_get($preview, 'pull_request_id') }})"> <x-forms.button class="bg-coolgray-500" wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
@if (data_get($preview, 'status') === 'exited') @if (data_get($preview, 'status') === 'exited')
Deploy Deploy
@else @else
Redeploy Redeploy
@endif @endif
</x-forms.button> </x-forms.button>
<x-forms.button wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove <x-forms.button class="bg-coolgray-500" wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove
Preview Preview
</x-forms.button> </x-forms.button>
<a <a
href="{{ route('project.application.deployments', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}"> href="{{ route('project.application.deployments', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
<x-forms.button> <x-forms.button class="bg-coolgray-500">
Get Deployment Logs Get Deployment Logs
</x-forms.button> </x-forms.button>
</a> </a>

View File

@@ -49,9 +49,14 @@
label="Public Port" /> label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" /> <x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div> </div>
<x-forms.input label="MariaDB URL" <x-forms.input label="MariaDB URL (internal)"
helper="If you change the user/password/port, this could be different. This is with the default values." helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url" /> type="password" readonly wire:model="db_url" />
@if ($db_url_public)
<x-forms.input label="MariaDB URL (public)"
helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url_public" />
@endif
</div> </div>
<x-forms.textarea label="Custom MariaDB Configuration" rows="10" id="database.mariadb_conf" /> <x-forms.textarea label="Custom MariaDB Configuration" rows="10" id="database.mariadb_conf" />
</form> </form>

View File

@@ -43,9 +43,14 @@
label="Public Port" /> label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" /> <x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div> </div>
<x-forms.input label="Mongo URL" <x-forms.input label="Mongo URL (internal)"
helper="If you change the user/password/port, this could be different. This is with the default values." helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url" /> type="password" readonly wire:model="db_url" />
@if ($db_url_public)
<x-forms.input label="Mongo URL (public)"
helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url_public" />
@endif
</div> </div>
<x-forms.textarea label="Custom MongoDB Configuration" rows="10" id="database.mongo_conf" /> <x-forms.textarea label="Custom MongoDB Configuration" rows="10" id="database.mongo_conf" />
</form> </form>

View File

@@ -49,9 +49,14 @@
label="Public Port" /> label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" /> <x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div> </div>
<x-forms.input label="MySQL URL" <x-forms.input label="MySQL URL (internal)"
helper="If you change the user/password/port, this could be different. This is with the default values." helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url" /> type="password" readonly wire:model="db_url" />
@if ($db_url_public)
<x-forms.input label="MySQL URL (public)"
helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url_public" />
@endif
</div> </div>
<x-forms.textarea label="Custom Mysql Configuration" rows="10" id="database.mysql_conf" /> <x-forms.textarea label="Custom Mysql Configuration" rows="10" id="database.mysql_conf" />
</form> </form>

View File

@@ -31,14 +31,15 @@
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Initial Username" id="database.postgres_username" placeholder="If empty: postgres" <x-forms.input label="Initial Username" id="database.postgres_username" placeholder="If empty: postgres"
readonly helper="You can only change this in the database." /> readonly helper="You can only change this in the database." />
<x-forms.input label="Initial Password" id="database.postgres_password" type="password" required readonly <x-forms.input label="Initial Password" id="database.postgres_password" type="password" required
helper="You can only change this in the database." /> readonly helper="You can only change this in the database." />
<x-forms.input label="Initial Database" id="database.postgres_db" <x-forms.input label="Initial Database" id="database.postgres_db"
placeholder="If empty, it will be the same as Username." readonly placeholder="If empty, it will be the same as Username." readonly
helper="You can only change this in the database." /> helper="You can only change this in the database." />
</div> </div>
@else @else
<div class="pt-8 text-warning">Please verify these values. You can only modify them before the initial start. After that, you need to modify it in the database. <div class="pt-8 text-warning">Please verify these values. You can only modify them before the initial
start. After that, you need to modify it in the database.
</div> </div>
<div class="flex gap-2 pb-8"> <div class="flex gap-2 pb-8">
<x-forms.input label="Username" id="database.postgres_user" placeholder="If empty: postgres" /> <x-forms.input label="Username" id="database.postgres_user" placeholder="If empty: postgres" />
@@ -62,8 +63,16 @@
label="Public Port" /> label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" /> <x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div> </div>
<x-forms.input label="Postgres URL" helper="If you change the user/password/port, this could be different. This is with the default values." type="password" readonly wire:model="db_url" /> <x-forms.input label="Postgres URL (internal)"
helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url" />
@if ($db_url_public)
<x-forms.input label="Postgres URL (public)"
helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url_public" />
@endif
</div> </div>
<x-forms.textarea label="Custom PostgreSQL Configuration" rows="10" id="database.postgres_conf" />
</form> </form>
<div class="pb-16"> <div class="pb-16">
<div class="flex gap-2 pt-4 pb-2"> <div class="flex gap-2 pt-4 pb-2">

View File

@@ -21,8 +21,17 @@
label="Public Port" /> label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" /> <x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div> </div>
<x-forms.input label="Redis URL" helper="If you change the user/password/port, this could be different. This is with the default values." type="password" readonly wire:model="db_url" /> <x-forms.input label="Redis URL (internal)"
helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url" />
@if ($db_url_public)
<x-forms.input label="Redis URL (public)"
helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url_public" />
@endif
</div> </div>
<x-forms.textarea helper="<a target='_blank' class='text-white underline' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>Redis Default Configuration</a>" label="Custom Redis Configuration" rows="10" id="database.redis_conf" /> <x-forms.textarea
helper="<a target='_blank' class='text-white underline' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>Redis Default Configuration</a>"
label="Custom Redis Configuration" rows="10" id="database.redis_conf" />
</form> </form>
</div> </div>

View File

@@ -1,12 +1,36 @@
<div class="flex flex-wrap gap-2"> <div>
@forelse($database->scheduledBackups as $backup) <div class="flex flex-wrap gap-2">
<a class="flex flex-col box" @forelse($database->scheduledBackups as $backup)
href="{{ route('project.database.backups.executions', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"> @if ($type === 'database')
<div>Frequency: {{ $backup->frequency }}</div> <a class="flex flex-col box"
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div> href="{{ route('project.database.backups.executions', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
<div>Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}</div> <div>Frequency: {{ $backup->frequency }}</div>
</a> <div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
@empty <div>Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}</div>
<div>No scheduled backups configured.</div> </a>
@endforelse @else
<div @class([
'border-coollabs' =>
data_get($backup, 'id') === data_get($selectedBackup, 'id'),
'flex flex-col box border-l-2 border-transparent',
])
wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')">
<div>Frequency: {{ $backup->frequency }}</div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
<div>Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}</div>
</div>
@endif
@empty
<div>No scheduled backups configured.</div>
@endforelse
</div>
@if ($type === 'service-database' && $selectedBackup)
<div class="pt-10">
<livewire:project.database.backup-edit key="{{ $selectedBackup->id }}" :backup="$selectedBackup" :s3s="$s3s"
:status="data_get($database, 'status')" />
<h3 class="py-4">Executions</h3>
<livewire:project.database.backup-executions key="{{ $selectedBackup->id }}" :backup="$selectedBackup"
:executions="$selectedBackup->executions" />
</div>
@endif
</div> </div>

View File

@@ -182,7 +182,7 @@
</button> </button>
@endif @endif
@empty @empty
<div>No service found.</div> <div>No service found. Please try to reload the list!</div>
@endforelse @endforelse
@endif @endif
</div> </div>

View File

@@ -16,19 +16,21 @@
<x-forms.input label="Description" id="application.description"></x-forms.input> <x-forms.input label="Description" id="application.description"></x-forms.input>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@if ($application->required_fqdn) @if (!$application->serviceType()?->contains(str($application->image)->before(':')))
<x-forms.input required placeholder="https://app.coolify.io" label="Domains" @if ($application->required_fqdn)
id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input> <x-forms.input required placeholder="https://app.coolify.io" label="Domains"
@else id="application.fqdn"
<x-forms.input placeholder="https://app.coolify.io" label="Domains" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input> @else
<x-forms.input placeholder="https://app.coolify.io" label="Domains" id="application.fqdn"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
@endif
@endif @endif
<x-forms.input required <x-forms.input required
helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
label="Image" id="application.image"></x-forms.input> label="Image" id="application.image"></x-forms.input>
</div> </div>
</div> </div>
<h3 class="pt-2">Advanced</h3> <h3 class="pt-2">Advanced</h3>
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave label="Exclude from service status" <x-forms.checkbox instantSave label="Exclude from service status"

View File

@@ -12,10 +12,19 @@
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Name" id="database.human_name" placeholder="Name"></x-forms.input> <x-forms.input label="Name" id="database.human_name" placeholder="Name"></x-forms.input>
<x-forms.input label="Description" id="database.description"></x-forms.input> <x-forms.input label="Description" id="database.description"></x-forms.input>
<x-forms.input required
helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
label="Image Tag" id="database.image"></x-forms.input>
</div> </div>
<div class="flex gap-2"> <div class="flex items-end gap-2">
<x-forms.input required helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" label="Image Tag" @if ($db_url_public)
id="database.image"></x-forms.input> <x-forms.input label="Database URL (public)"
helper="Your credentials are available in your environment variables." type="password" readonly
wire:model="db_url_public" />
@endif
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div> </div>
</div> </div>
<h3 class="pt-2">Advanced</h3> <h3 class="pt-2">Advanced</h3>

View File

@@ -28,7 +28,7 @@
<div class="w-full pl-8"> <div class="w-full pl-8">
<div x-cloak x-show="activeTab === 'service-stack'"> <div x-cloak x-show="activeTab === 'service-stack'">
<livewire:project.service.stack-form :service="$service" /> <livewire:project.service.stack-form :service="$service" />
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-1">
@foreach ($applications as $application) @foreach ($applications as $application)
<div @class([ <div @class([
'border-l border-dashed border-red-500' => Str::of( 'border-l border-dashed border-red-500' => Str::of(
@@ -58,7 +58,7 @@
@endif @endif
<div class="text-xs">{{ $application->status }}</div> <div class="text-xs">{{ $application->status }}</div>
</a> </a>
<a class="flex gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline" <a class="flex items-center gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span
class="hover:text-warning">Logs</span></a> class="hover:text-warning">Logs</span></a>
</div> </div>
@@ -88,7 +88,7 @@
@endif @endif
<div class="text-xs">{{ $database->status }}</div> <div class="text-xs">{{ $database->status }}</div>
</a> </a>
<a class="flex gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white" <a class="flex items-center gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span
class="hover:text-warning">Logs</span></a> class="hover:text-warning">Logs</span></a>
</div> </div>

View File

@@ -7,10 +7,19 @@
<button><- Back</button> <button><- Back</button>
</a> </a>
<a :class="activeTab === 'general' && 'text-white'" <a :class="activeTab === 'general' && 'text-white'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a> @click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''"
href="#">General</a>
<a :class="activeTab === 'storages' && 'text-white'" <a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages @click.prevent="activeTab = 'storages'; window.location.hash = 'storages'; if(window.location.search) window.location.search = ''"
href="#">Storages
</a> </a>
@if (
$serviceDatabase?->databaseType() === 'standalone-mysql' ||
$serviceDatabase?->databaseType() === 'standalone-postgresql' ||
$serviceDatabase?->databaseType() === 'standalone-mariadb')
<a :class="activeTab === 'backups' && 'text-white'"
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#">Backups</a>
@endif
@if (data_get($parameters, 'service_name')) @if (data_get($parameters, 'service_name'))
<a class="{{ request()->routeIs('project.service.logs') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.service.logs') ? 'text-white' : '' }}"
href="{{ route('project.service.logs', $parameters) }}"> href="{{ route('project.service.logs', $parameters) }}">
@@ -43,8 +52,15 @@
</div> </div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div> <div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="text-warning">Please modify storage layout in your Docker Compose file.</span> <span class="text-warning">Please modify storage layout in your Docker Compose file.</span>
<livewire:project.service.storage wire:key="application-{{ $serviceDatabase->id }}" <livewire:project.service.storage wire:key="application-{{ $serviceDatabase->id }}" :resource="$serviceDatabase" />
:resource="$serviceDatabase" /> </div>
<div x-cloak x-show="activeTab === 'backups'">
<div class="flex gap-2 ">
<h2 class="pb-4">Scheduled Backups</h2>
<x-forms.button onclick="createScheduledBackup.showModal()">+ Add</x-forms.button>
</div>
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" :s3s="$s3s" />
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
</div> </div>
@endisset @endisset
</div> </div>

View File

@@ -9,8 +9,20 @@
File</x-forms.button> File</x-forms.button>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input id="service.name" required label="Service Name" <x-forms.input id="service.name" required label="Service Name" placeholder="My super wordpress site" />
placeholder="My super wordpress site" />
<x-forms.input id="service.description" label="Description" /> <x-forms.input id="service.description" label="Description" />
</div> </div>
@if ($fields)
<div>
<h3>Service Specific Configuration</h3>
</div>
<div class="grid grid-cols-2 gap-2">
@foreach ($fields as $serviceName => $fields)
<x-forms.input type="{{ data_get($fields, 'isPassword') ? 'password' : 'text' }}" required
helper="Variable name: {{ $serviceName }}"
label="{{ data_get($fields, 'serviceName') }} {{ data_get($fields, 'name') }}"
id="fields.{{ $serviceName }}.value"></x-forms.input>
@endforeach
</div>
@endif
</form> </form>

View File

@@ -2,7 +2,9 @@
@if ( @if (
$resource->getMorphClass() == 'App\Models\Application' || $resource->getMorphClass() == 'App\Models\Application' ||
$resource->getMorphClass() == 'App\Models\StandalonePostgresql' || $resource->getMorphClass() == 'App\Models\StandalonePostgresql' ||
$resource->getMorphClass() == 'App\Models\StandaloneRedis') $resource->getMorphClass() == 'App\Models\StandaloneRedis' ||
$resource->getMorphClass() == 'App\Models\StandaloneMariadb' ||
$resource->getMorphClass() == 'App\Models\StandaloneMongodb')
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Storages</h2> <h2>Storages</h2>
<x-helper <x-helper

View File

@@ -14,7 +14,7 @@
<x-forms.button type="submit">Refresh</x-forms.button> <x-forms.button type="submit">Refresh</x-forms.button>
</form> </form>
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full pt-4 mx-auto'"> <div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full pt-4 mx-auto'">
<div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white scrollbar border-coolgray-300" <div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
:class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'"> :class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg <button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg
class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">

View File

@@ -4,32 +4,26 @@
<p>This source will be deleted. It is not reversible. <br>Please think again.</p> <p>This source will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody> </x-slot:modalBody>
</x-modal> </x-modal>
<form wire:submit.prevent='submit' x-data> @if (data_get($github_app, 'app_id'))
<div class="flex items-center gap-2"> <form wire:submit.prevent='submit'>
<h1>GitHub App</h1> <div class="flex items-center gap-2">
<div class="flex gap-2"> <h1>GitHub App</h1>
@if ($github_app->app_id) <div class="flex gap-2">
<x-forms.button type="submit">Save</x-forms.button>
@if (data_get($github_app, 'installation_id')) @if (data_get($github_app, 'installation_id'))
<x-forms.button type="submit">Save</x-forms.button>
<a href="{{ get_installation_path($github_app) }}"> <a href="{{ get_installation_path($github_app) }}">
<x-forms.button> <x-forms.button>
Update Repositories Update Repositories
<x-external-link /> <x-external-link />
</x-forms.button> </x-forms.button>
</a> </a>
@endif @endif
@else <x-forms.button isError isModal modalId="deleteSource">
<x-forms.button disabled type="submit">Save</x-forms.button> Delete
@endif </x-forms.button>
<x-forms.button isError isModal modalId="deleteSource"> </div>
Delete
</x-forms.button>
</div> </div>
</div> <div class="subtitle">Your Private GitHub App for private repositories.</div>
<div class="subtitle">Your Private GitHub App for private repositories.</div>
@if (data_get($github_app, 'app_id'))
@if (!data_get($github_app, 'installation_id')) @if (!data_get($github_app, 'installation_id'))
<div class="mb-10 rounded alert alert-warning"> <div class="mb-10 rounded alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
@@ -39,7 +33,7 @@
</svg> </svg>
<span>You must complete this step before you can use this source!</span> <span>You must complete this step before you can use this source!</span>
</div> </div>
<a class="justify-center box" href="{{ get_installation_path($github_app) }}"> <a class="items-center justify-center box" href="{{ get_installation_path($github_app) }}">
Install Repositories on GitHub Install Repositories on GitHub
</a> </a>
@else @else
@@ -47,7 +41,7 @@
<div class="w-48"> <div class="w-48">
<x-forms.checkbox label="System Wide?" <x-forms.checkbox label="System Wide?"
helper="If checked, this GitHub App will be available for everyone in this Coolify instance." helper="If checked, this GitHub App will be available for everyone in this Coolify instance."
instantSave id="is_system_wide" /> instantSave id="github_app.is_system_wide" />
</div> </div>
@endif @endif
<div class="flex gap-2"> <div class="flex gap-2">
@@ -78,110 +72,118 @@
<x-forms.input id="github_app.webhook_secret" label="Webhook Secret" type="password" /> <x-forms.input id="github_app.webhook_secret" label="Webhook Secret" type="password" />
</div> </div>
@endif @endif
@else </form>
<div class="mb-10 rounded alert alert-warning"> @else
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" <div class="flex items-center gap-2 pb-4">
viewBox="0 0 24 24"> <h1>GitHub App</h1>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <div class="flex gap-2">
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> <x-forms.button isError isModal modalId="deleteSource">
</svg> Delete
<span>You must complete this step before you can use this source!</span> </x-forms.button>
</div> </div>
<form class="flex gap-4"> </div>
<h2>Register a GitHub App</h2> <div class="mb-10 rounded alert alert-warning">
<div class="pt-1 pb-2 ">You need to register a GitHub App before using this source.</div> <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
<div class="pt-2 pb-10"> viewBox="0 0 24 24">
@if (!isCloud() || isDev()) <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
<div class="flex items-end gap-2"> d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
<x-forms.select wire:model='webhook_endpoint' label="Webhook Endpoint" </svg>
helper="All Git webhooks will be sent to this endpoint. <br><br>If you would like to use domain instead of IP address, set your Coolify instance's FQDN in the Settings menu."> <span>You must complete this step before you can use this source!</span>
@if ($ipv4) </div>
<option value="{{ $ipv4 }}">Use {{ $ipv4 }}</option> <div class="flex flex-col">
@endif <h2 >Register a GitHub App</h2>
@if ($ipv6) <div >You need to register a GitHub App before using this source.</div>
<option value="{{ $ipv6 }}">Use {{ $ipv6 }}</option> <div class="py-10">
@endif @if (!isCloud() || isDev())
@if ($fqdn) <div class="flex items-end gap-2">
<option value="{{ $fqdn }}">Use {{ $fqdn }}</option> <x-forms.select wire:model='webhook_endpoint' label="Webhook Endpoint"
@endif helper="All Git webhooks will be sent to this endpoint. <br><br>If you would like to use domain instead of IP address, set your Coolify instance's FQDN in the Settings menu.">
@if (config('app.url')) @if ($ipv4)
<option value="{{ config('app.url') }}">Use {{ config('app.url') }}</option> <option value="{{ $ipv4 }}">Use {{ $ipv4 }}</option>
@endif @endif
</x-forms.select> @if ($ipv6)
<x-forms.button <option value="{{ $ipv6 }}">Use {{ $ipv6 }}</option>
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}')"> @endif
Register @if ($fqdn)
</x-forms.button> <option value="{{ $fqdn }}">Use {{ $fqdn }}</option>
</div> @endif
@else @if (config('app.url'))
<option value="{{ config('app.url') }}">Use {{ config('app.url') }}</option>
@endif
</x-forms.select>
<x-forms.button <x-forms.button
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}')"> x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}')">
Register Now Register
</x-forms.button> </x-forms.button>
@endif
<div class="flex flex-col gap-2 pt-4">
<x-forms.checkbox disabled instantSave id="default_permissions" label="Default Permissions"
helper="Contents: read<br>Metadata: read<br>Email: read" />
<x-forms.checkbox instantSave id="preview_deployment_permissions"
label="Preview Deployments Permission"
helper="Necessary for updating pull requests with useful comments (deployment status, links, etc.)<br><br>Pull Request: read & write" />
</div> </div>
@else
<x-forms.button
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}')">
Register Now
</x-forms.button>
@endif
<div class="flex flex-col gap-2 pt-4">
<x-forms.checkbox disabled instantSave id="default_permissions" label="Default Permissions"
helper="Contents: read<br>Metadata: read<br>Email: read" />
<x-forms.checkbox instantSave id="preview_deployment_permissions"
label="Preview Deployments Permission"
helper="Necessary for updating pull requests with useful comments (deployment status, links, etc.)<br><br>Pull Request: read & write" />
</div> </div>
</form> </div>
<script> </div>
function createGithubApp(webhook_endpoint, preview_deployment_permissions) { <script>
const { function createGithubApp(webhook_endpoint, preview_deployment_permissions) {
organization, const {
uuid, organization,
html_url uuid,
} = @json($github_app); html_url
let baseUrl = webhook_endpoint; } = @json($github_app);
const name = @js($name); let baseUrl = webhook_endpoint;
const isDev = @js(config('app.env')) === const name = @js($name);
'local'; const isDev = @js(config('app.env')) ===
const devWebhook = @js(config('coolify.dev_webhook')); 'local';
if (isDev && devWebhook) { const devWebhook = @js(config('coolify.dev_webhook'));
baseUrl = devWebhook; if (isDev && devWebhook) {
} baseUrl = devWebhook;
const webhookBaseUrl = `${baseUrl}/webhooks`;
const path = organization ? `organizations/${organization}/settings/apps/new` : 'settings/apps/new';
const default_permissions = {
contents: 'read',
metadata: 'read',
emails: 'read'
};
if (preview_deployment_permissions) {
default_permissions.pull_requests = 'write';
}
const data = {
name,
url: baseUrl,
hook_attributes: {
url: `${webhookBaseUrl}/source/github/events`,
active: true,
},
redirect_url: `${webhookBaseUrl}/source/github/redirect`,
callback_urls: [`${baseUrl}/login/github/app`],
public: false,
request_oauth_on_install: false,
setup_url: `${webhookBaseUrl}/source/github/install?source=${uuid}`,
setup_on_update: true,
default_permissions,
default_events: ['pull_request', 'push']
};
const form = document.createElement('form');
form.setAttribute('method', 'post');
form.setAttribute('action', `${html_url}/${path}?state=${uuid}`);
const input = document.createElement('input');
input.setAttribute('id', 'manifest');
input.setAttribute('name', 'manifest');
input.setAttribute('type', 'hidden');
input.setAttribute('value', JSON.stringify(data));
form.appendChild(input);
document.getElementsByTagName('body')[0].appendChild(form);
form.submit();
} }
</script> const webhookBaseUrl = `${baseUrl}/webhooks`;
@endif const path = organization ? `organizations/${organization}/settings/apps/new` : 'settings/apps/new';
</form> const default_permissions = {
contents: 'read',
metadata: 'read',
emails: 'read'
};
if (preview_deployment_permissions) {
default_permissions.pull_requests = 'write';
}
const data = {
name,
url: baseUrl,
hook_attributes: {
url: `${webhookBaseUrl}/source/github/events`,
active: true,
},
redirect_url: `${webhookBaseUrl}/source/github/redirect`,
callback_urls: [`${baseUrl}/login/github/app`],
public: false,
request_oauth_on_install: false,
setup_url: `${webhookBaseUrl}/source/github/install?source=${uuid}`,
setup_on_update: true,
default_permissions,
default_events: ['pull_request', 'push']
};
const form = document.createElement('form');
form.setAttribute('method', 'post');
form.setAttribute('action', `${html_url}/${path}?state=${uuid}`);
const input = document.createElement('input');
input.setAttribute('id', 'manifest');
input.setAttribute('name', 'manifest');
input.setAttribute('type', 'hidden');
input.setAttribute('value', JSON.stringify(data));
form.appendChild(input);
document.getElementsByTagName('body')[0].appendChild(form);
form.submit();
}
</script>
@endif
</div> </div>

View File

@@ -0,0 +1,14 @@
<div>
@if (data_get(auth()->user(), 'is_notification_sponsorship_enabled'))
<div class="toast">
<div class="flex flex-col text-white rounded alert bg-coolgray-200">
<span>Love Coolify as we do? <a href="https://coolify.io/sponsorships"
class="underline text-warning">Please
consider donating!</a>💜</span>
<span>It enables us to keep creating features without paywalls, ensuring our work remains free and
open.</span>
<x-forms.button class="bg-coolgray-400" wire:click='disable'>Disable This Popup</x-forms.button>
</div>
</div>
@endif
</div>

View File

@@ -5,10 +5,12 @@
<div class="flex flex-col gap-4 min-w-fit"> <div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'general' && 'text-white'" <a :class="activeTab === 'general' && 'text-white'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a> @click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
<a :class="activeTab === 'environment-variables' && 'text-white'" @if ($application->build_pack !== 'static')
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'" <a :class="activeTab === 'environment-variables' && 'text-white'"
href="#">Environment @click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
Variables</a> href="#">Environment
Variables</a>
@endif
@if ($application->git_based()) @if ($application->git_based())
<a :class="activeTab === 'source' && 'text-white'" <a :class="activeTab === 'source' && 'text-white'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a> @click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@@ -16,21 +18,25 @@
<a :class="activeTab === 'server' && 'text-white'" <a :class="activeTab === 'server' && 'text-white'"
@click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server @click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server
</a> </a>
<a :class="activeTab === 'storages' && 'text-white'" @if ($application->build_pack !== 'static')
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages <a :class="activeTab === 'storages' && 'text-white'"
</a> @click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
</a>
@endif
<a :class="activeTab === 'webhooks' && 'text-white'" <a :class="activeTab === 'webhooks' && 'text-white'"
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks @click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
</a> </a>
@if ($application->git_based()) @if ($application->git_based() && $application->build_pack !== 'static')
<a :class="activeTab === 'previews' && 'text-white'" <a :class="activeTab === 'previews' && 'text-white'"
@click.prevent="activeTab = 'previews'; window.location.hash = 'previews'" href="#">Preview @click.prevent="activeTab = 'previews'; window.location.hash = 'previews'" href="#">Preview
Deployments Deployments
</a> </a>
@endif @endif
<a :class="activeTab === 'health' && 'text-white'" @if ($application->build_pack !== 'static')
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Health Checks <a :class="activeTab === 'health' && 'text-white'"
</a> @click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Health Checks
</a>
@endif
<a :class="activeTab === 'rollback' && 'text-white'" <a :class="activeTab === 'rollback' && 'text-white'"
@click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback @click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback
</a> </a>

View File

@@ -20,11 +20,10 @@ use App\Http\Livewire\Server\PrivateKey\Show as PrivateKeyShow;
use App\Http\Livewire\Server\Proxy\Show as ProxyShow; use App\Http\Livewire\Server\Proxy\Show as ProxyShow;
use App\Http\Livewire\Server\Proxy\Logs as ProxyLogs; use App\Http\Livewire\Server\Proxy\Logs as ProxyLogs;
use App\Http\Livewire\Server\Show; use App\Http\Livewire\Server\Show;
use App\Http\Livewire\Source\Github\Change as GitHubChange;
use App\Http\Livewire\Subscription\Show as SubscriptionShow; use App\Http\Livewire\Subscription\Show as SubscriptionShow;
use App\Http\Livewire\Waitlist\Index as WaitlistIndex; use App\Http\Livewire\Waitlist\Index as WaitlistIndex;
use App\Models\GithubApp;
use App\Models\GitlabApp; use App\Models\GitlabApp;
use App\Models\InstanceSettings;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
@@ -178,49 +177,7 @@ Route::middleware(['auth'])->group(function () {
'sources' => $sources, 'sources' => $sources,
]); ]);
})->name('source.all'); })->name('source.all');
Route::get('/source/github/{github_app_uuid}', function (Request $request) { Route::get('/source/github/{github_app_uuid}', GitHubChange::class)->name('source.github.show');
$github_app = GithubApp::where('uuid', request()->github_app_uuid)->first();
if (!$github_app) {
abort(404);
}
$github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$settings = InstanceSettings::get();
$name = Str::of(Str::kebab($github_app->name));
if ($settings->public_ipv4) {
$ipv4 = 'http://' . $settings->public_ipv4 . ':' . config('app.port');
}
if ($settings->public_ipv6) {
$ipv6 = 'http://' . $settings->public_ipv6 . ':' . config('app.port');
}
if ($github_app->installation_id && session('from')) {
$source_id = data_get(session('from'), 'source_id');
if (!$source_id || $github_app->id !== $source_id) {
session()->forget('from');
} else {
$parameters = data_get(session('from'), 'parameters');
$back = data_get(session('from'), 'back');
$environment_name = data_get($parameters, 'environment_name');
$project_uuid = data_get($parameters, 'project_uuid');
$type = data_get($parameters, 'type');
$destination = data_get($parameters, 'destination');
session()->forget('from');
return redirect()->route($back, [
'environment_name' => $environment_name,
'project_uuid' => $project_uuid,
'type' => $type,
'destination' => $destination,
]);
}
}
return view('source.github.show', [
'github_app' => $github_app,
'name' => $name,
'ipv4' => $ipv4 ?? null,
'ipv6' => $ipv6 ?? null,
'fqdn' => $settings->fqdn,
]);
})->name('source.github.show');
Route::get('/source/gitlab/{gitlab_app_uuid}', function (Request $request) { Route::get('/source/gitlab/{gitlab_app_uuid}', function (Request $request) {
$gitlab_app = GitlabApp::where('uuid', request()->gitlab_app_uuid)->first(); $gitlab_app = GitlabApp::where('uuid', request()->gitlab_app_uuid)->first();
return view('source.gitlab.show', [ return view('source.gitlab.show', [

View File

@@ -237,7 +237,7 @@ Route::post('/payments/stripe/events', function () {
try { try {
$webhookSecret = config('subscription.stripe_webhook_secret'); $webhookSecret = config('subscription.stripe_webhook_secret');
$signature = request()->header('Stripe-Signature'); $signature = request()->header('Stripe-Signature');
$excludedPlans = config('subscription.stripe_excluded_plans');
$event = \Stripe\Webhook::constructEvent( $event = \Stripe\Webhook::constructEvent(
request()->getContent(), request()->getContent(),
$signature, $signature,
@@ -253,6 +253,10 @@ Route::post('/payments/stripe/events', function () {
switch ($type) { switch ($type) {
case 'checkout.session.completed': case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id'); $clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
send_internal_notification('Checkout session completed without client reference id.');
break;
}
$userId = Str::before($clientReferenceId, ':'); $userId = Str::before($clientReferenceId, ':');
$teamId = Str::after($clientReferenceId, ':'); $teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription'); $subscriptionId = data_get($data, 'subscription');
@@ -282,12 +286,17 @@ Route::post('/payments/stripe/events', function () {
break; break;
case 'invoice.paid': case 'invoice.paid':
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first(); $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (!$subscription) { if (!$subscription) {
Sleep::for(5)->seconds(); Sleep::for(5)->seconds();
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
} }
$planId = data_get($data, 'lines.data.0.plan.id');
$subscription->update([ $subscription->update([
'stripe_plan_id' => $planId, 'stripe_plan_id' => $planId,
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
@@ -303,11 +312,15 @@ Route::post('/payments/stripe/events', function () {
break; break;
case 'customer.subscription.updated': case 'customer.subscription.updated':
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended');
$status = data_get($data, 'status'); $status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription'); $subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id'); $planId = data_get($data, 'items.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended');
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end'); $alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback'); $feedback = data_get($data, 'cancellation_details.feedback');

View File

@@ -5,7 +5,7 @@
## Always run "php artisan app:sync-to-bunny-cdn --env=secrets" or "scripts/run sync-bunny" if you update this file. ## Always run "php artisan app:sync-to-bunny-cdn --env=secrets" or "scripts/run sync-bunny" if you update this file.
########### ###########
VERSION="1.0.2" VERSION="1.0.3"
DOCKER_VERSION="24.0" DOCKER_VERSION="24.0"
CDN="https://cdn.coollabs.io/coolify" CDN="https://cdn.coollabs.io/coolify"
@@ -46,7 +46,14 @@ apt install -y curl wget git jq jc >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
echo "Docker is not installed. Installing Docker..." echo "Docker is not installed. Installing Docker..."
curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh
echo "Docker installed successfully" if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Docker installation failed."
echo "Maybe your OS is not supported."
echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
fi fi
echo -e "-------------" echo -e "-------------"
echo -e "Check Docker Configuration..." echo -e "Check Docker Configuration..."

View File

@@ -39,12 +39,12 @@ module.exports = {
themes: [ themes: [
{ {
coollabs: { coollabs: {
primary: "#323232", primary: "#202020",
"primary-focus": "#242424", "primary-focus": "#242424",
secondary: "#6B16ED", secondary: "#6B16ED",
accent: "#4338ca", accent: "#4338ca",
neutral: "#1B1D1D", neutral: "#1B1D1D",
"base-100": "#181818", "base-100": "#101010",
info: "#2563EB", info: "#2563EB",
success: "#16A34A", success: "#16A34A",
warning: "#FCD34D", warning: "#FCD34D",

View File

@@ -0,0 +1,48 @@
# documentation: https://docs.directus.io/self-hosted/quickstart.html
# slogan: Directus is an open-source tool that wraps custom SQL databases with a dynamic API, and provides an intuitive admin app for managing its content.
# tags: directus, cms, database, sql
services:
directus:
image: directus/directus:10.7
volumes:
- directus-uploads:/directus/uploads
- directus-extensions:/directus/extensions
environment:
- SERVICE_FQDN_DIRECTUS
- KEY=$SERVICE_BASE64_64_KEY
- SECRET=$SERVICE_BASE64_64_SECRET
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
- ADMIN_PASSWORD=$SERVICE_PASSWORD_ADMIN
- DB_CLIENT=postgres
- DB_HOST=postgresql
- DB_PORT=5432
- DB_DATABASE=${POSTGRESQL_DATABASE:-directus}
- DB_USER=$SERVICE_USER_POSTGRESQL
- DB_PASSWORD=$SERVICE_PASSWORD_POSTGRESQL
- REDIS_HOST=redis
- REDIS_PORT=6379
- WEBSOCKETS_ENABLED=true
postgresql:
image: postgres:15-alpine
volumes:
- directus-postgresql-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
- POSTGRES_DB=${POSTGRESQL_DATABASE:-directus}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 20s
retries: 10
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- directus-redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -0,0 +1,19 @@
# documentation: https://docs.directus.io/self-hosted/quickstart.html
# slogan: Directus is an open-source tool that wraps custom SQL databases with a dynamic API, and provides an intuitive admin app for managing its content.
# tags: directus, cms, database, sql
services:
directus:
image: directus/directus:10.7
volumes:
- directus-database:/directus/database
- directus-uploads:/directus/uploads
environment:
- SERVICE_FQDN_DIRECTUS
- KEY=$SERVICE_BASE64_64_KEY
- SECRET=$SERVICE_BASE64_64_SECRET
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
- ADMIN_PASSWORD=$SERVICE_PASSWORD_ADMIN
- DB_CLIENT=sqlite3
- DB_FILENAME=/directus/database/data.db
- WEBSOCKETS_ENABLED=true

View File

@@ -1,4 +1,4 @@
# documentation: https://fider.io/doc # documentation: https://fider.io/docs
# slogan: Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services. # slogan: Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services.
# tags: feedback, user-feedback # tags: feedback, user-feedback

View File

@@ -6,7 +6,7 @@ services:
gitea: gitea:
image: gitea/gitea:latest image: gitea/gitea:latest
environment: environment:
- SERVICE_FQDN_GITEA - SERVICE_FQDN_GITEA_3000
- USER_UID=1000 - USER_UID=1000
- USER_GID=1000 - USER_GID=1000
- GITEA__database__DB_TYPE=mysql - GITEA__database__DB_TYPE=mysql
@@ -18,8 +18,8 @@ services:
- gitea-data:/var/lib/gitea - gitea-data:/var/lib/gitea
- gitea-timezone:/etc/timezone:ro - gitea-timezone:/etc/timezone:ro
- gitea-localtime:/etc/localtime:ro - gitea-localtime:/etc/localtime:ro
labels: ports:
- "traefik.http.services.gitea-websecure.loadbalancer.server.port=3000" - 22222:22
depends_on: depends_on:
mariadb: mariadb:
condition: service_healthy condition: service_healthy

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