Compare commits

...

110 Commits

Author SHA1 Message Date
Andras Bacsai
cf01d32237 Merge pull request #1960 from coollabsio/next
v4.0.0-beta.255
2024-04-10 08:36:45 +02:00
Andras Bacsai
5ec553f68d Reset default labels when application FQDN is updated 2024-04-10 08:21:06 +02:00
Andras Bacsai
d95e5a169d Update version numbers to 4.0.0-beta.255 2024-04-10 08:17:31 +02:00
Andras Bacsai
8debefddad Merge pull request #1955 from coollabsio/next
v4.0.0-beta.254
2024-04-09 13:17:02 +02:00
Andras Bacsai
bd25860ccf Add check for enabled tasks in scheduled tasks loop 2024-04-09 13:03:06 +02:00
Andras Bacsai
b21cb5c0e9 Update job schedules to run every two minutes instead of every minute 2024-04-09 13:02:22 +02:00
Andras Bacsai
ff79a2d3f4 Commented out ray() function call in remoteProcess.php 2024-04-09 13:02:16 +02:00
Andras Bacsai
9ae3743a58 Update job schedules and version numbers 2024-04-09 12:48:58 +02:00
Andras Bacsai
ffcdbcc802 Merge branch 'main' into next 2024-04-09 12:28:43 +02:00
Andras Bacsai
3ff78a3a47 Update version to 1.3.1 in install.sh 2024-04-09 12:28:11 +02:00
Andras Bacsai
4d78ac4789 Remove unnecessary sed command in install.sh script 2024-04-09 12:28:05 +02:00
Andras Bacsai
be24f2d520 Create SECURITY.md 2024-04-09 12:27:35 +02:00
Andras Bacsai
ff7fccb6a2 Merge pull request #1954 from coollabsio/next
v4.0.0-beta.253
2024-04-09 12:22:32 +02:00
Andras Bacsai
ee5a2b3c38 Add migration to make custom docker commands longer 2024-04-09 11:58:14 +02:00
Andras Bacsai
72b9001447 Fix error message in generate_github_installation_token function 2024-04-09 11:54:15 +02:00
Andras Bacsai
353245bb7d fix: hashed random delimeter in ssh commands + make sure to remove the delimeter from the command 2024-04-09 11:48:57 +02:00
Andras Bacsai
f9411bf0ed Remove debug statement in generateGitImportCommands function 2024-04-09 11:48:26 +02:00
Andras Bacsai
0eedbd2aa1 Update version numbers to 4.0.0-beta.253 2024-04-09 11:09:16 +02:00
Andras Bacsai
5e36c37838 Add coolify database and handle exceptions 2024-04-09 11:09:13 +02:00
Andras Bacsai
35c76c8e2a Merge pull request #1946 from coollabsio/next
v4.0.0-beta.252
2024-04-09 09:28:29 +02:00
Andras Bacsai
90ad46b7c5 Update SUPPORTED_OS with new OS: amzn 2024-04-09 09:25:55 +02:00
Andras Bacsai
1c06e1e2a4 Update version numbers to 4.0.0-beta.252 2024-04-09 09:03:53 +02:00
Andras Bacsai
15d2c0e436 Update font style for logs in deployment show view 2024-04-09 08:46:06 +02:00
Andras Bacsai
9984aea42f Add ContainerStatusJob to Revived notification 2024-04-09 08:46:03 +02:00
Andras Bacsai
a9cc5cc351 Fix server functionality check and cleanup SSH keys 2024-04-09 08:46:00 +02:00
Andras Bacsai
a6cbabfba5 Add server connection validation in CheckProxy.php 2024-04-09 08:45:37 +02:00
Andras Bacsai
1ef85242ec update supabase 2024-04-08 15:16:52 +02:00
Andras Bacsai
ffe75b0856 Merge pull request #1943 from gionatamettifogo/main
Update supabase.yaml - JWT_SERCET -> should be JWT_SECRET
2024-04-08 15:16:15 +02:00
Andras Bacsai
25e3c4fcdc feat: add amazon linux 2023 2024-04-08 14:33:07 +02:00
Andras Bacsai
a922497f5d Merge branch 'next' of github.com:coollabsio/coolify into next 2024-04-08 14:09:40 +02:00
Andras Bacsai
acb100908b Merge pull request #1895 from moinulmoin/refactor/ui
fix: unintended left padding on sidebar
2024-04-08 14:09:14 +02:00
Andras Bacsai
7fa2cbc746 Merge branch 'main' into refactor/ui 2024-04-08 14:08:16 +02:00
Andras Bacsai
b36491af8e Update Stirling PDF healthcheck 2024-04-08 14:07:18 +02:00
Andras Bacsai
ea4fe81cb2 fix: git submodule update 2024-04-08 14:07:07 +02:00
Andras Bacsai
2191f1b826 Refactor source.blade.php layout 2024-04-08 14:06:40 +02:00
Andras Bacsai
08f6367752 Fix conditional statement in getServiceTemplates function 2024-04-08 14:06:31 +02:00
Andras Bacsai
e1b09f4844 Merge pull request #1913 from loganspark/application-update-submodules-after-git-checkout
feat(application): update submodules after git checkout
2024-04-08 13:43:43 +02:00
Andras Bacsai
884c46b054 Merge branch 'main' into application-update-submodules-after-git-checkout 2024-04-08 13:40:16 +02:00
Andras Bacsai
d1cde123dc Merge pull request #1933 from gionatamettifogo/patch-1
Update stirling-pdf.yaml
2024-04-08 13:32:55 +02:00
Andras Bacsai
5eaded7e3a Update package-lock.json and package.json with latest versions of dependencies 2024-04-08 13:23:44 +02:00
Andras Bacsai
4ae4e88800 Update instance update process 2024-04-08 13:04:38 +02:00
Andras Bacsai
85fb27a631 Refactor project links and add click event handlers 2024-04-08 13:04:33 +02:00
Andras Bacsai
8bf0561009 Refactor code for domain usage and validation 2024-04-08 12:15:44 +02:00
Andras Bacsai
ddfded048c fix: how to update docker-compose, environment variables and fqdns 2024-04-08 11:17:00 +02:00
Andras Bacsai
a2e889587e Refactor environment variable sorting by key 2024-04-08 11:16:42 +02:00
Andras Bacsai
b63aaad645 Update warning message for multiple domains in ServiceApplicationView.php 2024-04-08 11:16:25 +02:00
Andras Bacsai
2c41b5d4fb Fix null pointer exception in Index.php 2024-04-08 11:16:20 +02:00
Andras Bacsai
34aa4c6412 fix: storage layout 2024-04-08 09:51:14 +02:00
Andras Bacsai
82372ea252 Refactor upgrade method in Upgrade.php 2024-04-08 09:36:24 +02:00
Andras Bacsai
1f4440bcf9 Refactor image name generation in ApplicationDeploymentJob.php 2024-04-08 09:36:21 +02:00
Andras Bacsai
31fcff4afc Update OAuth button width in login page 2024-04-08 09:16:16 +02:00
Gionata Mettifogo
082f17f940 Update supabase.yaml - JWT_SERCET -> should be JWT_SECRET
There is a spelling error in supabase.yml that prevents the auth component from working. 

JWT_SERCET -> should be JWT_SECRET
2024-04-07 11:56:46 +02:00
Andras Bacsai
aa2ac3865c Merge pull request #1938 from coollabsio/next
v4.0.0-beta.251
2024-04-05 18:50:18 +02:00
Andras Bacsai
db8ffe50ac fix: isMember 2024-04-05 18:47:07 +02:00
Andras Bacsai
8fe658bacc fix: members cannot manage subscriptions 2024-04-05 18:34:33 +02:00
Andras Bacsai
c71d5e2dfb Merge pull request #1936 from coollabsio/next
v4.0.0-beta.250
2024-04-05 16:50:08 +02:00
Andras Bacsai
36c31dcd67 Add role-based authorization for updating teams 2024-04-05 16:48:06 +02:00
Andras Bacsai
c223408c3c Add updatedApplicationFqdn method to ServiceApplicationView 2024-04-05 15:59:59 +02:00
Andras Bacsai
3cdccb49ef Fix deploy button alignment in destination.blade.php 2024-04-05 15:42:31 +02:00
Andras Bacsai
0844645a8b version++ 2024-04-05 15:33:33 +02:00
Andras Bacsai
30bfad455c ui: multiple server view 2024-04-05 15:33:29 +02:00
Andras Bacsai
d84fdc3cd5 fix: make sure if envs are changed, rebuild is needed 2024-04-05 15:33:19 +02:00
Andras Bacsai
2b64b9de63 fix: do not rebuild dockerfile based apps twice 2024-04-05 15:33:11 +02:00
Gionata Mettifogo
cdb3d863db Update stirling-pdf.yaml
The service works fine now but shows up as Running (unhealthy). Adding a check on its main page on port 8080 makes it healthy.
2024-04-04 08:38:53 +02:00
Andras Bacsai
74e3524e92 Merge pull request #1917 from coollabsio/next
v4.0.0-beta.249
2024-04-03 15:16:09 +02:00
Andras Bacsai
d31f75d1ec Refactor form inputs and labels 2024-04-03 15:10:21 +02:00
Andras Bacsai
0b34207148 Update form input attributes for login and two-factor-challenge pages 2024-04-03 15:02:11 +02:00
Andras Bacsai
3c2beded68 Refactor login.blade.php layout and add registration and OAuth options 2024-04-03 14:52:01 +02:00
Andras Bacsai
f33fdb3bfd Refactor code and comment out unused imports and code blocks 2024-04-03 14:32:23 +02:00
Andras Bacsai
96a0f29f19 Refactor webhook handling logic and add file change validation 2024-04-03 14:14:13 +02:00
Andras Bacsai
49b3a75a8b Refactor manual webhook response payload 2024-04-03 14:14:06 +02:00
Andras Bacsai
f13fc737f1 Refactor manual webhook handling and add watch path check 2024-04-03 14:08:42 +02:00
Andras Bacsai
b7121c5000 Refactor deployment logic and add watch path check 2024-04-03 14:05:35 +02:00
Andras Bacsai
22a1d3882e feat: able to make rsa/ed ssh keys 2024-04-03 13:45:49 +02:00
Andras Bacsai
82f74e2264 Refactor status component styles 2024-04-03 13:04:21 +02:00
Andras Bacsai
3dd5699cde Update button styles and add helper text 2024-04-03 13:03:13 +02:00
Andras Bacsai
a198bfc5c0 Refactor deployment logic in Github webhook controller 2024-04-02 21:25:09 +02:00
Andras Bacsai
132807b55d fix: always rebuild Dockerfile / dockerimage buildpacks 2024-04-02 20:50:35 +02:00
Andras Bacsai
735081af50 fix: new github app creation 2024-04-02 20:50:12 +02:00
Andras Bacsai
31651aeaab ui: light buttons 2024-04-02 20:35:44 +02:00
Andras Bacsai
7f4230d026 Remove unnecessary code and comments 2024-04-02 15:40:52 +02:00
Andras Bacsai
6333d3fd13 Fix image environment variable bug 2024-04-02 15:40:25 +02:00
Andras Bacsai
fd9dae6e4b Refactor EnvironmentVariable creation logic 2024-04-02 15:40:19 +02:00
Andras Bacsai
db5d7857c8 fix: warning if you use multiple domains for a service 2024-04-02 15:15:43 +02:00
Andras Bacsai
0f4eab3cf2 Fix error handling in loadUnmanagedContainers method 2024-04-02 15:01:50 +02:00
Andras Bacsai
75b9f4fcbf Add search functionality and display active/inactive subscribers in Admin Dashboard 2024-04-02 15:00:01 +02:00
Andras Bacsai
b9b58b8985 Merge branch 'main' into next 2024-04-02 12:35:38 +02:00
Andras Bacsai
64b8aa1c01 Update README.md 2024-04-02 12:33:09 +02:00
Andras Bacsai
6b21dc132d Refactor subscription active check for Stripe 2024-04-01 09:40:39 +02:00
Andras Bacsai
7aca4930db fix: unfunctional server should see resources 2024-03-31 18:38:26 +02:00
Andras Bacsai
7a92ecfa30 fix: async public key loading 2024-03-30 18:58:41 +01:00
Andras Bacsai
171f6f4608 fix: trial users subscription page 2024-03-30 00:23:48 +01:00
Andras Bacsai
d569c8d31f fix: search services 2024-03-28 15:18:05 +01:00
Andras Bacsai
51d716253f feat: watch paths 2024-03-28 15:05:12 +01:00
Andras Bacsai
971b17b364 ui fixes 2024-03-28 12:30:06 +01:00
Darek Wróbel
9616d858cf feat(application): update submodules after git checkout 2024-03-28 09:53:09 +01:00
Andras Bacsai
db5ff7f16d Update CSS and Blade file 2024-03-28 09:25:42 +01:00
Andras Bacsai
5db045f392 Refactor deployment index.blade.php to add flex-col class 2024-03-28 09:20:21 +01:00
Andras Bacsai
42c143d19e ui: backup executions 2024-03-28 09:01:40 +01:00
Andras Bacsai
7c1948ebd9 Merge pull request #1911 from coollabsio/next
v4.0.0-beta.248
2024-03-27 21:25:08 +01:00
Andras Bacsai
9cd15645a2 Update Cloudflare tunnel configuration form 2024-03-27 21:11:28 +01:00
Andras Bacsai
c0a4a5c2f5 Add SSH domain field and save server IP in ConfigureCloudflareTunnels.php 2024-03-27 21:09:42 +01:00
Andras Bacsai
518004afbc fix: ui for cftunnels 2024-03-27 18:54:42 +01:00
Andras Bacsai
833a4b9367 Update dynamic configuration setup and version number 2024-03-27 18:41:19 +01:00
Andras Bacsai
6aa82724b4 Merge pull request #1908 from coollabsio/next
v4.0.0-beta.247
2024-03-27 18:26:44 +01:00
Andras Bacsai
cfbee40ecd Add checkbox for Cloudflare Tunnel 2024-03-27 18:26:17 +01:00
Andras Bacsai
79d589c7a9 wip: automatic cloudflare tunnels 2024-03-27 18:24:24 +01:00
Andras Bacsai
6b82fc3011 version++ 2024-03-27 15:12:59 +01:00
Moinul Moin
d9389c91ee Merge branch 'coollabsio:main' into refactor/ui 2024-03-26 19:05:04 +06:00
Moinul Moin
647376ab3f fix: unintended left padding on sidebar 2024-03-26 11:49:52 +00:00
91 changed files with 1309 additions and 672 deletions

View File

@@ -28,12 +28,12 @@ https://coolify.io/sponsorships
Thank you so much! Thank you so much!
Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)! Special thanks to our biggest sponsor, [CCCareers](https://cccareers.org/)!
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="cccareers logo" width="200"/></a> <a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="cccareers logo" width="200"/></a>
<a href="https://appwrite.io" target="_blank"><img src="./other/logos/appwrite.svg" alt="appwrite logo" width="200"/></a>
## Github Sponsors ($40+) ## Github Sponsors ($40+)
<a href="https://americancloud.com/?utm_source=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a>
<a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a> <a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>
<a href="https://typebot.io/?utm_source=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a> <a href="https://typebot.io/?utm_source=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a>
<a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a> <a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a>

16
SECURITY.md Normal file
View File

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

View File

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

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
class ConfigureCloudflared
{
use AsAction;
public function handle(Server $server, string $cloudflare_token)
{
try {
$config = [
"services" => [
"coolify-cloudflared" => [
"container_name" => "coolify-cloudflared",
"image" => "cloudflare/cloudflared:latest",
"restart" => RESTART_MODE,
"network_mode" => "host",
"command" => "tunnel run",
"environment" => [
"TUNNEL_TOKEN={$cloudflare_token}",
],
],
],
];
$config = Yaml::dump($config, 12, 2);
$docker_compose_yml_base64 = base64_encode($config);
$commands = collect([
"mkdir -p /tmp/cloudflared && cd /tmp/cloudflared",
"echo '$docker_compose_yml_base64' | base64 -d > docker-compose.yml",
"docker compose pull",
"docker compose down -v --remove-orphans > /dev/null 2>&1",
"docker compose up -d --remove-orphans",
]);
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
ray($e);
throw $e;
}
}
}

View File

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

View File

@@ -34,11 +34,13 @@ class Init extends Command
$this->cleanup_stucked_helper_containers(); $this->cleanup_stucked_helper_containers();
$this->call('cleanup:queue'); $this->call('cleanup:queue');
$this->call('cleanup:stucked-resources'); $this->call('cleanup:stucked-resources');
try { if (!isCloud()) {
$server = Server::find(0)->first(); try {
$server->setupDynamicProxyConfiguration(); $server = Server::find(0)->first();
} catch (\Throwable $e) { $server->setupDynamicProxyConfiguration();
echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; } catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
} }
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();

View File

@@ -121,6 +121,9 @@ class Kernel extends ConsoleKernel
return; return;
} }
foreach ($scheduled_tasks as $scheduled_task) { foreach ($scheduled_tasks as $scheduled_task) {
if ($scheduled_task->enabled === false) {
continue;
}
$service = $scheduled_task->service; $service = $scheduled_task->service;
$application = $scheduled_task->application; $application = $scheduled_task->application;

View File

@@ -22,7 +22,6 @@ class Github extends Controller
public function manual(Request $request) public function manual(Request $request)
{ {
try { try {
ray($request);
$return_payloads = collect([]); $return_payloads = collect([]);
$x_github_delivery = request()->header('X-GitHub-Delivery'); $x_github_delivery = request()->header('X-GitHub-Delivery');
if (app()->isDownForMaintenance()) { if (app()->isDownForMaintenance()) {
@@ -68,6 +67,10 @@ class Github extends Controller
if (Str::isMatch('/refs\/heads\/*/', $branch)) { if (Str::isMatch('/refs\/heads\/*/', $branch)) {
$branch = Str::after($branch, 'refs/heads/'); $branch = Str::after($branch, 'refs/heads/');
} }
$added_files = data_get($payload, 'commits.*.added');
$removed_files = data_get($payload, 'commits.*.removed');
$modified_files = data_get($payload, 'commits.*.modified');
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
ray('Manual Webhook GitHub Push Event with branch: ' . $branch); ray('Manual Webhook GitHub Push Event with branch: ' . $branch);
} }
if ($x_github_event === 'pull_request') { if ($x_github_event === 'pull_request') {
@@ -118,24 +121,41 @@ class Github extends Controller
} }
if ($x_github_event === 'push') { if ($x_github_event === 'push') {
if ($application->isDeployable()) { if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch); $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
$deployment_uuid = new Cuid2(7); if ($is_watch_path_triggered || is_null($application->watch_paths)) {
queue_application_deployment( ray('Deploying ' . $application->name . ' with branch ' . $branch);
application: $application, $deployment_uuid = new Cuid2(7);
deployment_uuid: $deployment_uuid, queue_application_deployment(
force_rebuild: false, application: $application,
is_webhook: true, deployment_uuid: $deployment_uuid,
); force_rebuild: false,
$return_payloads->push([ is_webhook: true,
'application' => $application->name, );
'status' => 'success', $return_payloads->push([
'message' => 'Deployment queued.', 'status' => 'success',
]); 'message' => 'Deployment queued.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]);
} else {
$paths = str($application->watch_paths)->explode("\n");
$return_payloads->push([
'status' => 'failed',
'message' => 'Changed files do not match watch paths. Ignoring deployment.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
'details' => [
'changed_files' => $changed_files,
'watch_paths' => $paths,
],
]);
}
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name,
'status' => 'failed', 'status' => 'failed',
'message' => 'Deployments disabled.', 'message' => 'Deployments disabled.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]); ]);
} }
} }
@@ -266,6 +286,10 @@ class Github extends Controller
if (Str::isMatch('/refs\/heads\/*/', $branch)) { if (Str::isMatch('/refs\/heads\/*/', $branch)) {
$branch = Str::after($branch, 'refs/heads/'); $branch = Str::after($branch, 'refs/heads/');
} }
$added_files = data_get($payload, 'commits.*.added');
$removed_files = data_get($payload, 'commits.*.removed');
$modified_files = data_get($payload, 'commits.*.modified');
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
ray('Webhook GitHub Push Event: ' . $id . ' with branch: ' . $branch); ray('Webhook GitHub Push Event: ' . $id . ' with branch: ' . $branch);
} }
if ($x_github_event === 'pull_request') { if ($x_github_event === 'pull_request') {
@@ -298,32 +322,50 @@ class Github extends Controller
$isFunctional = $application->destination->server->isFunctional(); $isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) { if (!$isFunctional) {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name,
'status' => 'failed', 'status' => 'failed',
'message' => 'Server is not functional.', 'message' => 'Server is not functional.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]); ]);
continue; continue;
} }
if ($x_github_event === 'push') { if ($x_github_event === 'push') {
if ($application->isDeployable()) { if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch); $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
$deployment_uuid = new Cuid2(7); if ($is_watch_path_triggered || is_null($application->watch_paths)) {
queue_application_deployment( ray('Deploying ' . $application->name . ' with branch ' . $branch);
application: $application, $deployment_uuid = new Cuid2(7);
deployment_uuid: $deployment_uuid, queue_application_deployment(
force_rebuild: false, application: $application,
is_webhook: true deployment_uuid: $deployment_uuid,
); force_rebuild: false,
$return_payloads->push([ is_webhook: true,
'application' => $application->name, );
'status' => 'success', $return_payloads->push([
'message' => 'Deployment queued.', 'status' => 'success',
]); 'message' => 'Deployment queued.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]);
} else {
$paths = str($application->watch_paths)->explode("\n");
$return_payloads->push([
'status' => 'failed',
'message' => 'Changed files do not match watch paths. Ignoring deployment.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
'details' => [
'changed_files' => $changed_files,
'watch_paths' => $paths,
],
]);
}
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name,
'status' => 'failed', 'status' => 'failed',
'message' => 'Deployments disabled.', 'message' => 'Deployments disabled.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]); ]);
} }
} }

View File

@@ -51,6 +51,10 @@ class Gitlab extends Controller
]); ]);
return response($return_payloads); return response($return_payloads);
} }
$added_files = data_get($payload, 'commits.*.added');
$removed_files = data_get($payload, 'commits.*.removed');
$modified_files = data_get($payload, 'commits.*.modified');
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
ray('Manual Webhook GitLab Push Event with branch: ' . $branch); ray('Manual Webhook GitLab Push Event with branch: ' . $branch);
} }
if ($x_gitlab_event === 'merge_request') { if ($x_gitlab_event === 'merge_request') {
@@ -113,19 +117,41 @@ class Gitlab extends Controller
} }
if ($x_gitlab_event === 'push') { if ($x_gitlab_event === 'push') {
if ($application->isDeployable()) { if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch); $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
$deployment_uuid = new Cuid2(7); if ($is_watch_path_triggered || is_null($application->watch_paths)) {
queue_application_deployment( ray('Deploying ' . $application->name . ' with branch ' . $branch);
application: $application, $deployment_uuid = new Cuid2(7);
deployment_uuid: $deployment_uuid, queue_application_deployment(
force_rebuild: false, application: $application,
is_webhook: true deployment_uuid: $deployment_uuid,
); force_rebuild: false,
is_webhook: true,
);
$return_payloads->push([
'status' => 'success',
'message' => 'Deployment queued.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]);
} else {
$paths = str($application->watch_paths)->explode("\n");
$return_payloads->push([
'status' => 'failed',
'message' => 'Changed files do not match watch paths. Ignoring deployment.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
'details' => [
'changed_files' => $changed_files,
'watch_paths' => $paths,
],
]);
}
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name,
'status' => 'failed', 'status' => 'failed',
'message' => 'Deployments disabled', 'message' => 'Deployments disabled',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]); ]);
ray('Deployments disabled for ' . $application->name); ray('Deployments disabled for ' . $application->name);
} }

View File

@@ -179,6 +179,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
if (!$this->server->isFunctional()) {
$this->application_deployment_queue->addLogEntry("Server is not functional.");
$this->fail("Server is not functional.");
return;
}
try { try {
// Generate custom host<->ip mapping // Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
@@ -234,7 +239,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_server = $this->server; $this->build_server = $this->server;
$this->original_server = $this->server; $this->original_server = $this->server;
} }
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') { if ($this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile') {
$this->just_restart(); $this->just_restart();
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server)); dispatch(new ContainerStatusJob($this->server));
@@ -326,17 +331,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
$this->generate_image_names(); $this->generate_image_names();
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely(); // Always rebuild dockerfile based container.
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { // if (!$this->force_rebuild) {
$this->create_workdir(); // $this->check_image_locally_or_remotely();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); // if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->generate_compose_file(); // $this->create_workdir();
$this->push_to_docker_registry(); // $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->rolling_update(); // $this->generate_compose_file();
return; // $this->push_to_docker_registry();
} // $this->rolling_update();
} // return;
// }
// }
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
@@ -644,21 +651,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
if ($this->application->dockerfile) { if ($this->application->dockerfile) {
if ($this->application->docker_registry_image_name) { if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build"); $this->build_image_name = "{$this->application->docker_registry_image_name}:build";
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest"); $this->production_image_name = "{$this->application->docker_registry_image_name}:latest";
} else { } else {
$this->build_image_name = Str::lower("{$this->application->uuid}:build"); $this->build_image_name = "{$this->application->uuid}:build";
$this->production_image_name = Str::lower("{$this->application->uuid}:latest"); $this->production_image_name = "{$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 = "{$this->dockerImage}:{$this->dockerImageTag}";
} else if ($this->pull_request_id !== 0) { } else if ($this->pull_request_id !== 0) {
if ($this->application->docker_registry_image_name) { if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build"); $this->build_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build";
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}"); $this->production_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}";
} else { } else {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); $this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build";
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); $this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}";
} }
} else { } else {
$this->dockerImageTag = str($this->commit)->substr(0, 128); $this->dockerImageTag = str($this->commit)->substr(0, 128);
@@ -666,11 +673,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->dockerImageTag = $this->application->docker_registry_image_tag; $this->dockerImageTag = $this->application->docker_registry_image_tag;
} }
if ($this->application->docker_registry_image_name) { if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build"); $this->build_image_name = "{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build";
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}"); $this->production_image_name = "{$this->application->docker_registry_image_name}:{$this->dockerImageTag}";
} else { } else {
$this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build"); $this->build_image_name = "{$this->application->uuid}:{$this->dockerImageTag}-build";
$this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}"); $this->production_image_name = "{$this->application->uuid}:{$this->dockerImageTag}";
} }
} }
} }
@@ -999,11 +1006,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return $commands; return $commands;
} }
private function set_git_import_settings($git_clone_command)
{
return $this->application->setGitImportSettings($this->deployment_uuid, $git_clone_command);
}
private function cleanup_git() private function cleanup_git()
{ {
$this->execute_remote_command( $this->execute_remote_command(
@@ -1812,7 +1814,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void public function failed(Throwable $exception): void
{ {
$this->next(ApplicationDeploymentStatus::FAILED->value); $this->next(ApplicationDeploymentStatus::FAILED->value);
$this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr'); $this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
if (str($exception->getMessage())->isNotEmpty()) { if (str($exception->getMessage())->isNotEmpty()) {

View File

@@ -8,7 +8,31 @@ use Livewire\Component;
class Index extends Component class Index extends Component
{ {
public $users = []; public $active_subscribers = [];
public $inactive_subscribers = [];
public $search = '';
public function submitSearch() {
if ($this->search !== "") {
$this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->where(function ($query) {
$query->where('name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%");
})->get()->filter(function ($user) {
return $user->id !== 0;
});
$this->active_subscribers = User::whereHas('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->where(function ($query) {
$query->where('name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%");
})->get()->filter(function ($user) {
return $user->id !== 0;
});
} else {
$this->getSubscribers();
}
}
public function mount() public function mount()
{ {
if (!isCloud()) { if (!isCloud()) {
@@ -17,7 +41,15 @@ class Index extends Component
if (auth()->user()->id !== 0) { if (auth()->user()->id !== 0) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$this->users = User::whereHas('teams', function ($query) { $this->getSubscribers();
}
public function getSubscribers() {
$this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->get()->filter(function ($user) {
return $user->id !== 0;
});
$this->active_subscribers = User::whereHas('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->get()->filter(function ($user) { })->get()->filter(function ($user) {
return $user->id !== 0; return $user->id !== 0;

View File

@@ -73,6 +73,7 @@ class General extends Component
'application.settings.is_static' => 'boolean|required', 'application.settings.is_static' => 'boolean|required',
'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required',
'application.watch_paths' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@@ -108,6 +109,7 @@ class General extends Component
'application.settings.is_static' => 'Is static', 'application.settings.is_static' => 'Is static',
'application.settings.is_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled', 'application.settings.is_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled',
'application.settings.is_build_server_enabled' => 'Is build server enabled', 'application.settings.is_build_server_enabled' => 'Is build server enabled',
'application.watch_paths' => 'Watch paths',
]; ];
public function mount() public function mount()
{ {
@@ -161,22 +163,23 @@ class General extends Component
} }
public function generateDomain(string $serviceName) public function generateDomain(string $serviceName)
{ {
$domain = $this->parsedServiceDomains[$serviceName]['domain'] ?? null; $uuid = new Cuid2(7);
if (!$domain) { $domain = generateFqdn($this->application->destination->server, $uuid);
$uuid = new Cuid2(7); $this->parsedServiceDomains[$serviceName]['domain'] = $domain;
$domain = generateFqdn($this->application->destination->server, $uuid); $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->parsedServiceDomains[$serviceName]['domain'] = $domain; $this->application->save();
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); $this->dispatch('success', 'Domain generated.');
$this->application->save();
}
return $domain; return $domain;
} }
public function updatedApplicationBaseDirectory() { public function updatedApplicationBaseDirectory()
raY('asdf'); {
if ($this->application->build_pack === 'dockercompose') { if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile(); $this->loadComposeFile();
} }
} }
public function updatedApplicationFqdn() {
$this->resetDefaultLabels();
}
public function updatedApplicationBuildPack() public function updatedApplicationBuildPack()
{ {
if ($this->application->build_pack !== 'nixpacks') { if ($this->application->build_pack !== 'nixpacks') {
@@ -204,30 +207,49 @@ class General extends Component
$fqdn = generateFqdn($server, $this->application->uuid); $fqdn = generateFqdn($server, $this->application->uuid);
$this->application->fqdn = $fqdn; $this->application->fqdn = $fqdn;
$this->application->save(); $this->application->save();
$this->updatedApplicationFqdn(); $this->resetDefaultLabels();
$this->dispatch('success', 'Wildcard domain generated.');
} }
} }
public function resetDefaultLabels($showToaster = true) public function resetDefaultLabels()
{ {
ray('resetDefaultLabels');
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->ports_exposes = $this->application->ports_exposes; $this->ports_exposes = $this->application->ports_exposes;
$this->submit($showToaster);
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
} }
public function updatedApplicationFqdn() public function checkFqdns($showToaster = true)
{ {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); if (data_get($this->application, 'fqdn')) {
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $domains = str($this->application->fqdn)->trim()->explode(',');
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { if ($this->application->additional_servers->count() === 0) {
return str($domain)->trim()->lower(); foreach ($domains as $domain) {
}); if (!validate_dns_entry($domain, $this->application->destination->server)) {
$this->application->fqdn = $this->application->fqdn->unique()->implode(','); $showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$this->application->save(); }
$this->resetDefaultLabels(false); }
}
check_domain_usage(resource: $this->application);
$this->application->fqdn = $domains->implode(',');
}
} }
public function submit($showToaster = true) public function submit($showToaster = true)
{ {
try { try {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->checkFqdns();
$this->application->save();
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->application->custom_labels = base64_encode($this->customLabels); $this->application->custom_labels = base64_encode($this->customLabels);
@@ -239,25 +261,14 @@ class General extends Component
} }
$this->validate(); $this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) { if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false); $this->resetDefaultLabels();
} }
if (data_get($this->application, 'build_pack') === 'dockerimage') { if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([ $this->validate([
'application.docker_registry_image_name' => 'required', 'application.docker_registry_image_name' => 'required',
]); ]);
} }
if (data_get($this->application, 'fqdn')) {
$domains = str($this->application->fqdn)->trim()->explode(',');
if ($this->application->additional_servers->count() === 0) {
foreach ($domains as $domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
}
}
check_fqdn_usage($this->application);
$this->application->fqdn = $domains->implode(',');
}
if (data_get($this->application, 'custom_docker_run_options')) { if (data_get($this->application, 'custom_docker_run_options')) {
$this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim(); $this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim();
} }
@@ -275,6 +286,15 @@ class General extends Component
} }
if ($this->application->build_pack === 'dockercompose') { if ($this->application->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
foreach ($this->parsedServiceDomains as $serviceName => $service) {
$domain = data_get($service, 'domain');
if ($domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
check_domain_usage(resource: $this->application);
}
}
if ($this->application->settings->is_raw_compose_deployment_enabled) { if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->parseRawCompose(); $this->application->parseRawCompose();
} else { } else {

View File

@@ -7,14 +7,12 @@ use App\Models\GithubApp;
use App\Models\Project; use App\Models\Project;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Livewire\Component; use Livewire\Component;
class GithubPrivateRepository extends Component class GithubPrivateRepository extends Component
{ {
use SaveFromRedirect;
public $current_step = 'github_apps'; public $current_step = 'github_apps';
public $github_apps; public $github_apps;
public GithubApp $github_app; public GithubApp $github_app;

View File

@@ -70,6 +70,10 @@ class Select extends Component
// } // }
// } // }
public function updatedSearch()
{
$this->loadServices();
}
public function loadServices(bool $force = false) public function loadServices(bool $force = false)
{ {
try { try {

View File

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

View File

@@ -24,6 +24,16 @@ class ServiceApplicationView extends Component
{ {
return view('livewire.project.service.service-application-view'); return view('livewire.project.service.service-application-view');
} }
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
}
public function instantSave() public function instantSave()
{ {
$this->submit(); $this->submit();
@@ -55,11 +65,15 @@ class ServiceApplicationView extends Component
public function submit() public function submit()
{ {
try { try {
check_fqdn_usage($this->application); check_domain_usage(resource: $this->application);
$this->validate(); $this->validate();
$this->application->save(); $this->application->save();
updateCompose($this->application); updateCompose($this->application);
$this->dispatch('success', 'Service saved.'); if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
} else {
$this->dispatch('success', 'Service saved.');
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally { } finally {

View File

@@ -26,7 +26,7 @@ class Create extends Component
'value' => 'private Key', 'value' => 'private Key',
]; ];
public function generateNewKey() public function generateNewRSAKey()
{ {
try { try {
$this->rateLimit(10); $this->rateLimit(10);
@@ -37,6 +37,17 @@ class Create extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function generateNewEDKey()
{
try {
$this->rateLimit(10);
$this->name = generate_random_name();
$this->description = 'Created by Coolify';
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519');
} catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function updated($updateProperty) public function updated($updateProperty)
{ {
if ($updateProperty === 'value') { if ($updateProperty === 'value') {

View File

@@ -8,7 +8,7 @@ use Livewire\Component;
class Show extends Component class Show extends Component
{ {
public PrivateKey $private_key; public PrivateKey $private_key;
public $public_key; public $public_key = "Loading...";
protected $rules = [ protected $rules = [
'private_key.name' => 'required|string', 'private_key.name' => 'required|string',
'private_key.description' => 'nullable|string', 'private_key.description' => 'nullable|string',
@@ -25,11 +25,13 @@ class Show extends Component
{ {
try { try {
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail(); $this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
$this->public_key = $this->private_key->publicKey();
}catch(\Throwable $e) { }catch(\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function loadPublicKey() {
$this->public_key = $this->private_key->publicKey();
}
public function delete() public function delete()
{ {
try { try {

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Server\ConfigureCloudflared;
use App\Models\Server;
use Livewire\Component;
class ConfigureCloudflareTunnels extends Component
{
public $server_id;
public string $cloudflare_token;
public string $ssh_domain;
public function alreadyConfigured()
{
try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
$server->settings->is_cloudflare_tunnel = true;
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
$this->dispatch('serverInstalled');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::run($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain;
$server->save();
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
$this->dispatch('serverInstalled');
} catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.configure-cloudflare-tunnels');
}
}

View File

@@ -42,7 +42,11 @@ class Resources extends Component
$this->dispatch('success', 'Resource statuses refreshed.'); $this->dispatch('success', 'Resource statuses refreshed.');
} }
public function loadUnmanagedContainers() { public function loadUnmanagedContainers() {
$this->unmanagedContainers = $this->server->loadUnmanagedContainers(); try {
$this->unmanagedContainers = $this->server->loadUnmanagedContainers();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function mount() { public function mount() {
$this->unmanagedContainers = collect(); $this->unmanagedContainers = collect();

View File

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

View File

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

View File

@@ -15,7 +15,10 @@ class Index extends Component
if (!isCloud()) { if (!isCloud()) {
return redirect(RouteServiceProvider::HOME); return redirect(RouteServiceProvider::HOME);
} }
if (data_get(currentTeam(), 'subscription')) { if (auth()->user()?->isMember()) {
return redirect()->route('dashboard');
}
if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) {
return redirect()->route('subscription.show'); return redirect()->route('subscription.show');
} }
$this->settings = InstanceSettings::get(); $this->settings = InstanceSettings::get();

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException; use RuntimeException;
@@ -501,9 +502,9 @@ class Application extends BaseModel
{ {
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels; $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
if ($this->pull_request_id === 0 || $this->pull_request_id === null) { if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables()); $newConfigHash .= json_encode($this->environment_variables()->get('updated_at'));
} else { } else {
$newConfigHash .= json_encode($this->environment_variables_preview->all()); $newConfigHash .= json_encode($this->environment_variables_preview->get('updated_at'));
} }
$newConfigHash = md5($newConfigHash); $newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash'); $oldConfigHash = data_get($this, 'config_hash');
@@ -597,19 +598,27 @@ class Application extends BaseModel
} else { } else {
$github_access_token = generate_github_installation_token($this->source); $github_access_token = generate_github_installation_token($this->source);
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}")); $git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git"; $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
} else { } else {
$commands->push("{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}"); $git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}"; $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
} }
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
} }
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name"; $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
$git_checkout_command = $this->buildGitCheckoutCommand($pr_branch_name);
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name")); $commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command"));
} else { } else {
$commands->push("cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name"); $commands->push("cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command");
} }
} }
return [ return [
@@ -653,7 +662,7 @@ class Application extends BaseModel
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'github') { } else if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name"; $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) { if ($exec_in_docker) {
@@ -661,14 +670,14 @@ class Application extends BaseModel
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'bitbucket') { } else if ($git_type === 'bitbucket') {
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git checkout $commit"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
} }
} }
@@ -696,7 +705,7 @@ class Application extends BaseModel
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'github') { } else if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name"; $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) { if ($exec_in_docker) {
@@ -704,14 +713,14 @@ class Application extends BaseModel
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} else if ($git_type === 'bitbucket') { } else if ($git_type === 'bitbucket') {
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else { } else {
$commands->push("echo 'Checking out $branch'"); $commands->push("echo 'Checking out $branch'");
} }
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git checkout $commit"; $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
} }
} }
@@ -903,4 +912,37 @@ class Application extends BaseModel
: explode(',', $this->fqdn), : explode(',', $this->fqdn),
); );
} }
protected function buildGitCheckoutCommand($target): string
{
$command = "git checkout $target";
if ($this->settings->is_git_submodules_enabled) {
$command .= " && git submodule update --init --recursive";
}
return $command;
}
public function watchPaths(): Attribute
{
return Attribute::make(
set: function ($value) {
if ($value) {
return trim($value);
}
}
);
}
public function isWatchPathsTriggered(Collection $modified_files): bool
{
if (is_null($this->watch_paths)) {
return false;
}
$watch_paths = collect(explode("\n", $this->watch_paths));
$matches = $modified_files->filter(function ($file) use ($watch_paths) {
return $watch_paths->contains(function ($glob) use ($file) {
return fnmatch($glob, $file);
});
});
return $matches->count() > 0;
}
} }

View File

@@ -25,19 +25,18 @@ class EnvironmentVariable extends Model
static::created(function (EnvironmentVariable $environment_variable) { static::created(function (EnvironmentVariable $environment_variable) {
if ($environment_variable->application_id && !$environment_variable->is_preview) { if ($environment_variable->application_id && !$environment_variable->is_preview) {
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first(); $found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first();
$application = Application::find($environment_variable->application_id);
if ($application->build_pack === 'dockerfile') {
return;
}
if (!$found) { if (!$found) {
ModelsEnvironmentVariable::create([ $application = Application::find($environment_variable->application_id);
'key' => $environment_variable->key, if ($application->build_pack !== 'dockerfile') {
'value' => $environment_variable->value, ModelsEnvironmentVariable::create([
'is_build_time' => $environment_variable->is_build_time, 'key' => $environment_variable->key,
'is_multiline' => $environment_variable->is_multiline, 'value' => $environment_variable->value,
'application_id' => $environment_variable->application_id, 'is_build_time' => $environment_variable->is_build_time,
'is_preview' => true 'is_multiline' => $environment_variable->is_multiline,
]); 'application_id' => $environment_variable->application_id,
'is_preview' => true
]);
}
} }
} }
$environment_variable->update([ $environment_variable->update([

View File

@@ -690,7 +690,13 @@ $schema://$host {
} }
public function isFunctional() public function isFunctional()
{ {
return $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled; $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled;
['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this);
if (!$isFunctional) {
Storage::disk('ssh-keys')->delete($private_key_filename);
Storage::disk('ssh-mux')->delete($mux_filename);
}
return $isFunctional;
} }
public function isLogDrainEnabled() public function isLogDrainEnabled()
{ {

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use Illuminate\Support\Collection;
use Spatie\Url\Url; use Spatie\Url\Url;
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null, bool $only_this_server = false) function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null, bool $only_this_server = false)

View File

@@ -37,7 +37,7 @@ const SPECIFIC_SERVICES = [
// Based on /etc/os-release // Based on /etc/os-release
const SUPPORTED_OS = [ const SUPPORTED_OS = [
'ubuntu debian raspbian', 'ubuntu debian raspbian',
'centos fedora rhel ol rocky', 'centos fedora rhel ol rocky amzn',
'sles opensuse-leap opensuse-tumbleweed' 'sles opensuse-leap opensuse-tumbleweed'
]; ];

View File

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

View File

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

View File

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

View File

@@ -39,6 +39,7 @@ use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Token\Builder; use Lcobucci\JWT\Token\Builder;
use phpseclib3\Crypt\EC;
use Poliander\Cron\CronExpression; use Poliander\Cron\CronExpression;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\RSA;
@@ -165,13 +166,22 @@ function generate_random_name(?string $cuid = null): string
} }
return Str::kebab("{$generator->getName()}-$cuid"); return Str::kebab("{$generator->getName()}-$cuid");
} }
function generateSSHKey() function generateSSHKey(string $type = 'rsa')
{ {
$key = RSA::createKey(); if ($type === 'rsa') {
return [ $key = RSA::createKey();
'private' => $key->toString('PKCS1'), return [
'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key']) 'private' => $key->toString('PKCS1'),
]; 'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key'])
];
} else if ($type === 'ed25519') {
$key = EC::createKey('Ed25519');
return [
'private' => $key->toString('OpenSSH'),
'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key'])
];
}
throw new Exception('Invalid key type');
} }
function formatPrivateKey(string $privateKey) function formatPrivateKey(string $privateKey)
{ {
@@ -282,7 +292,7 @@ function base_url(bool $withPort = true): string
function isSubscribed() function isSubscribed()
{ {
return auth()->user()->currentTeam()->subscription()->exists() || auth()->user()->isInstanceAdmin(); return isSubscriptionActive() || auth()->user()->isInstanceAdmin();
} }
function isDev(): bool function isDev(): bool
{ {
@@ -433,7 +443,7 @@ function sslip(Server $server)
function getServiceTemplates() 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();
} else { } else {
@@ -1158,6 +1168,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// }); // });
// ray($withoutServiceEnvs); // ray($withoutServiceEnvs);
// data_set($service, 'environment', $withoutServiceEnvs->toArray()); // data_set($service, 'environment', $withoutServiceEnvs->toArray());
updateCompose($savedService);
return $service; return $service;
}); });
$finalServices = [ $finalServices = [
@@ -1846,13 +1857,26 @@ function ip_match($ip, $cidrs, &$match = null)
} }
return false; return false;
} }
function check_fqdn_usage(ServiceApplication|Application $own_resource) function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
{ {
$domains = collect($own_resource->fqdns)->map(function ($domain) { if ($resource) {
if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') {
$domains = data_get(json_decode($resource->docker_compose_domains, true), "*.domain");
ray($domains);
$domains = collect($domains);
} else {
$domains = collect($resource->fqdns);
}
} else if ($domain) {
$domains = collect($domain);
} else {
throw new \RuntimeException("No resource or FQDN provided.");
}
$domains = $domains->map(function ($domain) {
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/'); $domain = str($domain)->beforeLast('/');
} }
return str($domain)->replace('http://', '')->replace('https://', ''); return str($domain);
}); });
$apps = Application::all(); $apps = Application::all();
foreach ($apps as $app) { foreach ($apps as $app) {
@@ -1861,10 +1885,15 @@ function check_fqdn_usage(ServiceApplication|Application $own_resource)
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/'); $domain = str($domain)->beforeLast('/');
} }
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value(); $naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) { if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) { if (data_get($resource, 'uuid')) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource:<br> {$app->name}."); ray($resource->uuid, $app->uuid);
if ($resource->uuid !== $app->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
}
} else if ($domain) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
} }
} }
} }
@@ -1876,12 +1905,29 @@ function check_fqdn_usage(ServiceApplication|Application $own_resource)
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/'); $domain = str($domain)->beforeLast('/');
} }
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value(); $naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) { if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) { if (data_get($resource, 'uuid')) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource."); if ($resource->uuid !== $app->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
}
} else if ($domain) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
} }
} }
} }
} }
if ($resource) {
$settings = InstanceSettings::get();
if (data_get($settings, 'fqdn')) {
$domain = data_get($settings, 'fqdn');
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance.");
}
}
}
} }

View File

@@ -66,7 +66,7 @@ function isSubscriptionActive()
// return $subscription->paddle_status === 'active'; // return $subscription->paddle_status === 'active';
// } // }
if (isStripe()) { if (isStripe()) {
return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false; return $subscription->stripe_invoice_paid === true;
} }
return false; return false;
} }

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.246', 'release' => '4.0.0-beta.255',
// 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

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.246'; return '4.0.0-beta.255';

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->longText('watch_paths')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('watch_paths');
});
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->text('custom_docker_run_options')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('custom_docker_run_options')->nullable()->change();
});
}
};

34
package-lock.json generated
View File

@@ -6,8 +6,8 @@
"": { "": {
"dependencies": { "dependencies": {
"@tailwindcss/forms": "0.5.7", "@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.12",
"alpinejs": "3.13.7", "alpinejs": "3.13.8",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
}, },
@@ -19,8 +19,8 @@
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.38", "postcss": "8.4.38",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.3",
"vite": "4.5.2", "vite": "4.5.3",
"vue": "3.4.21" "vue": "3.4.21"
} }
}, },
@@ -496,9 +496,9 @@
} }
}, },
"node_modules/@tailwindcss/typography": { "node_modules/@tailwindcss/typography": {
"version": "0.5.10", "version": "0.5.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.12.tgz",
"integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", "integrity": "sha512-CNwpBpconcP7ppxmuq3qvaCxiRWnbhANpY/ruH4L5qs2GCiVDJXde/pjj2HWPV1+Q4G9+V/etrwUYopdcjAlyg==",
"dependencies": { "dependencies": {
"lodash.castarray": "^4.4.0", "lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6", "lodash.isplainobject": "^4.0.6",
@@ -683,9 +683,9 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
}, },
"node_modules/alpinejs": { "node_modules/alpinejs": {
"version": "3.13.7", "version": "3.13.8",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.7.tgz", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.8.tgz",
"integrity": "sha512-rcTyjTANbsePq1hb7eSekt3qjI94HLGeO6JaRjCssCVbIIc+qBrc7pO5S/+2JB6oojIibjM6FA+xRI3zhGPZIg==", "integrity": "sha512-XolbBJryCndomtaHd/KHQjQeD/L72FJxy/YhLLFD4Lr7zzGcpcbg+UgXteMR2pYg1KhRUr6V4O3GfN1zJAmRWw==",
"dependencies": { "dependencies": {
"@vue/reactivity": "~3.1.1" "@vue/reactivity": "~3.1.1"
} }
@@ -1900,9 +1900,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.1", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
"integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@@ -1912,7 +1912,7 @@
"fast-glob": "^3.3.0", "fast-glob": "^3.3.0",
"glob-parent": "^6.0.2", "glob-parent": "^6.0.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"jiti": "^1.19.1", "jiti": "^1.21.0",
"lilconfig": "^2.1.0", "lilconfig": "^2.1.0",
"micromatch": "^4.0.5", "micromatch": "^4.0.5",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
@@ -2020,9 +2020,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.2", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",

View File

@@ -13,14 +13,14 @@
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.38", "postcss": "8.4.38",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.3",
"vite": "4.5.2", "vite": "4.5.3",
"vue": "3.4.21" "vue": "3.4.21"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/forms": "0.5.7", "@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.12",
"alpinejs": "3.13.7", "alpinejs": "3.13.8",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
} }

View File

@@ -13,7 +13,7 @@ body {
.input, .input,
.select { .select {
@apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-300 dark:ring-coolgray-300; @apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300;
} }
/* Readonly */ /* Readonly */
@@ -41,7 +41,7 @@ option {
} }
.button { .button {
@apply flex items-center justify-center gap-2 px-3 py-1 text-sm text-white normal-case rounded cursor-pointer hover:bg-black/80 bg-coolgray-200 hover:bg-coolgray-500 hover:text-white disabled:bg-coolgray-100/10 disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600; @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-black hover:text-black disabled:bg-coolgray-100/10 disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600;
} }
button[isError]:not(:disabled) { button[isError]:not(:disabled) {
@@ -78,7 +78,7 @@ label {
} }
table { table {
@apply min-w-full divide-y dark:divide-coolgray-200 divide-neutral-300; @apply min-w-full divide-y dark:divide-coolgray-200 divide-neutral-300 ;
} }
thead { thead {
@@ -90,7 +90,7 @@ tbody {
} }
tr { tr {
@apply text-neutral-400; @apply text-black dark:text-neutral-400 dark:hover:bg-black hover:bg-neutral-200;
} }
tr th { tr th {
@@ -195,7 +195,7 @@ tr td:first-child {
} }
.box { .box {
@apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 bg-white border border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:no-underline; @apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 bg-white border text-black dark:text-white hover:text-black border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:no-underline;
} }
.box-boarding { .box-boarding {
@apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 dark:text-white bg-neutral-50 border border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:text-black hover:no-underline text-black ; @apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 dark:text-white bg-neutral-50 border border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:text-black hover:no-underline text-black ;
@@ -203,6 +203,9 @@ tr td:first-child {
.box-without-bg { .box-without-bg {
@apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] border border-neutral-200 dark:border-black; @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] border border-neutral-200 dark:border-black;
} }
.box-without-bg-without-border {
@apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] ;
}
.on-box { .on-box {
@apply rounded hover:bg-neutral-300 dark:hover:bg-coolgray-500/20; @apply rounded hover:bg-neutral-300 dark:hover:bg-coolgray-500/20;

View File

@@ -1,6 +1,6 @@
import { createApp } from "vue"; // import { createApp } from "vue";
import MagicBar from "./components/MagicBar.vue"; // import MagicBar from "./components/MagicBar.vue";
const app = createApp({}); // const app = createApp({});
app.component("magic-bar", MagicBar); // app.component("magic-bar", MagicBar);
app.mount("#vue"); // app.mount("#vue");

View File

@@ -4,51 +4,31 @@
<a class="flex items-center mb-6 text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white"> <a class="flex items-center mb-6 text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Coolify Coolify
</a> </a>
<div <div class="w-full bg-white shadow md:mt-0 sm:max-w-md xl:p-0 dark:bg-base ">
class="w-full bg-white shadow md:mt-0 sm:max-w-md xl:p-0 dark:bg-base ">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8"> <div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<form action="/login" method="POST" class="flex flex-col gap-2"> <form action="/login" method="POST" class="flex flex-col gap-2">
@csrf @csrf
@env('local') @env('local')
<x-forms.input value="test@example.com" type="email" name="email" required <x-forms.input value="test@example.com" type="email" autocomplete="email" name="email"
label="{{ __('input.email') }}" autofocus /> required label="{{ __('input.email') }}" autofocus />
<x-forms.input value="password" type="password" name="password" required <x-forms.input value="password" type="password" autocomplete="current-password" name="password"
label="{{ __('input.password') }}" /> required label="{{ __('input.password') }}" />
<a href="/forgot-password" class="text-xs"> <a href="/forgot-password" class="text-xs">
{{ __('auth.forgot_password') }}? {{ __('auth.forgot_password') }}?
</a> </a>
@else @else
<x-forms.input type="email" name="email" required label="{{ __('input.email') }}" <x-forms.input type="email" name="email" autocomplete="email" required
autofocus /> label="{{ __('input.email') }}" autofocus />
<x-forms.input type="password" name="password" required label="{{ __('input.password') }}" /> <x-forms.input type="password" name="password" autocomplete="current-password" required
label="{{ __('input.password') }}" />
<a href="/forgot-password" class="text-xs"> <a href="/forgot-password" class="text-xs">
{{ __('auth.forgot_password') }}? {{ __('auth.forgot_password') }}?
</a> </a>
@endenv @endenv
<x-forms.button class="mt-10" type="submit">{{ __('auth.login') }}</x-forms.button> <x-forms.button class="mt-10" type="submit">{{ __('auth.login') }}</x-forms.button>
@if ($is_registration_enabled)
<a href="/register" class="button bg-coollabs-gradient">
{{ __('auth.register_now') }}
</a>
@endif
@if ($enabled_oauth_providers->isNotEmpty())
<div class="relative">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
<div class="w-full border-t dark:border-coolgray-200"></div>
</div>
<div class="relative flex justify-center">
<span class="px-2 text-sm dark:text-neutral-500 dark:bg-base">or</span>
</div>
</div>
@endif
@foreach ($enabled_oauth_providers as $provider_setting)
<x-forms.button type="button"
onclick="document.location.href='/auth/{{ $provider_setting->provider }}/redirect'">
{{ __("auth.login.$provider_setting->provider") }}
</x-forms.button>
@endforeach
@if (!$is_registration_enabled) @if (!$is_registration_enabled)
<div class="text-center text-neutral-500">{{ __('auth.registration_disabled') }}</div> <div class="text-center text-neutral-500">{{ __('auth.registration_disabled') }}</div>
@endif @endif
@@ -70,6 +50,27 @@
</div> </div>
@endif @endif
</form> </form>
@if ($is_registration_enabled)
<a href="/register" class="button bg-coollabs-gradient">
{{ __('auth.register_now') }}
</a>
@endif
@if ($enabled_oauth_providers->isNotEmpty())
<div class="relative">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
<div class="w-full border-t dark:border-coolgray-200"></div>
</div>
<div class="relative flex justify-center">
<span class="px-2 text-sm dark:text-neutral-500 dark:bg-base">or</span>
</div>
</div>
@endif
@foreach ($enabled_oauth_providers as $provider_setting)
<x-forms.button class="w-full" type="button"
onclick="document.location.href='/auth/{{ $provider_setting->provider }}/redirect'">
{{ __("auth.login.$provider_setting->provider") }}
</x-forms.button>
@endforeach
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,42 +1,40 @@
<x-layout-simple> <x-layout-simple>
<div class="flex items-center justify-center h-screen"> <section class="bg-gray-50 dark:bg-base" x-data="{ showRecovery: false }">
<div> <div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="flex flex-col items-center pb-8"> <a class="flex items-center mb-6 text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
<div class="text-5xl font-bold tracking-tight text-center dark:text-white">Coolify</div> Coolify
{{-- <x-version /> --}} </a>
</div> <div class="w-full bg-white shadow md:mt-0 sm:max-w-md xl:p-0 dark:bg-base ">
<div class="w-96"> <div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<form action="/two-factor-challenge" method="POST" class="flex flex-col gap-2"> <form action="/two-factor-challenge" method="POST" class="flex flex-col gap-2">
@csrf @csrf
<div> <div>
<x-forms.input autofocus type="number" name="code" label="{{ __('input.code') }}" autofocus /> <x-forms.input type="number" name="code" autocomplete="one-time-code" label="{{ __('input.code') }}" autofocus />
{{-- <div class="pt-2 text-xs cursor-pointer hover:underline hover:dark:text-white" <div x-show="!showRecovery"
x-on:click="showRecovery = !showRecovery">Use class="pt-2 text-xs cursor-pointer hover:underline hover:dark:text-white"
Recovery Code x-on:click="showRecovery = !showRecovery">Enter
</div> --}} Recovery Code
</div> </div>
<div> </div>
<x-forms.input name="recovery_code" label="{{ __('input.recovery_code') }}" /> <div x-show="showRecovery" x-cloak>
{{-- <div class="pt-2 text-xs cursor-pointer hover:underline hover:dark:text-white" <x-forms.input name="recovery_code" label="{{ __('input.recovery_code') }}" />
x-on:click="showRecovery = !showRecovery">Use </div>
One-Time Code <x-forms.button type="submit">{{ __('auth.login') }}</x-forms.button>
</div> --}} </form>
</div> @if ($errors->any())
<x-forms.button type="submit">{{ __('auth.login') }}</x-forms.button> <div class="text-xs text-center text-error">
</form> @foreach ($errors->all() as $error)
@if ($errors->any()) <p>{{ $error }}</p>
<div class="text-xs text-center text-error"> @endforeach
@foreach ($errors->all() as $error) </div>
<p>{{ $error }}</p> @endif
@endforeach @if (session('status'))
</div> <div class="mb-4 font-medium text-green-600">
@endif {{ session('status') }}
@if (session('status')) </div>
<div class="mb-4 font-medium text-green-600"> @endif
{{ session('status') }} </div>
</div>
@endif
</div> </div>
</div> </div>
</div> </section>
</x-layout-simple> </x-layout-simple>

View File

@@ -3,7 +3,7 @@
'w-full' => !$isMultiline, 'w-full' => !$isMultiline,
])> ])>
@if ($label) @if ($label)
<label for="small-input" class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }} <label class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
@if ($required) @if ($required)
<x-highlighted text="*" /> <x-highlighted text="*" />
@endif @endif

View File

@@ -1,6 +1,6 @@
<div class="w-full"> <div class="w-full">
@if ($label) @if ($label)
<label for="small-input" class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }} <label class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
@if ($required) @if ($required)
<x-highlighted text="*" /> <x-highlighted text="*" />
@endif @endif

View File

@@ -1,6 +1,6 @@
<div class="flex-1 form-control"> <div class="flex-1 form-control">
@if ($label) @if ($label)
<label for="small-input" class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }} <label class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
@if ($required) @if ($required)
<x-highlighted text="*" /> <x-highlighted text="*" />
@endif @endif

View File

@@ -1 +1,3 @@
<img class="inline-flex w-4 h-4" src="{{ asset('svgs/internal-link.svg') }}"> <svg class="inline-flex w-4 h-4 text-black dark:text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-6 6l6-6m-6-6l6 6"/>
</svg>

Before

Width:  |  Height:  |  Size: 78 B

After

Width:  |  Height:  |  Size: 272 B

View File

@@ -1,4 +1,4 @@
<nav class="flex flex-col flex-1 pl-2 bg-white border-r dark:border-coolgray-200 dark:bg-base" x-data="{ <nav class="flex flex-col flex-1 bg-white border-r dark:border-coolgray-200 dark:bg-base" x-data="{
switchWidth() { switchWidth() {
if (this.full === 'full') { if (this.full === 'full') {
localStorage.removeItem('pageWidth'); localStorage.removeItem('pageWidth');

View File

@@ -48,7 +48,7 @@
<div class="pb-10 text-xl text-center">For the detailed list of features, please visit our landing page: <a <div class="pb-10 text-xl text-center">For the detailed list of features, please visit our landing page: <a
class="font-bold underline dark:text-white" href="https://coolify.io">coolify.io</a></div> class="font-bold underline dark:text-white" href="https://coolify.io">coolify.io</a></div>
<div <div
class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-3 lg:divide-x lg:divide-y-0 xl:-mx-4"> class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-3 lg:divide-x lg:divide-y-0 xl:-mx-4">
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14"> <div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
<h3 id="tier-basic" class="text-base font-semibold leading-7 dark:text-white">Basic</h3> <h3 id="tier-basic" class="text-base font-semibold leading-7 dark:text-white">Basic</h3>
@@ -186,7 +186,7 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14"> <div class="pt-16 lg:px-8 lg:pt-0 xl:px-12">
<h3 id="tier-ultimate" class="text-base font-semibold leading-7 dark:text-white">Ultimate</h3> <h3 id="tier-ultimate" class="text-base font-semibold leading-7 dark:text-white">Ultimate</h3>
<p class="flex items-baseline mt-6 gap-x-1"> <p class="flex items-baseline mt-6 gap-x-1">
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>

View File

@@ -8,7 +8,7 @@
<x-status.stopped :status="$resource->status" /> <x-status.stopped :status="$resource->status" />
@endif @endif
@if (!str($resource->status)->contains('exited') && $showRefreshButton) @if (!str($resource->status)->contains('exited') && $showRefreshButton)
<button title="Refresh Status" wire:click='check_status(true)' class="mx-1 dark:hover:fill-white fill-coollabs dark:fill-warning"> <button title="Refresh Status" wire:click='check_status(true)' class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" /> d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />

View File

@@ -1,11 +1,11 @@
@props([ @props([
'status' => 'Restarting', 'status' => 'Restarting',
]) ])
<div class="flex items-center" > <div class="flex items-center">
<x-loading wire:loading.delay.longer /> <x-loading wire:loading.delay.longer />
<span wire:loading.remove.delay.longer class="flex items-center"> <span wire:loading.remove.delay.longer class="flex items-center">
<div class="badge badge-warning "></div> <div class="badge badge-warning "></div>
<div class="pl-2 pr-1 text-xs font-bold tracking-widerr dark:text-warning"> <div class="pl-2 pr-1 text-xs font-bold tracking-wider dark:text-warning">
{{ str($status)->before(':')->headline() }} {{ str($status)->before(':')->headline() }}
</div> </div>
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('(')) @if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))

View File

@@ -1,7 +1,7 @@
@props([ @props([
'status' => 'Running', 'status' => 'Running',
]) ])
<div class="flex items-center" > <div class="flex items-center">
<x-loading wire:loading.delay.longer /> <x-loading wire:loading.delay.longer />
<span wire:loading.remove.delay.longer class="flex items-center"> <span wire:loading.remove.delay.longer class="flex items-center">
<div class="badge badge-success "></div> <div class="badge badge-success "></div>

View File

@@ -8,7 +8,7 @@
<x-status.stopped :status="$complexStatus" /> <x-status.stopped :status="$complexStatus" />
@endif @endif
@if (!str($complexStatus)->contains('exited') && $showRefreshButton) @if (!str($complexStatus)->contains('exited') && $showRefreshButton)
<button title="Refresh Status" wire:click='check_status(true)' class="mx-1 hover:fill-white fill-warning"> <button title="Refresh Status" wire:click='check_status(true)' class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" /> d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />

View File

@@ -1,5 +1,6 @@
@extends('layouts.base') @extends('layouts.base')
@section('body') @section('body')
@livewireScripts
<main class="h-full bg-gray-50 dark:bg-base"> <main class="h-full bg-gray-50 dark:bg-base">
{{ $slot }} {{ $slot }}
</main> </main>

View File

@@ -1,17 +1,31 @@
<div> <div>
<h1>Admin Dashboard</h1> <h1>Admin Dashboard</h1>
<h3 class="pt-4">Who am I now?</h3> <h3 class="pt-4">Who am I now?</h3>
{{ auth()->user()->name }} <div class="pb-4">{{ auth()->user()->name }}</div>
<h3 class="pt-4">Users</h3> <form wire:submit="submitSearch" class="flex flex-col gap-2 lg:flex-row">
<x-forms.input wire:model="search" placeholder="Search for a user" />
<x-forms.button type="submit">Search</x-forms.button>
</form>
<h3 class="pt-4">Active Subscribers</h3>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<div class="dark:text-white cursor-pointer w-96 box-without-bg bg-coollabs-100" wire:click="switchUser('0')"> @forelse ($active_subscribers as $user)
Root <div class="flex gap-2 box" wire:click="switchUser('{{ $user->id }}')">
</div>
@foreach ($users as $user)
<div class="w-96 box" wire:click="switchUser('{{ $user->id }}')">
<p>{{ $user->name }}</p> <p>{{ $user->name }}</p>
<p>{{ $user->email }}</p> <p>{{ $user->email }}</p>
</div> </div>
@endforeach @empty
<p>No active subscribers</p>
@endforelse
</div>
<h3 class="pt-4">Inactive Subscribers</h3>
<div class="flex flex-col flex-wrap gap-2">
@forelse ($inactive_subscribers as $user)
<div class="flex gap-2 box" wire:click="switchUser('{{ $user->id }}')">
<p>{{ $user->name }}</p>
<p>{{ $user->email }}</p>
</div>
@empty
<p>No inactive subscribers</p>
@endforelse
</div> </div>
</div> </div>

View File

@@ -19,22 +19,17 @@
@if ($projects->count() > 0) @if ($projects->count() > 0)
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2"> <div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
@foreach ($projects as $project) @foreach ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group"> <div class="gap-2 border border-transparent cursor-pointer box group"
@if (data_get($project, 'environments')->count() === 1) @if (data_get($project, 'environments')->count() === 1) onclick="gotoProject('{{ data_get($project, 'uuid') }}', '{{ data_get($project, 'environments.0.name', 'production') }}')"
<a class="flex flex-col justify-center flex-1 mx-6" @else
href="{{ route('project.resource.index', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}"> onclick="window.location.href = '{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}'" @endif>
<div class="box-title">{{ $project->name }}</div> <div class="flex flex-col justify-center flex-1 mx-6">
<div class="box-description"> {{ $project->description }}</div> <div class="box-title">{{ $project->name }}</div>
</a> <div class="box-description">
@else {{ $project->description }}</div>
<a class="flex flex-col justify-center flex-1 mx-6" </div>
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}"> <span
<div class="box-title">{{ $project->name }}</div> class="flex items-center justify-center gap-2 pt-4 pb-2 mr-4 text-xs lg:py-0 lg:justify-normal">
<div class="box-description">
{{ $project->description }}</div>
</a>
@endif
<div class="flex items-center justify-center gap-2 pt-4 pb-2 mr-4 text-xs lg:py-0 lg:justify-normal">
<a class="hover:underline" <a class="hover:underline"
href="{{ route('project.resource.create', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}"> href="{{ route('project.resource.create', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<span class="p-2 font-bold">+ <span class="p-2 font-bold">+
@@ -44,7 +39,7 @@
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
Settings Settings
</a> </a>
</div> </span>
</div> </div>
@endforeach @endforeach
</div> </div>

View File

@@ -19,8 +19,8 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<x-forms.input id="current_password" label="Current Password" required type="password" /> <x-forms.input id="current_password" label="Current Password" required type="password" />
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input id="new_password" label="New Password" required type="password" /> <x-forms.input id="new_password" label="New Password" required type="password" />
<x-forms.input id="new_password_confirmation" label="New Password Again" required type="password" /> <x-forms.input id="new_password_confirmation" label="New Password Again" required type="password" />
</div> </div>
</div> </div>
</form> </form>

View File

@@ -27,7 +27,7 @@
@endif @endif
@forelse ($deployments as $deployment) @forelse ($deployments as $deployment)
<a @class([ <a @class([
'dark:bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline box-without-bg bg-white', 'dark:bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline box-without-bg-without-border bg-white flex-col',
'dark:hover:bg-coolgray-200' => 'dark:hover:bg-coolgray-200' =>
data_get($deployment, 'status') === 'queued', data_get($deployment, 'status') === 'queued',
'border-warning hover:bg-warning hover:text-black' => 'border-warning hover:bg-warning hover:text-black' =>

View File

@@ -81,15 +81,14 @@
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" /> d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
</g> </g>
</svg></button> </svg></button>
<div id="logs" class="flex flex-col"> <div id="logs" class="flex flex-col font-mono">
@if (decode_remote_command_output($application_deployment_queue)->count() > 0) @if (decode_remote_command_output($application_deployment_queue)->count() > 0)
@foreach (decode_remote_command_output($application_deployment_queue) as $line) @foreach (decode_remote_command_output($application_deployment_queue) as $line)
<div @class([ <span @class([
'font-mono', 'dark:text-warning' => $line['hidden'],
'dark:text-warning whitespace-pre-line' => $line['hidden'], 'text-red-500 font-bold' => $line['type'] == 'stderr',
'text-red-500 whitespace-pre-line' => $line['type'] == 'stderr',
])>[{{ $line['timestamp'] }}] @if ($line['hidden']) ])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
<br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT: <br>COMMAND: {{ $line['command'] }}<br>OUTPUT :
@endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://')) @endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://'))
@php @php
$line['output'] = preg_replace( $line['output'] = preg_replace(
@@ -101,7 +100,7 @@
@else @else
{{ $line['output'] }} {{ $line['output'] }}
@endif @endif
</div> </span>
@endforeach @endforeach
@else @else
<span class="font-mono text-neutral-400">No logs yet.</span> <span class="font-mono text-neutral-400">No logs yet.</span>

View File

@@ -59,10 +59,8 @@
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. " 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. "
label="Domains for {{ str($serviceName)->headline() }}" label="Domains for {{ str($serviceName)->headline() }}"
id="parsedServiceDomains.{{ $serviceName }}.domain"></x-forms.input> id="parsedServiceDomains.{{ $serviceName }}.domain"></x-forms.input>
@if (!data_get($parsedServiceDomains, "$serviceName.domain")) <x-forms.button wire:click="generateDomain('{{ $serviceName }}')">Generate
<x-forms.button wire:click="generateDomain('{{ $serviceName }}')">Generate Domain</x-forms.button>
Domain</x-forms.button>
@endif
</div> </div>
@endif @endif
@endforeach @endforeach
@@ -147,8 +145,9 @@
<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.start_command" label="Start Command" /> id="application.start_command" label="Start Command" />
</div> </div>
<div>Nixpacks will detect the required configuration automatically. <div class="pb-4 text-xs">Nixpacks will detect the required configuration automatically.
<a class="underline" href="https://coolify.io/docs/resources/introduction">Framework Specific Docs</a> <a class="underline" href="https://coolify.io/docs/resources/introduction">Framework
Specific Docs</a>
</div> </div>
@endif @endif
@endif @endif
@@ -201,10 +200,14 @@
label="Publish Directory" /> label="Publish Directory" />
@endif @endif
@endif @endif
</div> </div>
<div>The following options are for advanced use cases. Only modify them if you @if ($this->application->is_github_based() && !$this->application->is_public_repository())
know what are <div class="pb-4">
you doing.</div> <x-forms.textarea helper="Gitignore-style rules to filter Git based webhook deployments."
placeholder="src/pages/**" id="application.watch_paths" label="Watch Paths" />
</div>
@endif
<x-forms.input <x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>" helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k" placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"

View File

@@ -25,14 +25,16 @@
</div> </div>
<div class="pb-4">Code source of your application.</div> <div class="pb-4">Code source of your application.</div>
<div class="flex gap-2"> <div class="flex flex-col gap-2">
<x-forms.input placeholder="coollabsio/coolify-example" id="application.git_repository" <div class="flex gap-2">
label="Repository" /> <x-forms.input placeholder="coollabsio/coolify-example" id="application.git_repository"
<x-forms.input placeholder="main" id="application.git_branch" label="Branch" /> label="Repository" />
</div> <x-forms.input placeholder="main" id="application.git_branch" label="Branch" />
<div class="flex items-end gap-2"> </div>
<x-forms.input placeholder="HEAD" id="application.git_commit_sha" placeholder="HEAD" label="Commit SHA" /> <div class="flex items-end gap-2">
<x-forms.input placeholder="HEAD" id="application.git_commit_sha" placeholder="HEAD"
label="Commit SHA" />
</div>
</div> </div>
@isset($application->private_key_id) @isset($application->private_key_id)
<h3 class="pt-4">Deploy Key</h3> <h3 class="pt-4">Deploy Key</h3>

View File

@@ -1,7 +1,7 @@
<div class="flex flex-col-reverse gap-2"> <div class="flex flex-col-reverse gap-2">
@forelse($executions as $execution) @forelse($executions as $execution)
<form wire:key="{{ data_get($execution, 'id') }}" <form wire:key="{{ data_get($execution, 'id') }}"
class="relative flex flex-col p-4 border-dotted border-1 bg-coolgray-100" @class([ class="relative flex flex-col p-4 bg-white box-without-bg dark:bg-coolgray-100" @class([
'border-green-500' => data_get($execution, 'status') === 'success', 'border-green-500' => data_get($execution, 'status') === 'success',
'border-red-500' => data_get($execution, 'status') === 'failed', 'border-red-500' => data_get($execution, 'status') === 'failed',
])> ])>
@@ -23,7 +23,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<div class="flex-1"></div> <div class="flex-1"></div>
@if (data_get($execution, 'status') === 'success') @if (data_get($execution, 'status') === 'success')
<x-forms.button class=" hover:bg-coolgray-400" <x-forms.button class=" dark:hover:bg-coolgray-400"
wire:click="download({{ data_get($execution, 'id') }})">Download</x-forms.button> wire:click="download({{ data_get($execution, 'id') }})">Download</x-forms.button>
@endif @endif
<x-modal-confirmation isErrorButton action="deleteBackup({{ data_get($execution, 'id') }})"> <x-modal-confirmation isErrorButton action="deleteBackup({{ data_get($execution, 'id') }})">

View File

@@ -27,7 +27,7 @@
helper="More info <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper> helper="More info <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@forelse ($project->environment_variables->sort()->sortBy('real_value') as $env) @forelse ($project->environment_variables->sort()->sortBy('key') as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" type="project" /> :env="$env" type="project" />
@empty @empty

View File

@@ -52,7 +52,7 @@
helper="More info <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper> helper="More info <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@forelse ($environment->environment_variables->sort()->sortBy('real_value') as $env) @forelse ($environment->environment_variables->sort()->sortBy('key') as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" type="environment" /> :env="$env" type="environment" />
@empty @empty

View File

@@ -1,9 +1,9 @@
<div> <div>
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<h1>Create a new Application</h1> <h1>Create a new Application</h1>
<x-forms.button wire:click="saveFromRedirect('source.new')" class="group-hover:dark:text-white"> <x-modal-input buttonTitle="+ Add GitHub App" title="New GitHub App">
+ Add New GitHub App <livewire:source.github.create />
</x-forms.button> </x-modal-input>
@if ($repositories->count() > 0) @if ($repositories->count() > 0)
<a target="_blank" class="flex hover:no-underline" href="{{ get_installation_path($github_app) }}"> <a target="_blank" class="flex hover:no-underline" href="{{ get_installation_path($github_app) }}">
<x-forms.button> <x-forms.button>

View File

@@ -1,4 +1,4 @@
<div x-data x-init="$wire.loadServers" x-init="$wire.loadServices"> <div x-data x-init="$wire.loadServers">
<div class="flex flex-col gap-4 lg:flex-row "> <div class="flex flex-col gap-4 lg:flex-row ">
<h1>New Resource</h1> <h1>New Resource</h1>
<div class="w-full pb-4 lg:w-96 lg:pb-0"> <div class="w-full pb-4 lg:w-96 lg:pb-0">
@@ -377,12 +377,12 @@
<div class="flex items-center gap-4" wire:init='loadServices'> <div class="flex items-center gap-4" wire:init='loadServices'>
<h2 class="py-4">Services</h2> <h2 class="py-4">Services</h2>
<x-forms.button wire:click="loadServices('force')">Reload List</x-forms.button> <x-forms.button wire:click="loadServices('force')">Reload List</x-forms.button>
<input class="input" autofocus wire:model.live.debounce.200ms="search" autofocus
placeholder="Search...">
</div> </div>
<div class="pb-4 text-xs">Trademarks Policy: The respective trademarks mentioned here are owned by the <div class="pb-4 text-xs">Trademarks Policy: The respective trademarks mentioned here are owned by the
respective respective
companies, and use of them does not imply any affiliation or endorsement.</div> companies, and use of them does not imply any affiliation or endorsement.</div>
<input class="input" autofocus wire:model.live.debounce.200ms="search" autofocus
placeholder="Search...">
@if ($loadingServices) @if ($loadingServices)
<x-loading text="Loading services..." /> <x-loading text="Loading services..." />
@else @else

View File

@@ -1,27 +1,17 @@
<div tabindex="0" x-data="{ open: false }" <div class="p-4 transition border rounded cursor-pointer border-coolgray-200">
class="transition border rounded cursor-pointer collapse collapse-arrow border-coolgray-200" <div class="flex flex-col justify-center pb-4 text-sm select-text">
:class="open ? 'collapse-open' : 'collapse-close'"> <h2>{{ $service->name }}</h2>
<div class="flex flex-col justify-center text-sm select-text collapse-title" x-on:click="open = !open">
<div>{{ $workdir }}{{ $fs_path }} -> {{ $fileStorage->mount_path }}</div> <div>{{ $workdir }}{{ $fs_path }} -> {{ $fileStorage->mount_path }}</div>
</div> </div>
<div class="collapse-content"> <div>
<form wire:submit='submit' class="flex flex-col gap-2"> <form wire:submit='submit' class="flex flex-col gap-2">
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox> <x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox>
</div> </div>
{{-- @if ($fileStorage->is_directory)
<x-forms.input readonly label="Directory on Filesystem (save files here)" id="fs_path"></x-forms.input>
@else --}}
{{-- <div class="flex gap-2">
<x-forms.input readonly label="File in Docker Compose file" id="fileStorage.fs_path"></x-forms.input>
<x-forms.input readonly label="File on Filesystem (save files here)" id="fs_path"></x-forms.input>
</div>
<x-forms.input readonly label="Mount (in container)" id="fileStorage.mount_path"></x-forms.input> --}}
@if (!$fileStorage->is_directory) @if (!$fileStorage->is_directory)
<x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea> <x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
@endif @endif
{{-- @endif --}}
</form> </form>
</div> </div>
</div> </div>

View File

@@ -32,7 +32,7 @@
@endif @endif
@endif @endif
@else @else
@if ($resource->persistentStorages()->get()->count() > 0 || $resource->fileStorages()->get()->count() > 0) @if ($resource->persistentStorages()->get()->count() > 0)
<h3 class="pt-4">{{ Str::headline($resource->name) }} </h3> <h3 class="pt-4">{{ Str::headline($resource->name) }} </h3>
@endif @endif
@if ($resource->persistentStorages()->get()->count() > 0) @if ($resource->persistentStorages()->get()->count() > 0)

View File

@@ -2,9 +2,10 @@
<h2>Servers</h2> <h2>Servers</h2>
<div class="">Server related configurations.</div> <div class="">Server related configurations.</div>
<div class="grid grid-cols-1 gap-4 py-4"> <div class="grid grid-cols-1 gap-4 py-4">
<div class="flex gap-2"> <div class="flex flex-col gap-2">
<div class="relative flex flex-col bg-white border cursor-default dark:text-white box-without-bg dark:bg-coolgray-100 w-96 dark:border-black"> <h3>Primary Server</h3>
<div class="text-xl font-bold">Primary Server</div> <div
class="relative flex flex-col bg-white border cursor-default dark:text-white box-without-bg dark:bg-coolgray-100 w-96 dark:border-black">
@if (str($resource->realStatus())->startsWith('running')) @if (str($resource->realStatus())->startsWith('running'))
<div title="{{ $resource->realStatus() }}" class="absolute bg-success -top-1 -left-1 badge "> <div title="{{ $resource->realStatus() }}" class="absolute bg-success -top-1 -left-1 badge ">
</div> </div>
@@ -12,26 +13,29 @@
<div title="{{ $resource->realStatus() }}" class="absolute bg-error -top-1 -left-1 badge "> <div title="{{ $resource->realStatus() }}" class="absolute bg-error -top-1 -left-1 badge ">
</div> </div>
@endif @endif
<div> <div class="box-title">
Server: {{ data_get($resource, 'destination.server.name') }} Server: {{ data_get($resource, 'destination.server.name') }}
</div> </div>
<div> <div class="box-description">
Network: {{ data_get($resource, 'destination.network') }} Network: {{ data_get($resource, 'destination.network') }}
</div> </div>
</div> </div>
@if ($resource?->additional_networks?->count() > 0) @if ($resource?->additional_networks?->count() > 0)
<x-forms.button <div class="flex gap-2">
wire:click="redeploy('{{ data_get($resource, 'destination.id') }}','{{ data_get($resource, 'destination.server.id') }}')">Deploy</x-forms.button> <x-forms.button
@if (str($resource->realStatus())->startsWith('running')) wire:click="redeploy('{{ data_get($resource, 'destination.id') }}','{{ data_get($resource, 'destination.server.id') }}')">Deploy</x-forms.button>
<x-forms.button isError @if (str($resource->realStatus())->startsWith('running'))
wire:click="stop('{{ data_get($resource, 'destination.server.id') }}')">Stop</x-forms.button> <x-forms.button isError
@endif wire:click="stop('{{ data_get($resource, 'destination.server.id') }}')">Stop</x-forms.button>
@endif
</div>
@endif @endif
</div> </div>
@if ($resource?->additional_networks?->count() > 0) @if ($resource?->additional_networks?->count() > 0)
<h3>Additional Server(s)</h3>
@foreach ($resource->additional_networks as $destination) @foreach ($resource->additional_networks as $destination)
<div class="flex gap-2"> <div class="flex flex-col gap-2">
<div class="relative flex flex-col box w-96"> <div class="relative flex flex-col w-full box">
@if (str(data_get($destination, 'pivot.status'))->startsWith('running')) @if (str(data_get($destination, 'pivot.status'))->startsWith('running'))
<div title="{{ data_get($destination, 'pivot.status') }}" <div title="{{ data_get($destination, 'pivot.status') }}"
class="absolute bg-success -top-1 -left-1 badge "></div> class="absolute bg-success -top-1 -left-1 badge "></div>
@@ -40,28 +44,31 @@
class="absolute bg-error -top-1 -left-1 badge "></div> class="absolute bg-error -top-1 -left-1 badge "></div>
@endif @endif
<div> <div>
Server: {{ data_get($destination, 'server.name') }} <div class="box-title">
</div> Server: {{ data_get($destination, 'server.name') }}
<div> </div>
Network: {{ data_get($destination, 'network') }} <div class="box-description">
Network: {{ data_get($destination, 'network') }}
</div>
</div> </div>
</div> </div>
<x-forms.button <div class="flex gap-2">
wire:click="redeploy('{{ data_get($destination, 'id') }}','{{ data_get($destination, 'server.id') }}')">Deploy</x-forms.button> <x-forms.button
<x-forms.button wire:click="redeploy('{{ data_get($destination, 'id') }}','{{ data_get($destination, 'server.id') }}')">Deploy</x-forms.button>
wire:click="promote('{{ data_get($destination, 'id') }}','{{ data_get($destination, 'server.id') }}')">Promote <x-forms.button
to Primary </x-forms.button> wire:click="promote('{{ data_get($destination, 'id') }}','{{ data_get($destination, 'server.id') }}')">Promote
@if (data_get_str($destination, 'pivot.status')->startsWith('running')) to Primary </x-forms.button>
<x-forms.button isError @if (data_get_str($destination, 'pivot.status')->startsWith('running'))
wire:click="stop('{{ data_get($destination, 'server.id') }}')">Stop</x-forms.button> <x-forms.button isError
@endif wire:click="stop('{{ data_get($destination, 'server.id') }}')">Stop</x-forms.button>
<x-modal-confirmation @endif
action="removeServer({{ data_get($destination, 'id') }},{{ data_get($destination, 'server.id') }})" <x-modal-confirmation
isErrorButton buttonTitle="Remove Server"> action="removeServer({{ data_get($destination, 'id') }},{{ data_get($destination, 'server.id') }})"
This will stop the running application in this server and remove it as a deployment isErrorButton buttonTitle="Remove Server">
destination.<br><br>Please think again. This will stop the running application in this server and remove it as a deployment
</x-modal-confirmation> destination.<br><br>Please think again.
</x-modal-confirmation>
</div>
</div> </div>
@endforeach @endforeach
@endif @endif
@@ -75,10 +82,12 @@
<div wire:click="addServer('{{ $network->id }}','{{ data_get($network, 'server.id') }}')" <div wire:click="addServer('{{ $network->id }}','{{ data_get($network, 'server.id') }}')"
class="relative flex flex-col cursor-default dark:text-white box w-96"> class="relative flex flex-col cursor-default dark:text-white box w-96">
<div> <div>
Server: {{ data_get($network, 'server.name') }} <div class="box-title">
</div> Server: {{ data_get($network, 'server.name') }}
<div> </div>
Network: {{ data_get($network, 'name') }} <div class="box-description">
Network: {{ data_get($network, 'name') }}
</div>
</div> </div>
</div> </div>
@endforeach @endforeach

View File

@@ -16,7 +16,7 @@
@endif @endif
</div> </div>
@if ($view === 'normal') @if ($view === 'normal')
@forelse ($resource->environment_variables->sort()->sortBy('real_value') as $env) @forelse ($resource->environment_variables->sort()->sortBy('key') as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" :type="$resource->type()" /> :env="$env" :type="$resource->type()" />
@empty @empty
@@ -27,7 +27,7 @@
<h3>Preview Deployments</h3> <h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div> <div>Environment (secrets) variables for Preview Deployments.</div>
</div> </div>
@foreach ($resource->environment_variables_preview->sort()->sortBy('real_value') as $env) @foreach ($resource->environment_variables_preview->sort()->sortBy('key') as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" :type="$resource->type()" /> :env="$env" :type="$resource->type()" />
@endforeach @endforeach

View File

@@ -1,6 +1,12 @@
<div> <div>
{{-- <div class="subtitle">Private Keys are used to connect to your servers without passwords.</div> --}} <div class="pb-0 subtitle">
<x-forms.button class="mb-4" wire:click="generateNewKey">Generate new SSH key for me</x-forms.button> <div >Private Keys are used to connect to your servers without passwords.</div>
<div class="font-bold">You should not use passphrase protected keys.</div>
</div>
<div class="flex gap-2 mb-4">
<x-forms.button wire:click="generateNewRSAKey">Generate new RSA SSH Key</x-forms.button>
<x-forms.button wire:click="generateNewEDKey">Generate new ED25519 SSH Key</x-forms.button>
</div>
<form class="flex flex-col gap-2" wire:submit='createPrivateKey'> <form class="flex flex-col gap-2" wire:submit='createPrivateKey'>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input id="name" label="Name" required /> <x-forms.input id="name" label="Name" required />

View File

@@ -1,4 +1,4 @@
<div> <div x-init="$wire.loadPublicKey()">
<x-security.navbar /> <x-security.navbar />
<div x-data="{ showPrivateKey: false }"> <div x-data="{ showPrivateKey: false }">
<form class="flex flex-col gap-2" wire:submit='changePrivateKey'> <form class="flex flex-col gap-2" wire:submit='changePrivateKey'>
@@ -22,11 +22,11 @@
<x-forms.input readonly id="public_key" /> <x-forms.input readonly id="public_key" />
<div class="flex items-end gap-2 py-2 "> <div class="flex items-end gap-2 py-2 ">
<div class="pl-1 ">Private Key <span class='text-helper'>*</span></div> <div class="pl-1 ">Private Key <span class='text-helper'>*</span></div>
<div class="text-xs dark:text-white underline cursor-pointer" x-cloak x-show="!showPrivateKey" <div class="text-xs underline cursor-pointer dark:text-white" x-cloak x-show="!showPrivateKey"
x-on:click="showPrivateKey = true"> x-on:click="showPrivateKey = true">
Edit Edit
</div> </div>
<div class="text-xs dark:text-white underline cursor-pointer" x-cloak x-show="showPrivateKey" <div class="text-xs underline cursor-pointer dark:text-white" x-cloak x-show="showPrivateKey"
x-on:click="showPrivateKey = false"> x-on:click="showPrivateKey = false">
Hide Hide
</div> </div>

View File

@@ -0,0 +1,8 @@
<form wire:submit.prevent='submit' class="flex flex-col w-full gap-2">
<x-forms.input id="cloudflare_token" required label="Cloudflare Token" />
<x-forms.input id="ssh_domain" label="Configured SSH Domain" required
helper="The SSH Domain you configured in Cloudflare" />
<x-forms.button type="submit" isHighlighted @click="modalOpen=false">Automated Configuration (experimental)</x-forms.button>
<h3 class="text-center">Or</h3>
<x-forms.button wire:click.prevent='alreadyConfigured' @click="modalOpen=false">I have already set up the tunnel manually on the server.</x-forms.button>
</form>

View File

@@ -70,17 +70,27 @@
</div> </div>
</div> </div>
<div class="w-64"> <div class="w-64">
@if (!$server->isLocalhost()) @if ($server->isFunctional())
@if ($server->settings->is_build_server) @if (!$server->isLocalhost())
<x-forms.checkbox instantSave disabled id="server.settings.is_build_server" <x-forms.checkbox instantSave disabled id="server.settings.is_build_server"
label="Use it as a build server?" /> label="Use it as a build server?" />
@else <div class="flex items-center gap-1 pt-6">
<x-forms.checkbox instantSave <h3 class="">Cloudflare Tunnels
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br><span class='dark:text-warning'>Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" </h3>
id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" /> <x-helper class="inline-flex"
@if ($server->isSwarm()) helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br><span class='dark:text-warning'>Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
<div class="pt-6"> Swarm support is experimental. </div> </div>
@if ($server->settings->is_cloudflare_tunnel)
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel" label="Enabled" />
@else
<x-modal-input buttonTitle="Configure" title="Cloudflare Tunnels">
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input>
@endif @endif
<h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span></h3>
<div class="pb-4">Read the docs <a class='underline dark:text-white'
href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>.
</div>
@if ($server->settings->is_swarm_worker) @if ($server->settings->is_swarm_worker)
<x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_manager" <x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_manager"
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>." helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
@@ -90,6 +100,7 @@
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>." helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
label="Is it a Swarm Manager?" /> label="Is it a Swarm Manager?" />
@endif @endif
@if ($server->settings->is_swarm_manager) @if ($server->settings->is_swarm_manager)
<x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_worker" <x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_worker"
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>." helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
@@ -100,7 +111,22 @@
label="Is it a Swarm Worker?" /> label="Is it a Swarm Worker?" />
@endif @endif
@endif @endif
@else
<div class="flex items-center gap-1 pt-6">
<h3 class="">Cloudflare Tunnels
</h3>
<x-helper class="inline-flex"
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br><span class='dark:text-warning'>Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
</div>
@if ($server->settings->is_cloudflare_tunnel)
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel" label="Enabled" />
@else
<x-modal-input buttonTitle="Configure" title="Cloudflare Tunnels">
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input>
@endif
@endif @endif
</div> </div>
</div> </div>

View File

@@ -1,165 +1,159 @@
<div> <div>
<x-server.navbar :server="$server" :parameters="$parameters" /> <x-server.navbar :server="$server" :parameters="$parameters" />
@if ($server->isFunctional()) <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'managed' }" class="flex h-full">
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'managed' }" class="flex h-full"> <div class="flex flex-col gap-4">
<div class="flex flex-col gap-4"> <a :class="activeTab === 'managed' && 'dark:text-white'"
<a :class="activeTab === 'managed' && 'dark:text-white'" @click.prevent="activeTab = 'managed'; window.location.hash = 'managed'" href="#">Managed</a>
@click.prevent="activeTab = 'managed'; window.location.hash = 'managed'" href="#">Managed</a> <a :class="activeTab === 'unmanaged' && 'dark:text-white'"
<a :class="activeTab === 'unmanaged' && 'dark:text-white'" @click.prevent="activeTab = 'unmanaged'; window.location.hash = 'unmanaged'" href="#">Unmanaged</a>
@click.prevent="activeTab = 'unmanaged'; window.location.hash = 'unmanaged'" </div>
href="#">Unmanaged</a> <div class="w-full pl-8">
</div> <div x-cloak x-show="activeTab === 'managed'" class="h-full">
<div class="w-full pl-8"> <div class="flex flex-col">
<div x-cloak x-show="activeTab === 'managed'" class="h-full"> <div class="flex gap-2">
<h2>Resources</h2>
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>
</div>
<div class="subtitle">Here you can find all resources that are managed by Coolify.</div>
</div>
@if ($server->definedResources()->count() > 0)
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex gap-2">
<h2>Resources</h2>
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>
</div>
<div class="subtitle">Here you can find all resources that are managed by Coolify.</div>
</div>
@if ($server->definedResources()->count() > 0)
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col"> <div class="overflow-x-auto">
<div class="overflow-x-auto"> <div class="inline-block min-w-full">
<div class="inline-block min-w-full"> <div class="overflow-hidden">
<div class="overflow-hidden"> <table class="min-w-full">
<table class="min-w-full divide-y divide-coolgray-400"> <thead>
<thead> <tr>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
Project
</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
Environment</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
Name
</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
Type
</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
Status
</th>
</tr>
</thead>
<tbody>
@forelse ($server->definedResources()->sortBy('name',SORT_NATURAL) as $resource)
<tr> <tr>
<th class="px-5 py-3 text-xs font-medium text-left uppercase"> <td class="px-5 py-4 text-sm whitespace-nowrap">
Project {{ data_get($resource->project(), 'name') }}
</th> </td>
<th class="px-5 py-3 text-xs font-medium text-left uppercase"> <td class="px-5 py-4 text-sm whitespace-nowrap">
Environment</th> {{ data_get($resource, 'environment.name') }}
<th class="px-5 py-3 text-xs font-medium text-left uppercase"> </td>
Name <td class="px-5 py-4 text-sm whitespace-nowrap hover:underline">
</th> <a class=""
<th class="px-5 py-3 text-xs font-medium text-left uppercase"> href="{{ $resource->link() }}">{{ $resource->name }}
Type <x-internal-link /></a>
</th> </td>
<th class="px-5 py-3 text-xs font-medium text-left uppercase"> <td class="px-5 py-4 text-sm whitespace-nowrap">
Status {{ str($resource->type())->headline() }}</td>
</th> <td class="px-5 py-4 text-sm font-medium whitespace-nowrap">
@if ($resource->type() === 'service')
<x-status.services :service="$resource"
:showRefreshButton="false" />
@else
<x-status.index :resource="$resource" :showRefreshButton="false" />
@endif
</td>
</tr> </tr>
</thead> @empty
<tbody class="divide-y divide-coolgray-400"> @endforelse
@forelse ($server->definedResources()->sortBy('name',SORT_NATURAL) as $resource) </tbody>
<tr class="dark:text-white bg-coolblack hover:bg-coolgray-100"> </table>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource->project(), 'name') }}
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource, 'environment.name') }}
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap"><a
class=""
href="{{ $resource->link() }}">{{ $resource->name }}
<x-internal-link /></a>
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ str($resource->type())->headline() }}</td>
<td class="px-5 py-4 text-sm font-medium whitespace-nowrap">
@if ($resource->type() === 'service')
<x-status.services :service="$resource"
:showRefreshButton="false" />
@else
<x-status.index :resource="$resource"
:showRefreshButton="false" />
@endif
</td>
</tr>
@empty
@endforelse
</tbody>
</table>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@else
<div>No resources found.</div>
@endif
</div>
<div x-cloak x-show="activeTab === 'unmanaged'" class="h-full">
<div class="flex flex-col" x-init="$wire.loadUnmanagedContainers()">
<div class="flex gap-2">
<h2>Resources</h2>
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>
</div>
<div class="subtitle">Here you can find all other containers running on the server.</div>
</div> </div>
@if ($unmanagedContainers->count() > 0) @else
<div>No resources found.</div>
@endif
</div>
<div x-cloak x-show="activeTab === 'unmanaged'" class="h-full">
<div class="flex flex-col" x-init="$wire.loadUnmanagedContainers()">
<div class="flex gap-2">
<h2>Resources</h2>
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>
</div>
<div class="subtitle">Here you can find all other containers running on the server.</div>
</div>
@if ($unmanagedContainers->count() > 0)
<div class="flex flex-col">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col"> <div class="overflow-x-auto">
<div class="overflow-x-auto"> <div class="inline-block min-w-full">
<div class="inline-block min-w-full"> <div class="overflow-hidden">
<div class="overflow-hidden"> <table class="min-w-full">
<table class="min-w-full divide-y divide-coolgray-400"> <thead>
<thead> <tr>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
Name
</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
Image
</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
Status
</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
Action
</th>
</tr>
</thead>
<tbody>
@forelse ($unmanagedContainers->sortBy('name',SORT_NATURAL) as $resource)
<tr> <tr>
<th class="px-5 py-3 text-xs font-medium text-left uppercase"> <td class="px-5 py-4 text-sm whitespace-nowrap">
Name {{ data_get($resource, 'Names') }}
</th> </td>
<th class="px-5 py-3 text-xs font-medium text-left uppercase"> <td class="px-5 py-4 text-sm whitespace-nowrap">
Image {{ data_get($resource, 'Image') }}
</th> </td>
<th class="px-5 py-3 text-xs font-medium text-left uppercase"> <td class="px-5 py-4 text-sm whitespace-nowrap">
Status {{ data_get($resource, 'State') }}
</th> </td>
<th class="px-5 py-3 text-xs font-medium text-left uppercase"> <td class="flex gap-2 px-5 py-4 text-sm whitespace-nowrap">
Action @if (data_get($resource, 'State') === 'running')
</th> <x-forms.button
wire:click="restartUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Restart</x-forms.button>
<x-forms.button isError
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Stop</x-forms.button>
@elseif (data_get($resource, 'State') === 'exited')
<x-forms.button
wire:click="startUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Start</x-forms.button>
@elseif (data_get($resource, 'State') === 'restarting')
<x-forms.button
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Stop</x-forms.button>
@endif
</td>
</tr> </tr>
</thead> @empty
<tbody class="divide-y divide-coolgray-400"> @endforelse
@forelse ($unmanagedContainers->sortBy('name',SORT_NATURAL) as $resource) </tbody>
<tr class="dark:text-white bg-coolblack hover:bg-coolgray-100"> </table>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource, 'Names') }}
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource, 'Image') }}
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource, 'State') }}
</td>
<td class="flex gap-2 px-5 py-4 text-sm whitespace-nowrap">
@if (data_get($resource, 'State') === 'running')
<x-forms.button
wire:click="restartUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Restart</x-forms.button>
<x-forms.button isError
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Stop</x-forms.button>
@elseif (data_get($resource, 'State') === 'exited')
<x-forms.button
wire:click="startUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Start</x-forms.button>
@elseif (data_get($resource, 'State') === 'restarting')
<x-forms.button
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Stop</x-forms.button>
@endif
</td>
</tr>
@empty
@endforelse
</tbody>
</table>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@else </div>
<div>No resources found.</div> @else
@endif <div>No resources found.</div>
</div> @endif
</div> </div>
</div> </div>
@else </div>
<div>Server is not validated. Validate first.</div>
@endif
</div> </div>

View File

@@ -26,12 +26,12 @@
<h3 class="pb-4">Choose another Key</h3> <h3 class="pb-4">Choose another Key</h3>
<div class="grid grid-cols-3 gap-2"> <div class="grid grid-cols-3 gap-2">
@forelse ($privateKeys as $private_key) @forelse ($privateKeys as $private_key)
<div class="box group"> <div class="box group" wire:click='setPrivateKey({{ $private_key->id }})'>
<div class="flex flex-col " wire:click='setPrivateKey({{ $private_key->id }})'> <div class="flex flex-col ">
<div class="box-title">{{ $private_key->name }}</div> <div class="box-title">{{ $private_key->name }}</div>
<div class="box-description">{{ $private_key->description }}</div> <div class="box-description">{{ $private_key->description }}</div>
</div>
</div> </div>
</div>
@empty @empty
<div>No private keys found. </div> <div>No private keys found. </div>
@endforelse @endforelse

View File

@@ -107,7 +107,7 @@
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<div class="inline-block min-w-full"> <div class="inline-block min-w-full">
<div class="overflow-hidden"> <div class="overflow-hidden">
<table class="min-w-full divide-y divide-coolgray-400"> <table class="min-w-full">
<thead> <thead>
<tr> <tr>
<th class="px-5 py-3 text-xs font-medium text-left uppercase"> <th class="px-5 py-3 text-xs font-medium text-left uppercase">
@@ -121,9 +121,9 @@
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-coolgray-400"> <tbody class="divide-y">
@forelse ($applications->sortBy('name',SORT_NATURAL) as $resource) @forelse ($applications->sortBy('name',SORT_NATURAL) as $resource)
<tr class="dark:text-white bg-coolblack hover:bg-coolgray-100"> <tr>
<td class="px-5 py-4 text-sm whitespace-nowrap"> <td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource->project(), 'name') }} {{ data_get($resource->project(), 'name') }}
</td> </td>

View File

@@ -12,7 +12,7 @@
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@forelse ($team->environment_variables->sort()->sortBy('real_value') as $env) @forelse ($team->environment_variables->sort()->sortBy('key') as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" type="team" /> :env="$env" type="team" />
@empty @empty

View File

@@ -6,7 +6,7 @@
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<div class="inline-block min-w-full"> <div class="inline-block min-w-full">
<div class="overflow-hidden"> <div class="overflow-hidden">
<table class="min-w-full divide-y divide-coolgray-400"> <table class="min-w-full">
<thead> <thead>
<tr> <tr>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Email <th class="px-5 py-3 text-xs font-medium text-left uppercase">Email
@@ -20,9 +20,9 @@
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-coolgray-400"> <tbody>
@foreach ($invitations as $invite) @foreach ($invitations as $invite)
<tr class="dark:text-white bg-coolblack hover:bg-coolgray-100/40"> <tr>
<td class="px-5 py-4 text-sm whitespace-nowrap">{{ $invite->email }}</td> <td class="px-5 py-4 text-sm whitespace-nowrap">{{ $invite->email }}</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">{{ $invite->via }}</td> <td class="px-5 py-4 text-sm whitespace-nowrap">{{ $invite->via }}</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">{{ $invite->role }}</td> <td class="px-5 py-4 text-sm whitespace-nowrap">{{ $invite->role }}</td>

View File

@@ -7,7 +7,7 @@
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<div class="inline-block min-w-full"> <div class="inline-block min-w-full">
<div class="overflow-hidden"> <div class="overflow-hidden">
<table class="min-w-full divide-y dark:divide-coolgray-400 divide-neutral-400"> <table class="min-w-full">
<thead> <thead>
<tr> <tr>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Name <th class="px-5 py-3 text-xs font-medium text-left uppercase">Name
@@ -17,7 +17,7 @@
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Actions</th> <th class="px-5 py-3 text-xs font-medium text-left uppercase">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y dark:divide-coolgray-400 divide-neutral-200"> <tbody>
@foreach (currentTeam()->members as $member) @foreach (currentTeam()->members as $member)
<livewire:team.member :member="$member" :wire:key="$member->id" /> <livewire:team.member :member="$member" :wire:key="$member->id" />
@endforeach @endforeach

View File

@@ -6,7 +6,7 @@ set -e # Exit immediately if a command exits with a non-zero status
#set -u # Treat unset variables as an error and exit #set -u # Treat unset variables as an error and exit
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
VERSION="1.2.3" VERSION="1.3.1"
DOCKER_VERSION="24.0" DOCKER_VERSION="24.0"
CDN="https://cdn.coollabs.io/coolify" CDN="https://cdn.coollabs.io/coolify"
@@ -18,7 +18,12 @@ else
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
fi fi
LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | sed -n '2p' | xargs | awk '{print $2}' | tr -d ',') # Install xargs on Amazon Linux 2023 - lol
if [ "$OS_TYPE" = 'amzn' ]; then
dnf install -y findutils >/dev/null 2>&1
fi
LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',')
DATE=$(date +"%Y%m%d-%H%M%S") DATE=$(date +"%Y%m%d-%H%M%S")
if [ $EUID != 0 ]; then if [ $EUID != 0 ]; then
@@ -27,7 +32,7 @@ if [ $EUID != 0 ]; then
fi fi
case "$OS_TYPE" in case "$OS_TYPE" in
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux) ;; arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn) ;;
*) *)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
exit exit
@@ -64,8 +69,12 @@ ubuntu | debian | raspbian)
apt update -y >/dev/null 2>&1 apt update -y >/dev/null 2>&1
apt install -y curl wget git jq >/dev/null 2>&1 apt install -y curl wget git jq >/dev/null 2>&1
;; ;;
centos | fedora | rhel | ol | rocky | almalinux) centos | fedora | rhel | ol | rocky | almalinux | amzn)
dnf install -y curl wget git jq >/dev/null 2>&1 if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq >/dev/null 2>&1
else
dnf install -y curl wget git jq >/dev/null 2>&1
fi
;; ;;
sles | opensuse-leap | opensuse-tumbleweed) sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null 2>&1 zypper refresh >/dev/null 2>&1
@@ -133,6 +142,7 @@ if [ -x "$(command -v snap)" ]; then
fi fi
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
# Almalinux
if [ "$OS_TYPE" == 'almalinux' ]; then if [ "$OS_TYPE" == 'almalinux' ]; then
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
@@ -146,6 +156,7 @@ if ! [ -x "$(command -v docker)" ]; then
set +e set +e
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."
# Arch Linux
if [ "$OS_TYPE" = "arch" ]; then if [ "$OS_TYPE" = "arch" ]; then
pacman -Sy docker docker-compose --noconfirm pacman -Sy docker docker-compose --noconfirm
systemctl enable docker.service systemctl enable docker.service
@@ -157,19 +168,38 @@ if ! [ -x "$(command -v docker)" ]; then
exit exit
fi fi
else else
curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh # Amazon Linux 2023
if [ -x "$(command -v docker)" ]; then if [ "$OS_TYPE" = "amzn" ]; then
echo "Docker installed successfully." dnf install docker -y
else DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
echo "Docker installation failed with Rancher script. Trying with official script." mkdir -p $DOCKER_CONFIG/cli-plugins
curl https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
systemctl start docker
systemctl enable docker
if [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully." echo "Docker installed successfully."
else else
echo "Docker installation failed with official script." echo "Failed to install Docker with pacman. Try to install it manually."
echo "Maybe your OS is not supported?" echo "Please visit https://wiki.archlinux.org/title/docker for more information."
echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." exit
exit 1 fi
else
# Automated Docker installation
curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Docker installation failed with Rancher script. Trying with official script."
curl https://get.docker.com | sh -s -- --version ${DOCKER_VERSION}
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Docker installation failed with official script."
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
fi fi
fi fi

View File

@@ -15,3 +15,8 @@ services:
environment: environment:
- SERVICE_FQDN_SPDF_8080 - SERVICE_FQDN_SPDF_8080
- DOCKER_ENABLE_SECURITY=false - DOCKER_ENABLE_SECURITY=false
healthcheck:
test: 'curl --fail -I http://localhost:8080 || exit 1'
interval: 5s
timeout: 20s
retries: 10

View File

@@ -15,7 +15,7 @@ services:
condition: service_healthy condition: service_healthy
environment: environment:
- SERVICE_FQDN_SUPABASE_8000 - SERVICE_FQDN_SUPABASE_8000
- JWT_SERCET=${SERVICE_PASSWORD_JWT} - JWT_SECRET=${SERVICE_PASSWORD_JWT}
- KONG_DATABASE=off - KONG_DATABASE=off
- KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml - KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml
# https://github.com/supabase/cli/issues/14 # https://github.com/supabase/cli/issues/14

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.246" "version": "4.0.0-beta.255"
} }
} }
} }