Compare commits

...

86 Commits

Author SHA1 Message Date
Andras Bacsai
ab853cac87 Merge pull request #1560 from coollabsio/next
v4.0.0-beta.169
2023-12-20 16:21:06 +01:00
Andras Bacsai
51db2f797d fix: storage error on dbs 2023-12-20 16:19:48 +01:00
Andras Bacsai
0c90d3d0a1 fix: docker compose apps env rewritten 2023-12-20 16:15:13 +01:00
Andras Bacsai
51efe23690 Merge pull request #1559 from coollabsio/next
disable db + service deployments swarm
2023-12-20 14:46:12 +01:00
Andras Bacsai
e3ee84105c disable db + service deployments swarm 2023-12-20 14:45:47 +01:00
Andras Bacsai
6cbd61ac6c Merge pull request #1558 from coollabsio/next
fix: get swarm service logs
2023-12-20 14:25:53 +01:00
Andras Bacsai
638d0c8c99 fix: get swarm service logs 2023-12-20 14:11:50 +01:00
Andras Bacsai
aecc81fe9d Merge pull request #1557 from coollabsio/next
v4.0.0-beta.166
2023-12-20 13:21:45 +01:00
Andras Bacsai
c9a1437870 Fix handle method in ServerStatusJob 2023-12-20 12:33:58 +01:00
Andras Bacsai
66b41b3d4c Update ServerStatusJob middleware and uniqueId()
method
2023-12-20 12:33:21 +01:00
Andras Bacsai
c41cfe2a2f Fix server status check and cleanup logic 2023-12-20 12:32:46 +01:00
Andras Bacsai
5f2ad56529 Update container and server status job 2023-12-20 12:25:14 +01:00
Andras Bacsai
cd842bc1b2 Update number of tries in ContainerStatusJob 2023-12-20 12:13:34 +01:00
Andras Bacsai
27b6aad53a fix 2023-12-20 11:59:53 +01:00
Andras Bacsai
64b58b7661 hm 2023-12-20 11:59:06 +01:00
Andras Bacsai
94960d96a9 add max horizon processes 2023-12-20 11:47:51 +01:00
Andras Bacsai
2549244f97 hm 2023-12-20 11:44:46 +01:00
Andras Bacsai
5bfffce33b hm 2023-12-20 11:37:04 +01:00
Andras Bacsai
3a4f19f368 version++ 2023-12-20 11:22:10 +01:00
Andras Bacsai
50e17ed932 fix: server ready 2023-12-20 11:21:17 +01:00
Andras Bacsai
a8fcd7aee4 Merge pull request #1556 from coollabsio/next
small UI fixes
2023-12-20 10:31:59 +01:00
Andras Bacsai
87036cc49b link 2023-12-20 10:27:39 +01:00
Andras Bacsai
e48842c6ec fix: swarm support ui 2023-12-20 10:19:21 +01:00
Andras Bacsai
b9e405c497 Merge pull request #1555 from coollabsio/next
v4.0.0-beta.165
2023-12-19 21:01:40 +01:00
Andras Bacsai
f75effe022 fix 2023-12-19 15:44:41 +01:00
Andras Bacsai
a745f568f3 gh actions update 2023-12-19 15:44:01 +01:00
Andras Bacsai
e2e3ad0358 get branchname gh actions 2023-12-19 15:41:53 +01:00
Andras Bacsai
ba769f5fb7 fix 2023-12-19 15:36:59 +01:00
Andras Bacsai
0126286731 fix: server update schedule 2023-12-19 15:16:08 +01:00
Andras Bacsai
7952202435 fix: do not autovalidate server on mount 2023-12-19 14:19:23 +01:00
Andras Bacsai
798acb8ee5 add swarm server grouping
fixes for swarm
2023-12-19 13:47:12 +01:00
Andras Bacsai
ef595dd4c2 fix: server not found 2023-12-19 12:24:43 +01:00
Andras Bacsai
70c662daf8 disable swarm for the next release 2023-12-18 17:13:22 +01:00
Andras Bacsai
8ae385b9f9 fix: add alpha to swarm 2023-12-18 14:34:04 +01:00
Andras Bacsai
802a0f7684 fix: do not push dockerimage 2023-12-18 14:17:48 +01:00
Andras Bacsai
62c38c9859 wip: swarm 2023-12-18 14:01:25 +01:00
Andras Bacsai
27c36bec83 feat: custom docker compose commands 2023-12-17 20:56:12 +01:00
Andras Bacsai
c6b8eabe10 wip: swarm 2023-12-15 15:48:01 +01:00
Andras Bacsai
967fca9eca version ++ 2023-12-15 15:17:49 +01:00
Andras Bacsai
40a239ddda Merge pull request #1550 from coollabsio/next
refactor: gitlab manual webhooks
2023-12-15 14:19:49 +01:00
Andras Bacsai
99d07981cf fix 2023-12-15 14:19:29 +01:00
Andras Bacsai
b3ee6b7144 fix: add debug output to gitlab webhooks 2023-12-15 14:17:53 +01:00
Andras Bacsai
468ad7d904 fix: no action in webhooks 2023-12-15 14:09:14 +01:00
Andras Bacsai
f1aa97e374 Merge pull request #1548 from coollabsio/next
fix: compose domains & links
2023-12-15 11:01:03 +01:00
Andras Bacsai
3b6d3343c7 fix: domains for compose bp 2023-12-15 11:00:51 +01:00
Andras Bacsai
ab2f9f073f Merge pull request #1546 from stooit/fix/docker-compose-multidomain
fix: Multiple domain links in docker compose applications.
2023-12-15 10:39:01 +01:00
Andras Bacsai
67131152cc fix: reset domains on compose file change 2023-12-15 10:37:45 +01:00
Andras Bacsai
3bda289428 fix: ui for adding new destination 2023-12-15 10:24:02 +01:00
Andras Bacsai
fadfa0ad8e Merge pull request #1547 from coollabsio/next
fix: server checking status
2023-12-15 10:01:58 +01:00
Andras Bacsai
11a957c6c9 fix: server checking status 2023-12-15 10:01:14 +01:00
Stuart Rowlands
b46de99af9 Fixes multi-domain links in docker compose applications. 2023-12-14 18:16:17 -08:00
Andras Bacsai
03420076c9 Merge pull request #1545 from coollabsio/next
v4.0.0-beta.163
2023-12-14 15:41:16 +01:00
Andras Bacsai
549446abdf fix: handle other types of generated values 2023-12-14 15:34:05 +01:00
Andras Bacsai
06ab2145ca fix: improve server status check times 2023-12-14 15:33:46 +01:00
Andras Bacsai
5d088e530e version++ 2023-12-14 15:33:34 +01:00
Andras Bacsai
123e6eddd7 fix: only check server status in container status job 2023-12-14 15:33:25 +01:00
Andras Bacsai
2c17e431ac Merge pull request #1544 from coollabsio/next
fix: backup executions view
2023-12-14 15:01:35 +01:00
Andras Bacsai
fe6073ba7d fix: backup executions view 2023-12-14 15:01:19 +01:00
Andras Bacsai
8a9ad04744 Merge pull request #1540 from coollabsio/next
v4.0.0-beta.162
2023-12-14 14:58:30 +01:00
Andras Bacsai
75d1ec4f42 feat: pull latest images for services 2023-12-14 14:50:38 +01:00
Andras Bacsai
a0abde8652 ui: add image name to service stack + better options visibility 2023-12-14 14:24:54 +01:00
Andras Bacsai
db13dd9304 fix: revert random container job delay 2023-12-13 15:40:57 +01:00
Andras Bacsai
638bcf9732 update 2023-12-13 15:34:33 +01:00
Andras Bacsai
b06b465ffa fix: add catch all route 2023-12-13 15:29:45 +01:00
Andras Bacsai
02c8b9f471 fix: password reset / invitation link requests 2023-12-13 15:22:37 +01:00
Andras Bacsai
1ff1664b6c fix: copy invitation 2023-12-13 14:44:11 +01:00
Andras Bacsai
52d84c5e9e refactor: clone project 2023-12-13 14:22:23 +01:00
Andras Bacsai
e0289e2949 feat: randomly sleep between executions 2023-12-13 12:35:56 +01:00
Andras Bacsai
ff8d8371ad fix: check queued deployments as well 2023-12-13 12:13:20 +01:00
Andras Bacsai
69343f974a soft delete models 2023-12-13 12:08:12 +01:00
Andras Bacsai
2dc175be63 fix: null notify 2023-12-13 12:01:27 +01:00
Andras Bacsai
d93bf97919 cleanup on start 2023-12-13 12:01:21 +01:00
Andras Bacsai
4ea8916d53 fix: update Coolify script 2023-12-13 11:55:08 +01:00
Andras Bacsai
f7fca69a23 update sentry key 2023-12-13 11:53:50 +01:00
Andras Bacsai
f954ee15c3 fix: init script echos 2023-12-13 11:53:01 +01:00
Andras Bacsai
3c54e01d87 improve more 2023-12-13 11:35:53 +01:00
Andras Bacsai
00d708610d improve local dev + contribution guide 2023-12-13 11:12:53 +01:00
Andras Bacsai
f3b04c1ef9 refactor: custom labels 2023-12-13 09:23:27 +01:00
Andras Bacsai
6b751f965b Merge pull request #1539 from coollabsio/next
v4.0.0-beta.161
2023-12-12 16:47:56 +01:00
Andras Bacsai
d3c9894479 fix: labels 2023-12-12 16:45:46 +01:00
Andras Bacsai
f68aace445 fix: non-ascii chars in labels 2023-12-12 16:34:05 +01:00
Andras Bacsai
f042c70b3c fix: labelling 2023-12-12 15:48:51 +01:00
Andras Bacsai
2116d79aad Merge pull request #1538 from coollabsio/next
v4.0.0-beta.160
2023-12-12 14:32:24 +01:00
Andras Bacsai
4bc63e283c fix: service env variable ovewritten if it has a default value 2023-12-12 14:28:11 +01:00
Andras Bacsai
d5804f99c2 Merge pull request #1537 from coollabsio/next
v4.0.0-beta.159
2023-12-12 13:25:40 +01:00
Andras Bacsai
dfc353ce54 fix: ignore if dynamic config could not be set 2023-12-12 13:20:26 +01:00
106 changed files with 1600 additions and 777 deletions

View File

@@ -2,7 +2,7 @@ name: Development Build (v4)
on:
push:
branches: ["next"]
branches-ignore: ["main", "v3"]
paths-ignore:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
@@ -29,51 +29,51 @@ jobs:
file: docker/prod-ssu/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
aarch64:
runs-on: [self-hosted, arm64]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v3
with:
context: .
file: docker/prod-ssu/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64
runs-on: [self-hosted, arm64]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v3
with:
context: .
file: docker/prod-ssu/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [amd64, aarch64]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [amd64, aarch64]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -22,8 +22,6 @@ You can ask for guidance anytime on our
- Run `spin up` - You can notice that errors will be thrown. Don't worry.
- If you see weird permission errors, especially on Mac, run `sudo spin up` instead.
- Run `./scripts/run setup:dev` - This will generate a secret key for you, delete any existing database layouts, migrate database to the new layout, and seed your database.
### 4) Start development
You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
@@ -31,7 +29,6 @@ Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if y
Mails are caught by Mailpit: `localhost:8025`
## New Service Contribution
Check out the docs [here](https://coolify.io/docs/how-to-add-a-service).

View File

@@ -21,7 +21,10 @@ class CheckProxy
$status = getContainerStatus($server, 'coolify-proxy_traefik');
$server->proxy->set('status', $status);
$server->save();
return false;
if ($status === 'running') {
return false;
}
return true;
} else {
$status = getContainerStatus($server, 'coolify-proxy');
if ($status === 'running') {

View File

@@ -18,7 +18,7 @@ class UpdateCoolify
try {
$settings = InstanceSettings::get();
ray('Running InstanceAutoUpdateJob');
$this->server = Server::find(0)->first();
$this->server = Server::find(0);
if (!$this->server) {
return;
}

View File

@@ -11,6 +11,7 @@ class StartService
use AsAction;
public function handle(Service $service)
{
ray('Starting service: ' . $service->name);
$network = $service->destination->network;
$service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir();

View File

@@ -10,6 +10,7 @@ class StopService
use AsAction;
public function handle(Service $service)
{
ray('Stopping service: ' . $service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Actions\Shared;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
class PullImage
{
use AsAction;
public function handle(Service $resource)
{
$resource->saveComposeConfigs();
$commands[] = "cd " . $resource->workdir();
$commands[] = "echo 'Saved configuration files to {$resource->workdir()}.'";
$commands[] = "docker compose pull";
$server = data_get($resource, 'server');
if (!$server) return;
instant_remote_process($commands, $resource->server);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Models\InstanceSettings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Process;
class Dev extends Command
{
protected $signature = 'dev:init';
protected $description = 'Init the app in dev mode';
public function handle()
{
// Generate APP_KEY if not exists
if (empty(env('APP_KEY'))) {
echo "Generating APP_KEY.\n";
Artisan::call('key:generate');
}
// Seed database if it's empty
$settings = InstanceSettings::find(0);
if (!$settings) {
echo "Initializing instance, seeding database.\n";
Artisan::call('migrate --seed');
} else {
echo "Instance already initialized.\n";
}
// Set permissions
Process::run(['chmod', '-R', 'o+rwx', '.']);
}
}

View File

@@ -35,7 +35,12 @@ class Init extends Command
}
$this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_helper_containers();
setup_dynamic_configuration();
try {
setup_dynamic_configuration();
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
$settings = InstanceSettings::get();
if (!is_null(env('AUTOUPDATE', null))) {
@@ -94,7 +99,7 @@ class Init extends Command
// Cleanup any failed deployments
try {
$halted_deployments = ApplicationDeploymentQueue::where('status', '==', 'in_progress')->get();
$halted_deployments = ApplicationDeploymentQueue::where('status', '==', ApplicationDeploymentStatus::IN_PROGRESS)->where('status', '==', ApplicationDeploymentStatus::QUEUED)->get();
foreach ($halted_deployments as $deployment) {
$deployment->status = ApplicationDeploymentStatus::FAILED->value;
$deployment->save();
@@ -110,16 +115,19 @@ class Init extends Command
$applications = Application::all();
foreach ($applications as $application) {
if (!data_get($application, 'environment')) {
echo 'Application without environment' . $application->name . 'deleting\n';
echo 'Application without environment: ' . $application->name . ' soft deleting\n';
$application->delete();
continue;
}
if (!$application->destination()) {
echo 'Application without destination' . $application->name . 'deleting\n';
echo 'Application without destination: ' . $application->name . ' soft deleting\n';
$application->delete();
continue;
}
if (!data_get($application, 'destination.server')) {
echo 'Application without server' . $application->name . 'deleting\n';
echo 'Application without server: ' . $application->name . ' soft deleting\n';
$application->delete();
continue;
}
}
} catch (\Throwable $e) {
@@ -129,16 +137,19 @@ class Init extends Command
$postgresqls = StandalonePostgresql::all();
foreach ($postgresqls as $postgresql) {
if (!data_get($postgresql, 'environment')) {
echo 'Postgresql without environment' . $postgresql->name . 'deleting\n';
echo 'Postgresql without environment: ' . $postgresql->name . ' soft deleting\n';
$postgresql->delete();
continue;
}
if (!$postgresql->destination()) {
echo 'Postgresql without destination' . $postgresql->name . 'deleting\n';
echo 'Postgresql without destination: ' . $postgresql->name . ' soft deleting\n';
$postgresql->delete();
continue;
}
if (!data_get($postgresql, 'destination.server')) {
echo 'Postgresql without server' . $postgresql->name . 'deleting\n';
echo 'Postgresql without server: ' . $postgresql->name . ' soft deleting\n';
$postgresql->delete();
continue;
}
}
} catch (\Throwable $e) {
@@ -148,16 +159,19 @@ class Init extends Command
$redis = StandaloneRedis::all();
foreach ($redis as $redis) {
if (!data_get($redis, 'environment')) {
echo 'Redis without environment' . $redis->name . 'deleting\n';
echo 'Redis without environment: ' . $redis->name . ' soft deleting\n';
$redis->delete();
continue;
}
if (!$redis->destination()) {
echo 'Redis without destination' . $redis->name . 'deleting\n';
echo 'Redis without destination: ' . $redis->name . ' soft deleting\n';
$redis->delete();
continue;
}
if (!data_get($redis, 'destination.server')) {
echo 'Redis without server' . $redis->name . 'deleting\n';
echo 'Redis without server: ' . $redis->name . ' soft deleting\n';
$redis->delete();
continue;
}
}
} catch (\Throwable $e) {
@@ -168,16 +182,19 @@ class Init extends Command
$mongodbs = StandaloneMongodb::all();
foreach ($mongodbs as $mongodb) {
if (!data_get($mongodb, 'environment')) {
echo 'Mongodb without environment' . $mongodb->name . 'deleting\n';
echo 'Mongodb without environment: ' . $mongodb->name . ' soft deleting\n';
$mongodb->delete();
continue;
}
if (!$mongodb->destination()) {
echo 'Mongodb without destination' . $mongodb->name . 'deleting\n';
echo 'Mongodb without destination: ' . $mongodb->name . ' soft deleting\n';
$mongodb->delete();
continue;
}
if (!data_get($mongodb, 'destination.server')) {
echo 'Mongodb without server' . $mongodb->name . 'deleting\n';
echo 'Mongodb without server: ' . $mongodb->name . ' soft deleting\n';
$mongodb->delete();
continue;
}
}
} catch (\Throwable $e) {
@@ -188,16 +205,19 @@ class Init extends Command
$mysqls = StandaloneMysql::all();
foreach ($mysqls as $mysql) {
if (!data_get($mysql, 'environment')) {
echo 'Mysql without environment' . $mysql->name . 'deleting\n';
echo 'Mysql without environment: ' . $mysql->name . ' soft deleting\n';
$mysql->delete();
continue;
}
if (!$mysql->destination()) {
echo 'Mysql without destination' . $mysql->name . 'deleting\n';
echo 'Mysql without destination: ' . $mysql->name . ' soft deleting\n';
$mysql->delete();
continue;
}
if (!data_get($mysql, 'destination.server')) {
echo 'Mysql without server' . $mysql->name . 'deleting\n';
echo 'Mysql without server: ' . $mysql->name . ' soft deleting\n';
$mysql->delete();
continue;
}
}
} catch (\Throwable $e) {
@@ -208,16 +228,19 @@ class Init extends Command
$mariadbs = StandaloneMariadb::all();
foreach ($mariadbs as $mariadb) {
if (!data_get($mariadb, 'environment')) {
echo 'Mariadb without environment' . $mariadb->name . 'deleting\n';
echo 'Mariadb without environment: ' . $mariadb->name . ' soft deleting\n';
$mariadb->delete();
continue;
}
if (!$mariadb->destination()) {
echo 'Mariadb without destination' . $mariadb->name . 'deleting\n';
echo 'Mariadb without destination: ' . $mariadb->name . ' soft deleting\n';
$mariadb->delete();
continue;
}
if (!data_get($mariadb, 'destination.server')) {
echo 'Mariadb without server' . $mariadb->name . 'deleting\n';
echo 'Mariadb without server: ' . $mariadb->name . ' soft deleting\n';
$mariadb->delete();
continue;
}
}
} catch (\Throwable $e) {
@@ -228,16 +251,19 @@ class Init extends Command
$services = Service::all();
foreach ($services as $service) {
if (!data_get($service, 'environment')) {
echo 'Service without environment' . $service->name . 'deleting\n';
echo 'Service without environment: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
if (!$service->destination()) {
echo 'Service without destination' . $service->name . 'deleting\n';
echo 'Service without destination: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
if (!data_get($service, 'server')) {
echo 'Service without server' . $service->name . 'deleting\n';
echo 'Service without server: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
}
} catch (\Throwable $e) {
@@ -247,8 +273,9 @@ class Init extends Command
$serviceApplications = ServiceApplication::all();
foreach ($serviceApplications as $service) {
if (!data_get($service, 'service')) {
echo 'ServiceApplication without service' . $service->name . 'deleting\n';
echo 'ServiceApplication without service: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
}
} catch (\Throwable $e) {
@@ -258,8 +285,9 @@ class Init extends Command
$serviceDatabases = ServiceDatabase::all();
foreach ($serviceDatabases as $service) {
if (!data_get($service, 'service')) {
echo 'ServiceDatabase without service' . $service->name . 'deleting\n';
echo 'ServiceDatabase without service: ' . $service->name . ' soft deleting\n';
$service->delete();
continue;
}
}
} catch (\Throwable $e) {

View File

@@ -56,16 +56,20 @@ class Kernel extends ConsoleKernel
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
$containerServers = $servers->where('settings.is_swarm_worker', false);
} else {
$servers = Server::all()->where('ip', '!=', '1.2.3.4');
$containerServers = $servers->where('settings.is_swarm_worker', false);
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
foreach ($containerServers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
}
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
}
}
private function instance_auto_update($schedule)
{

View File

@@ -39,7 +39,7 @@ class Controller extends BaseController
} else {
$team = $user->teams()->first();
}
if (is_null(data_get($user, 'email_verified_at'))){
if (is_null(data_get($user, 'email_verified_at'))) {
$user->email_verified_at = now();
$user->save();
}
@@ -137,16 +137,28 @@ class Controller extends BaseController
public function acceptInvitation()
{
try {
$invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
$resetPassword = request()->query('reset-password');
$invitationUuid = request()->route('uuid');
$invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail();
$user = User::whereEmail($invitation->email)->firstOrFail();
if (auth()->user()->id !== $user->id) {
abort(401);
}
$invitationValid = $invitation->isValid();
if ($invitationValid) {
if ($resetPassword) {
$user->update([
'password' => Hash::make($invitationUuid),
'force_password_reset' => true
]);
}
if ($user->teams()->where('team_id', $invitation->team->id)->exists()) {
$invitation->delete();
return redirect()->route('team.index');
}
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
refreshSession($invitation->team);
$invitation->delete();
if (auth()->user()?->id !== $user->id) {
return redirect()->route('login');
}
refreshSession($invitation->team);
return redirect()->route('team.index');
} else {
abort(401);

View File

@@ -24,7 +24,7 @@ class CheckForcePasswordReset
}
$force_password_reset = auth()->user()->force_password_reset;
if ($force_password_reset) {
if ($request->routeIs('auth.force-password-reset') || $request->path() === 'livewire/message/force-password-reset') {
if ($request->routeIs('auth.force-password-reset') || $request->path() === 'force-password-reset' || $request->path() === 'livewire/update' || $request->path() === 'logout') {
return $next($request);
}
return redirect()->route('auth.force-password-reset');

View File

@@ -11,6 +11,9 @@ class DecideWhatToDoWithUser
{
public function handle(Request $request, Closure $next): Response
{
if(auth()?->user()?->currentTeam()){
refreshSession(auth()->user()->currentTeam());
}
if (!auth()->user() || !isCloud() || isInstanceAdmin()) {
if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding');

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Str;
class IsBoardingFlow
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// ray()->showQueries()->color('orange');
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) {
return $next($request);
}
return redirect('boarding');
}
return $next($request);
}
}

View File

@@ -1,45 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Str;
class IsSubscriptionValid
{
public function handle(Request $request, Closure $next): Response
{
if (isInstanceAdmin()) {
return $next($request);
}
if (!auth()->user() || !isCloud()) {
if ($request->path() === 'subscription') {
return redirect('/');
} else {
return $next($request);
}
}
if (isSubscriptionActive() && $request->path() === 'subscription') {
// ray('active subscription Middleware');
return redirect('/');
}
if (isSubscriptionOnGracePeriod()) {
// ray('is_subscription_in_grace_period Middleware');
return $next($request);
}
if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) {
// ray('SubscriptionValid Middleware');
if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) {
return $next($request);
}
return redirect('subscription');
} else {
return $next($request);
}
}
return $next($request);
}
}

View File

@@ -75,6 +75,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile';
private string $docker_compose_location = '/docker-compose.yml';
private ?string $docker_compose_custom_start_command = null;
private ?string $docker_compose_custom_build_command = null;
private ?string $addHosts = null;
private ?string $buildTarget = null;
private Collection $saved_outputs;
@@ -215,19 +217,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') {
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage' && !$this->application->destination->server->isSwarm()) {
$this->push_to_docker_registry();
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Creating / updating stack.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && docker stack deploy --with-registry-auth -c docker-compose.yml {$this->application->uuid}")
],
[
"echo 'Stack deployed. It may take a few minutes to fully available in your swarm.'"
]
);
}
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
@@ -267,7 +258,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"ignore_errors" => true,
]
);
ApplicationStatusChanged::dispatch(data_get($this->application,'environment.project.team.id'));
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
}
}
private function push_to_docker_registry()
@@ -299,6 +290,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"echo -n 'Image pushed to docker registry.'"
]);
} catch (Exception $e) {
if ($this->application->destination->server->isSwarm()) {
throw $e;
}
$this->execute_remote_command(
["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"],
);
@@ -432,6 +426,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (data_get($this->application, 'docker_compose_location')) {
$this->docker_compose_location = $this->application->docker_compose_location;
}
if (data_get($this->application, 'docker_compose_custom_start_command')) {
$this->docker_compose_custom_start_command = $this->application->docker_compose_custom_start_command;
}
if (data_get($this->application, 'docker_compose_custom_build_command')) {
$this->docker_compose_custom_build_command = $this->application->docker_compose_custom_build_command;
}
if ($this->pull_request_id === 0) {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
} else {
@@ -454,7 +454,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]);
$this->save_environment_variables();
// Build new container to limit downtime.
$this->build_by_compose_file();
$this->application_deployment_queue->addLogEntry("Pulling & building required images.");
if ($this->docker_compose_custom_build_command) {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_build_command}"), "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
);
}
$this->stop_running_container(force: true);
$networkId = $this->application->uuid;
@@ -488,7 +499,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
);
}
$this->start_by_compose_file();
// Start compose file
if ($this->docker_compose_custom_start_command) {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true],
);
}
$this->application_deployment_queue->addLogEntry("New container started.");
}
private function deploy_dockerfile_buildpack()
{
@@ -575,7 +596,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update()
{
if ($this->server->isSwarm()) {
// Skip this.
if ($this->build_pack !== 'dockerimage') {
$this->push_to_docker_registry();
}
$this->application_deployment_queue->addLogEntry("Rolling update started.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}")
],
);
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
} else {
if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command(
@@ -674,10 +704,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
if ($this->application->destination->server->isSwarm()) {
ray("{$this->workdir}{$this->docker_compose_location}");
$this->push_to_docker_registry();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}-{$this->pull_request_id}")
],
);
} else {
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
}
}
private function create_workdir()
{
@@ -874,6 +914,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$environment_variables = $this->generate_environment_variables($ports);
if (data_get($this->application, 'custom_labels')) {
$this->application->parseContainerLabels();
$labels = collect(preg_split("/\r\n|\n|\r/", base64_decode($this->application->custom_labels)));
$labels = $labels->filter(function ($value, $key) {
return !Str::startsWith($value, 'coolify.');
@@ -940,13 +981,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares');
$docker_compose['services'][$this->container_name]['deploy'] = [
'placement' => [
'constraints' => [
'node.role == worker'
]
],
'mode' => 'replicated',
'replicas' => 1,
'replicas' => data_get($this->application, 'swarm_replicas', 1),
'update_config' => [
'order' => 'start-first'
],
@@ -965,6 +1001,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
]
];
if (data_get($this->application, 'settings.is_swarm_only_worker_nodes')) {
$docker_compose['services'][$this->container_name]['deploy']['placement'] = [
'constraints' => [
'node.role == worker'
]
];
}
if ($this->pull_request_id !== 0) {
$docker_compose['services'][$this->container_name]['deploy']['replicas'] = 1;
}
} else {
$docker_compose['services'][$this->container_name]['labels'] = $labels;
}
@@ -1247,7 +1293,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
}
private function build_by_compose_file() {
private function build_by_compose_file()
{
$this->application_deployment_queue->addLogEntry("Pulling & building required images.");
if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
@@ -1256,15 +1303,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} build"), "hidden" => true],
);
} else {
if ($this->docker_compose_location) {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} build"), "hidden" => true],
);
}
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
);
}
$this->application_deployment_queue->addLogEntry("New images built.");
}
@@ -1339,10 +1380,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
queue_next_deployment($this->application);
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
$this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
if ($status === ApplicationDeploymentStatus::FAILED->value) {
$this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
}
}

View File

@@ -44,7 +44,7 @@ class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted
{
// ray("checking log drain statuses for {$this->server->id}");
try {
if (!$this->server->isServerReady()) {
if (!$this->server->isFunctional()) {
return;
};
$containers = instant_remote_process(["docker container ls -q"], $this->server, false);
@@ -63,19 +63,19 @@ class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted
Sleep::for(10)->seconds();
if ($this->healthcheck()) {
if ($this->server->log_drain_notification_sent) {
$this->server->team->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->update(['log_drain_notification_sent' => false]);
}
return;
}
if (!$this->server->log_drain_notification_sent) {
ray('Log drain container still unhealthy. Sending notification...');
$this->server->team->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null));
$this->server->team?->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null));
$this->server->update(['log_drain_notification_sent' => true]);
}
} else {
if ($this->server->log_drain_notification_sent) {
$this->server->team->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->update(['log_drain_notification_sent' => false]);
}
}

View File

@@ -22,32 +22,35 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 4;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->id;
return $this->server->uuid;
}
public function __construct(public Server $server)
{
$this->handle();
// $this->handle();
}
public function handle()
{
// ray("checking container statuses for {$this->server->id}");
if (!$this->server->isServerReady($this->tries)) {
throw new \RuntimeException('Server is not reachable.');
};
try {
if (!$this->server->isServerReady()) {
return;
};
if ($this->server->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
$containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
} else {
// Precheck for containers
$containers = instant_remote_process(["docker container ls -q"], $this->server, false);
@@ -55,15 +58,15 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = null;
$containerReplicates = null;
}
if (is_null($containers)) {
return;
}
$containers = format_docker_command_output_to_json($containers);
if ($containerReplicase) {
$containerReplicase = format_docker_command_output_to_json($containerReplicase);
foreach ($containerReplicase as $containerReplica) {
if ($containerReplicates) {
$containerReplicates = format_docker_command_output_to_json($containerReplicates);
foreach ($containerReplicates as $containerReplica) {
$name = data_get($containerReplica, 'Name');
$containers = $containers->map(function ($container) use ($name, $containerReplica) {
if (data_get($container, 'Spec.Name') === $name) {

View File

@@ -286,7 +286,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->team->notify(new BackupSuccess($this->backup, $this->database));
$this->team?->notify(new BackupSuccess($this->backup, $this->database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
@@ -302,7 +302,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
]);
}
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e;
}
}

View File

@@ -32,9 +32,10 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
try {
$server = $this->resource->destination->server;
if (!$server->isFunctional()) {
$this->resource->delete();
$this->resource->forceDelete();
return 'Server is not functional';
}
$this->resource->delete();
switch ($this->resource->type()) {
case 'application':
StopApplication::run($this->resource);
@@ -56,10 +57,9 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
break;
}
if ($this->resource->type() === 'service') {
$this->resource->delete();
DeleteService::dispatch($this->resource);
} else {
$this->resource->delete();
$this->resource->forceDelete();
}
} catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());

View File

@@ -17,30 +17,38 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public ?int $disk_usage = null;
public $tries = 4;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->id;
return $this->server->uuid;
}
public function handle(): void
public function handle()
{
ray("checking server status for {$this->server->id}");
if (!$this->server->isServerReady(4)) {
throw new \RuntimeException('Server is not reachable.');
};
try {
if ($this->server->isServerReady()) {
if ($this->server->isFunctional()) {
$this->cleanup(notify: false);
}
} catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
handleError($e);
return handleError($e);
}
}
public function cleanup(bool $notify = false): void
@@ -54,7 +62,7 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
} else {
$this->server->high_disk_usage_notification_sent = true;
$this->server->save();
$this->server->team->notify(new HighDiskUsage($this->server, $this->disk_usage, $this->server->settings->cleanup_after_percentage));
$this->server->team?->notify(new HighDiskUsage($this->server, $this->disk_usage, $this->server->settings->cleanup_after_percentage));
}
} else {
DockerCleanupJob::dispatchSync($this->server);

View File

@@ -4,29 +4,32 @@ namespace App\Livewire\Destination\New;
use App\Models\Server;
use App\Models\StandaloneDocker as ModelsStandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class StandaloneDocker extends Component
class Docker extends Component
{
public string $name;
public string $network;
public Collection $servers;
public Server $server;
public int|null $server_id = null;
public ?int $server_id = null;
public bool $is_swarm = false;
protected $rules = [
'name' => 'required|string',
'network' => 'required|string',
'server_id' => 'required|integer'
'server_id' => 'required|integer',
'is_swarm' => 'boolean'
];
protected $validationAttributes = [
'name' => 'name',
'network' => 'network',
'server_id' => 'server'
'server_id' => 'server',
'is_swarm' => 'swarm'
];
public function mount()
@@ -43,13 +46,13 @@ class StandaloneDocker extends Component
} else {
$this->network = new Cuid2(7);
}
$this->name = Str::kebab("{$this->servers->first()->name}-{$this->network}");
$this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab();
}
public function generate_name()
{
$this->server = Server::find($this->server_id);
$this->name = Str::kebab("{$this->server->name}-{$this->network}");
$this->name = str("{$this->server->name}-{$this->network}")->kebab();
}
public function submit()
@@ -57,17 +60,30 @@ class StandaloneDocker extends Component
$this->validate();
try {
$this->server = Server::find($this->server_id);
$found = $this->server->standaloneDockers()->where('network', $this->network)->first();
if ($found) {
$this->createNetworkAndAttachToProxy();
$this->dispatch('error', 'Network already added to this server.');
return;
if ($this->is_swarm) {
$found = $this->server->swarmDockers()->where('network', $this->network)->first();
if ($found) {
$this->dispatch('error', 'Network already added to this server.');
return;
} else {
$docker = SwarmDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->server_id,
]);
}
} else {
$docker = ModelsStandaloneDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->server_id,
]);
$found = $this->server->standaloneDockers()->where('network', $this->network)->first();
if ($found) {
$this->dispatch('error', 'Network already added to this server.');
return;
} else {
$docker = ModelsStandaloneDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->server_id,
]);
}
}
$this->createNetworkAndAttachToProxy();
return $this->redirectRoute('destination.show', $docker->uuid, navigate: true);

View File

@@ -13,7 +13,11 @@ class Show extends Component
public function scan()
{
$alreadyAddedNetworks = $this->server->standaloneDockers;
if ($this->server->isSwarm()) {
$alreadyAddedNetworks = $this->server->swarmDockers;
} else {
$alreadyAddedNetworks = $this->server->standaloneDockers;
}
$networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false);
$this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) {
return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none';

View File

@@ -52,7 +52,7 @@ class DiscordSettings extends Component
public function sendTestNotification()
{
$this->team->notify(new Test());
$this->team?->notify(new Test());
$this->dispatch('success', 'Test notification sent.');
}
}

View File

@@ -70,7 +70,7 @@ class EmailSettings extends Component
}
public function sendTestNotification()
{
$this->team->notify(new Test($this->emails));
$this->team?->notify(new Test($this->emails));
$this->dispatch('success', 'Test Email sent successfully.');
}
public function instantSaveInstance()

View File

@@ -58,7 +58,7 @@ class TelegramSettings extends Component
public function sendTestNotification()
{
$this->team->notify(new Test());
$this->team?->notify(new Test());
$this->dispatch('success', 'Test notification sent.');
}
}

View File

@@ -64,6 +64,8 @@ class General extends Component
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_command' => 'nullable',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -94,6 +96,8 @@ class General extends Component
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
'application.settings.is_static' => 'Is static',
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
];
public function mount()
{
@@ -109,17 +113,7 @@ class General extends Component
$this->application->isConfigurationChanged(true);
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if (base64_encode(base64_decode(data_get($this->application, 'custom_labels'), true)) === data_get($this->application, 'custom_labels')) {
ray('custom_labels is base64 encoded');
} else {
ray('custom_labels is not base64 encoded');
$this->application->custom_labels = str($this->application->custom_labels)->replace(',', "\n");
$this->application->custom_labels = base64_encode(data_get($this->application, 'custom_labels'));
$this->application->save();
}
$this->customLabels = base64_decode(data_get($this->application, 'custom_labels'));
$this->customLabels = $this->application->parseContainerLabels();
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->checkLabelUpdates();
}
@@ -156,7 +150,6 @@ class General extends Component
$this->parsedServiceDomains[$serviceName]['domain'] = $domain;
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->application->save();
$this->dispatch('success', 'Domain generated.');
}
return $domain;
}
@@ -206,7 +199,8 @@ class General extends Component
public function submit($showToaster = true)
{
try {
if ($this->application->build_pack === 'dockercompose' && ($this->initialDockerComposeLocation !== $this->application->docker_compose_location || $this->initialDockerComposePrLocation !== $this->application->docker_compose_pr_location)) {
ray($this->initialDockerComposeLocation, $this->application->docker_compose_location);
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
$this->loadComposeFile();
}
$this->validate();
@@ -242,7 +236,6 @@ class General extends Component
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->parsedServices = $this->application->parseCompose();
}
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
$showToaster && $this->dispatch('success', 'Application settings updated!');

View File

@@ -73,6 +73,10 @@ class Heading extends Component
$this->dispatch('error', 'Please load a Compose file first.');
return;
}
if ($this->application->destination->server->isSwarm() && is_null($this->application->docker_registry_image_name)) {
$this->dispatch('error', 'Please set a Docker image name first.');
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,

View File

@@ -72,10 +72,14 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
} else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
}
}
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
$this->application->refresh();

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Livewire\Project\Application;
use App\Models\Application;
use Livewire\Component;
class Swarm extends Component
{
public Application $application;
public string $swarm_placement_constraints = '';
protected $rules = [
'application.swarm_replicas' => 'required',
'application.swarm_placement_constraints' => 'nullable',
'application.settings.is_swarm_only_worker_nodes' => 'required',
];
public function mount() {
if ($this->application->swarm_placement_constraints) {
$this->swarm_placement_constraints = base64_decode($this->application->swarm_placement_constraints);
}
}
public function instantSave() {
try {
$this->validate();
$this->application->settings->save();
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit() {
try {
$this->validate();
if ($this->swarm_placement_constraints) {
$this->application->swarm_placement_constraints = base64_encode($this->swarm_placement_constraints);
} else {
$this->application->swarm_placement_constraints = null;
}
$this->application->save();
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.application.swarm');
}
}

View File

@@ -19,12 +19,14 @@ class CloneProject extends Component
public $servers;
public ?Environment $environment = null;
public ?int $selectedServer = null;
public ?int $selectedDestination = null;
public ?Server $server = null;
public $resources = [];
public string $newProjectName = '';
protected $messages = [
'selectedServer' => 'Please select a server.',
'selectedDestination' => 'Please select a server & destination.',
'newProjectName' => 'Please enter a name for the new project.',
];
public function mount($project_uuid)
@@ -34,7 +36,7 @@ class CloneProject extends Component
$this->environment = $this->project->environments->where('name', $this->environment_name)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()->servers;
$this->newProjectName = $this->project->name . ' (clone)';
$this->newProjectName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug();
}
public function render()
@@ -42,9 +44,10 @@ class CloneProject extends Component
return view('livewire.project.clone-project');
}
public function selectServer($server_id)
public function selectServer($server_id, $destination_id)
{
$this->selectedServer = $server_id;
$this->selectedDestination = $destination_id;
$this->server = $this->servers->where('id', $server_id)->first();
}
@@ -52,7 +55,7 @@ class CloneProject extends Component
{
try {
$this->validate([
'selectedServer' => 'required',
'selectedDestination' => 'required',
'newProjectName' => 'required',
]);
$foundProject = Project::where('name', $this->newProjectName)->first();
@@ -81,7 +84,8 @@ class CloneProject extends Component
'fqdn' => generateFqdn($this->server, $uuid),
'status' => 'exited',
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
// This is not correct, but we need to set it to something
'destination_id' => $this->selectedDestination,
]);
$newApplication->save();
$environmentVaribles = $application->environment_variables()->get();
@@ -107,7 +111,7 @@ class CloneProject extends Component
'status' => 'exited',
'started_at' => null,
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
'destination_id' => $this->selectedDestination,
]);
$newDatabase->save();
$environmentVaribles = $database->environment_variables()->get();
@@ -133,7 +137,7 @@ class CloneProject extends Component
$newService = $service->replicate()->fill([
'uuid' => $uuid,
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
'destination_id' => $this->selectedDestination,
]);
$newService->save();
foreach ($newService->applications() as $application) {

View File

@@ -8,7 +8,7 @@ use Livewire\Component;
class BackupExecutions extends Component
{
public $backup;
public $executions;
public $executions = [];
public $setDeletableBackup;
public function getListeners()
{
@@ -65,6 +65,6 @@ class BackupExecutions extends Component
}
public function refreshBackupExecutions(): void
{
$this->executions = $this->backup->executions;
$this->executions = data_get($this->backup, 'executions', []);
}
}

View File

@@ -6,22 +6,24 @@ use App\Models\Project;
use App\Models\Server;
use Countable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class Select extends Component
{
public $current_step = 'type';
public ?int $server = null;
public ?Server $server = null;
public string $type;
public string $server_id;
public string $destination_uuid;
public Countable|array|Server $allServers = [];
public Countable|array|Server $servers = [];
public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters;
public Collection|array $services = [];
public Collection|array $allServices = [];
public bool $isDatabase = false;
public bool $includeSwarm = true;
public bool $loadingServices = true;
public bool $loading = false;
@@ -31,7 +33,7 @@ class Select extends Component
public ?string $search = null;
protected $queryString = [
'server',
'server_id',
'search'
];
@@ -97,21 +99,45 @@ class Select extends Component
$this->loadingServices = false;
}
}
public function instantSave()
{
if ($this->includeSwarm) {
$this->servers = $this->allServers;
} else {
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false);
}
}
public function setType(string $type)
{
$this->type = $type;
if ($this->loading) return;
$this->loading = true;
$this->type = $type;
switch ($type) {
case 'postgresql':
case 'mysql':
case 'mariadb':
case 'redis':
case 'mongodb':
$this->isDatabase = true;
$this->includeSwarm = false;
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false);
break;
}
if (str($type)->startsWith('one-click-service') || str($type)->startsWith('docker-compose-empty') || str($type)->startsWith('docker-image')) {
$this->isDatabase = true;
$this->includeSwarm = false;
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false);
}
if ($type === "existing-postgresql") {
$this->current_step = $type;
return;
}
if (count($this->servers) === 1) {
$server = $this->servers->first();
$this->setServer($server);
}
// if (count($this->servers) === 1) {
// $server = $this->servers->first();
// $this->setServer($server);
// }
if (!is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server)->first();
$foundServer = $this->servers->where('id', $this->server->id)->first();
if ($foundServer) {
return $this->setServer($foundServer);
}
@@ -122,6 +148,7 @@ class Select extends Component
public function setServer(Server $server)
{
$this->server_id = $server->id;
$this->server = $server;
$this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers;
$this->current_step = 'destinations';
@@ -142,5 +169,6 @@ class Select extends Component
public function loadServers()
{
$this->servers = Server::isUsable()->get();
$this->allServers = $this->servers;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Service;
use App\Actions\Shared\PullImage;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Events\ServiceStatusChanged;
@@ -69,4 +70,18 @@ class Navbar extends Component
}
ServiceStatusChanged::dispatch();
}
public function restart()
{
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
return;
}
PullImage::run($this->service);
$this->dispatch('image-pulled');
StopService::run($this->service);
$this->service->parse();
$activity = StartService::run($this->service);
$this->dispatch('newMonitorActivity', $activity->id);
}
}

View File

@@ -65,7 +65,11 @@ class Show extends Component
public function delete()
{
$this->env->delete();
$this->dispatch('refreshEnvs');
try {
$this->env->delete();
$this->dispatch('refreshEnvs');
} catch (\Exception $e) {
return handleError($e);
}
}
}

View File

@@ -73,9 +73,17 @@ class GetLogs extends Component
if (!$refresh && $this->resource?->getMorphClass() === 'App\Models\Service') return;
if ($this->container) {
if ($this->showTimeStamps) {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
if ($this->server->isSwarm()) {
$sshCommand = generateSshCommand($this->server, "docker service logs -n {$this->numberOfLines} -t {$this->container}");
} else {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
}
} else {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} {$this->container}");
if ($this->server->isSwarm()) {
$sshCommand = generateSshCommand($this->server, "docker service logs -n {$this->numberOfLines} {$this->container}");
} else {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} {$this->container}");
}
}
if ($refresh) {
$this->outputs = '';

View File

@@ -34,7 +34,15 @@ class Logs extends Component
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($this->server->isSwarm()) {
$containers = collect([
[
'Names' => $this->resource->uuid . '_' . $this->resource->uuid,
]
]);
} else {
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
}
if ($containers->count() > 0) {
$containers->each(function ($container) {
$this->containers->push(str_replace('/', '', $container['Names']));
@@ -62,7 +70,7 @@ class Logs extends Component
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid;
if (str(data_get($this,'resource.status'))->startsWith('running')) {
if (str(data_get($this, 'resource.status'))->startsWith('running')) {
$this->containers->push($this->container);
}
} else if (data_get($this->parameters, 'service_uuid')) {

View File

@@ -2,22 +2,26 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\Application;
use Livewire\Component;
class Add extends Component
{
public $uuid;
public $parameters;
public $isSwarm = false;
public string $name;
public string $mount_path;
public string|null $host_path = null;
public ?string $host_path = null;
protected $listeners = ['clearAddStorage' => 'clear'];
protected $rules = [
public $rules = [
'name' => 'required|string',
'mount_path' => 'required|string',
'host_path' => 'string|nullable',
];
protected $listeners = ['clearAddStorage' => 'clear'];
protected $validationAttributes = [
'name' => 'name',
'mount_path' => 'mount',
@@ -27,17 +31,33 @@ class Add extends Component
public function mount()
{
$this->parameters = get_route_parameters();
if (data_get($this->parameters, 'application_uuid')) {
$applicationUuid = $this->parameters['application_uuid'];
$application = Application::where('uuid', $applicationUuid)->first();
if (!$application) {
abort(404);
}
if ($application->destination->server->isSwarm()) {
$this->isSwarm = true;
$this->rules['host_path'] = 'required|string';
}
}
}
public function submit()
{
$this->validate();
$name = $this->uuid . '-' . $this->name;
$this->dispatch('addNewVolume', [
'name' => $name,
'mount_path' => $this->mount_path,
'host_path' => $this->host_path,
]);
try {
$this->validate($this->rules);
$name = $this->uuid . '-' . $this->name;
$this->dispatch('addNewVolume', [
'name' => $name,
'mount_path' => $this->mount_path,
'host_path' => $this->host_path,
]);
$this->dispatch('closeStorageModal');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function clear()

View File

@@ -25,7 +25,7 @@ class Form extends Component
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
'server.settings.is_reachable' => 'required',
'server.settings.is_swarm_manager' => 'required|boolean',
// 'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean',
'wildcard_domain' => 'nullable|url',
];
protected $validationAttributes = [
@@ -37,16 +37,13 @@ class Form extends Component
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'Is reachable',
'server.settings.is_swarm_manager' => 'Swarm Manager',
// 'server.settings.is_swarm_worker' => 'Swarm Worker',
'server.settings.is_swarm_worker' => 'Swarm Worker',
];
public function mount()
{
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
if (!$this->server->isFunctional()) {
$this->validateServer();
}
}
public function serverRefresh($install = true)
{

View File

@@ -22,7 +22,10 @@ class ByIp extends Component
public string $user = 'root';
public int $port = 22;
public bool $is_swarm_manager = false;
public bool $is_swarm_worker = false;
public $selected_swarm_cluster = null;
public $swarm_managers = [];
protected $rules = [
'name' => 'required|string',
'description' => 'nullable|string',
@@ -30,6 +33,7 @@ class ByIp extends Component
'user' => 'required|string',
'port' => 'required|integer',
'is_swarm_manager' => 'required|boolean',
'is_swarm_worker' => 'required|boolean',
];
protected $validationAttributes = [
'name' => 'Name',
@@ -38,12 +42,17 @@ class ByIp extends Component
'user' => 'User',
'port' => 'Port',
'is_swarm_manager' => 'Swarm Manager',
'is_swarm_worker' => 'Swarm Worker',
];
public function mount()
{
$this->name = generate_random_name();
$this->private_key_id = $this->private_keys->first()->id;
$this->swarm_managers = Server::isUsable()->get()->where('settings.is_swarm_manager', true);
if ($this->swarm_managers->count() > 0) {
$this->selected_swarm_cluster = $this->swarm_managers->first()->id;
}
}
public function setPrivateKey(string $private_key_id)
@@ -53,7 +62,7 @@ class ByIp extends Component
public function instantSave()
{
$this->dispatch('success', 'Application settings updated!');
// $this->dispatch('success', 'Application settings updated!');
}
public function submit()
@@ -63,7 +72,7 @@ class ByIp extends Component
if (is_null($this->private_key_id)) {
return $this->dispatch('error', 'You must select a private key');
}
$server = Server::create([
$payload = [
'name' => $this->name,
'description' => $this->description,
'ip' => $this->ip,
@@ -75,11 +84,16 @@ class ByIp extends Component
"type" => ProxyTypes::TRAEFIK_V2->value,
"status" => ProxyStatus::EXITED->value,
],
]);
];
if ($this->is_swarm_worker) {
$payload['swarm_cluster'] = $this->selected_swarm_cluster;
}
$server = Server::create($payload);
$server->settings->is_swarm_manager = $this->is_swarm_manager;
$server->settings->is_swarm_worker = $this->is_swarm_worker;
$server->settings->save();
$server->addInitialNetwork();
return $this->redirectRoute('server.show', $server->uuid, navigate: true);
return $this->redirectRoute('server.show', $server->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -106,7 +106,7 @@ class Email extends Component
public function sendTestNotification()
{
$this->settings->notify(new Test($this->emails));
$this->settings?->notify(new Test($this->emails));
$this->dispatch('success', 'Test email sent.');
}
}

View File

@@ -2,8 +2,10 @@
namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str;
@@ -13,6 +15,7 @@ use Visus\Cuid2\Cuid2;
class Application extends BaseModel
{
use SoftDeletes;
protected $guarded = [];
protected static function booted()
@@ -338,7 +341,7 @@ class Application extends BaseModel
}
public function isDeploymentInprogress()
{
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::IN_PROGRESS)->where('status', ApplicationDeploymentStatus::QUEUED)->count();
if ($deployments > 0) {
return true;
}
@@ -963,65 +966,98 @@ class Application extends BaseModel
function loadComposeFile($isInit = false)
{
$initialDockerComposeLocation = $this->docker_compose_location;
// $initialDockerComposePrLocation = $this->docker_compose_pr_location;
if ($this->build_pack === 'dockercompose') {
if ($isInit && $this->docker_compose_raw) {
return;
}
$uuid = new Cuid2();
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;
// $prComposeFile = $this->docker_compose_pr_location;
$fileList = collect([".$workdir$composeFile"]);
// if ($composeFile !== $prComposeFile) {
// $fileList->push(".$prComposeFile");
// }
$commands = collect([
"mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}",
$cloneCommand,
"git sparse-checkout init --cone",
"git sparse-checkout set {$fileList->implode(' ')}",
"git read-tree -mu HEAD",
"cat .$workdir$composeFile",
]);
$composeFileContent = instant_remote_process($commands, $this->destination->server, false);
if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation;
$this->save();
throw new \Exception("Could not load base compose file from $workdir$composeFile");
} else {
$this->docker_compose_raw = $composeFileContent;
$this->save();
}
// if ($composeFile === $prComposeFile) {
// $this->docker_compose_pr_raw = $composeFileContent;
// $this->save();
// } else {
// $commands = collect([
// "cd /tmp/{$uuid}",
// "cat .$workdir$prComposeFile",
// ]);
// $composePrFileContent = instant_remote_process($commands, $this->destination->server, false);
// if (!$composePrFileContent) {
// $this->docker_compose_pr_location = $initialDockerComposePrLocation;
// $this->save();
// throw new \Exception("Could not load compose file from $workdir$prComposeFile");
// } else {
// $this->docker_compose_pr_raw = $composePrFileContent;
// $this->save();
// }
// }
$commands = collect([
"rm -rf /tmp/{$uuid}",
]);
instant_remote_process($commands, $this->destination->server, false);
return [
'parsedServices' => $this->parseCompose(),
'initialDockerComposeLocation' => $this->docker_compose_location,
'initialDockerComposePrLocation' => $this->docker_compose_pr_location,
];
if ($isInit && $this->docker_compose_raw) {
return;
}
$uuid = new Cuid2();
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;
// $prComposeFile = $this->docker_compose_pr_location;
$fileList = collect([".$workdir$composeFile"]);
// if ($composeFile !== $prComposeFile) {
// $fileList->push(".$prComposeFile");
// }
$commands = collect([
"mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}",
$cloneCommand,
"git sparse-checkout init --cone",
"git sparse-checkout set {$fileList->implode(' ')}",
"git read-tree -mu HEAD",
"cat .$workdir$composeFile",
]);
$composeFileContent = instant_remote_process($commands, $this->destination->server, false);
if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation;
$this->save();
throw new \Exception("Could not load base compose file from $workdir$composeFile");
} else {
$this->docker_compose_raw = $composeFileContent;
$this->save();
}
// if ($composeFile === $prComposeFile) {
// $this->docker_compose_pr_raw = $composeFileContent;
// $this->save();
// } else {
// $commands = collect([
// "cd /tmp/{$uuid}",
// "cat .$workdir$prComposeFile",
// ]);
// $composePrFileContent = instant_remote_process($commands, $this->destination->server, false);
// if (!$composePrFileContent) {
// $this->docker_compose_pr_location = $initialDockerComposePrLocation;
// $this->save();
// throw new \Exception("Could not load compose file from $workdir$prComposeFile");
// } else {
// $this->docker_compose_pr_raw = $composePrFileContent;
// $this->save();
// }
// }
$commands = collect([
"rm -rf /tmp/{$uuid}",
]);
instant_remote_process($commands, $this->destination->server, false);
$parsedServices = $this->parseCompose();
if ($this->docker_compose_domains) {
$json = collect(json_decode($this->docker_compose_domains));
$names = collect(data_get($parsedServices, 'services'))->keys()->toArray();
$jsonNames = $json->keys()->toArray();
$diff = array_diff($jsonNames, $names);
$json = $json->filter(function ($value, $key) use ($diff) {
return !in_array($key, $diff);
});
if ($json) {
$this->docker_compose_domains = json_encode($json);
} else {
$this->docker_compose_domains = null;
}
$this->save();
}
return [
'parsedServices' => $parsedServices,
'initialDockerComposeLocation' => $this->docker_compose_location,
'initialDockerComposePrLocation' => $this->docker_compose_pr_location,
];
}
function parseContainerLabels(?ApplicationPreview $preview = null)
{
$customLabels = data_get($this, 'custom_labels');
if (!$customLabels) {
return;
}
if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) {
ray('custom_labels is not base64 encoded');
$this->custom_labels = str($customLabels)->replace(',', "\n");
$this->custom_labels = base64_encode($customLabels);
}
$customLabels = base64_decode($this->custom_labels);
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
ray('custom_labels contains non-ascii characters');
$customLabels = str(implode(",", generateLabelsApplication($this, $preview)))->replace(',', "\n");
}
$this->custom_labels = base64_encode($customLabels);
$this->save();
return $customLabels;
}
}

View File

@@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
@@ -72,7 +71,7 @@ class Server extends BaseModel
static public function isUsable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true);
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false);
}
static public function destinationsByServer(string $server_id)
@@ -150,29 +149,40 @@ class Server extends BaseModel
}
return false;
}
public function isServerReady()
public function isServerReady(int $tries = 3)
{
$serverUptimeCheckNumber = $this->unreachable_count;
$serverUptimeCheckNumberMax = 3;
if ($this->skipServer()) {
return false;
}
if ($this->unreachable_count < $tries) {
$serverUptimeCheckNumber = $this->unreachable_count + 1;
}
$serverUptimeCheckNumberMax = $tries;
$currentTime = now()->timestamp;
$runtime = 30;
ray('server: ' . $this->name);
ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber);
ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax);
$isReady = false;
// Run for 30 seconds max and check every 5 seconds for 3 times
while ($currentTime + $runtime > now()->timestamp) {
$result = $this->validateConnection();
if ($result) {
if ($this->unreachable_notification_sent === true) {
$this->update(['unreachable_notification_sent' => false]);
}
return true;
} else {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
// Reached max number of retries
if ($this->unreachable_notification_sent === false) {
ray('Server unreachable, sending notification...');
$this->team->notify(new Unreachable($this));
$this->team?->notify(new Unreachable($this));
$this->update(['unreachable_notification_sent' => true]);
}
$this->settings()->update([
'is_reachable' => false,
]);
$this->update([
'unreachable_count' => 0,
]);
if ($this->settings->is_reachable === true) {
$this->settings()->update([
'is_reachable' => false,
]);
}
foreach ($this->applications() as $application) {
$application->update(['status' => 'exited']);
}
@@ -189,23 +199,13 @@ class Server extends BaseModel
$db->update(['status' => 'exited']);
}
}
$isReady = false;
break;
}
$result = $this->validateConnection();
// ray('validateConnection: ' . $result);
if (!$result) {
$serverUptimeCheckNumber++;
} else {
$this->update([
'unreachable_count' => $serverUptimeCheckNumber,
'unreachable_count' => $this->unreachable_count + 1,
]);
Sleep::for(5)->seconds();
return;
}
$isReady = true;
break;
return false;
}
return $isReady;
}
public function getDiskUsage()
{
@@ -379,9 +379,20 @@ class Server extends BaseModel
{
return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker');
}
public function isSwarmManager()
{
return data_get($this, 'settings.is_swarm_manager');
}
public function isSwarmWorker()
{
return data_get($this, 'settings.is_swarm_worker');
}
public function validateConnection()
{
$server = Server::find($this->id);
if (!$server) {
return false;
}
if ($server->skipServer()) {
return false;
}
@@ -401,7 +412,7 @@ class Server extends BaseModel
}
if (data_get($server, 'unreachable_notification_sent') === true) {
$server->team->notify(new Revived($server));
$server->team?->notify(new Revived($server));
$server->update(['unreachable_notification_sent' => false]);
}

View File

@@ -4,11 +4,12 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
class ServiceApplication extends BaseModel
{
use HasFactory;
use HasFactory, SoftDeletes;
protected $guarded = [];
protected static function booted()

View File

@@ -3,10 +3,11 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class ServiceDatabase extends BaseModel
{
use HasFactory;
use HasFactory, SoftDeletes;
protected $guarded = [];
protected static function booted()

View File

@@ -6,10 +6,11 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMariadb extends BaseModel
{
use HasFactory;
use HasFactory,SoftDeletes;
protected $guarded = [];
protected $casts = [

View File

@@ -5,10 +5,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMongodb extends BaseModel
{
use HasFactory;
use HasFactory, SoftDeletes;
protected $guarded = [];
protected static function booted()

View File

@@ -5,10 +5,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMysql extends BaseModel
{
use HasFactory;
use HasFactory, SoftDeletes;
protected $guarded = [];
protected $casts = [

View File

@@ -5,10 +5,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandalonePostgresql extends BaseModel
{
use HasFactory;
use HasFactory, SoftDeletes;
protected $guarded = [];
protected $casts = [

View File

@@ -5,10 +5,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneRedis extends BaseModel
{
use HasFactory;
use HasFactory, SoftDeletes;
protected $guarded = [];
protected static function booted()

View File

@@ -98,7 +98,7 @@ class User extends Authenticatable implements SendsEmail
}
public function sendPasswordResetNotification($token): void
{
$this->notify(new TransactionalEmailsResetPassword($token));
$this?->notify(new TransactionalEmailsResetPassword($token));
}
public function isAdmin()

View File

@@ -54,7 +54,7 @@ class Revived extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
$message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
return $message;
}
public function toTelegram(): array

View File

@@ -43,7 +43,7 @@ class Unreachable extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Server ({$this->server->name}) is unreachable after trying to connect to it 3 times");
$mail->subject("Coolify: Your server ({$this->server->name}) is unreachable.");
$mail->view('emails.server-lost-connection', [
'name' => $this->server->name,
]);
@@ -52,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 3 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
$message = "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 3 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
"message" => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
];
}
}

View File

@@ -14,20 +14,23 @@ use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection
{
$containers = collect([]);
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) use ($pullRequestId) {
$labels = data_get($container, 'Labels');
if (!str($labels)->contains("coolify.pullRequestId=")) {
data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
return $container;
}
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
return $container;
}
return null;
});
$containers = $containers->filter();
if (!$server->isSwarm()) {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) use ($pullRequestId) {
$labels = data_get($container, 'Labels');
if (!str($labels)->contains("coolify.pullRequestId=")) {
data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
return $container;
}
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
return $container;
}
return null;
});
$containers = $containers->filter();
return $containers;
}
return $containers;
}

View File

@@ -15,10 +15,16 @@ function get_proxy_path()
}
function connectProxyToNetworks(Server $server)
{
// Standalone networks
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
});
if ($server->isSwarm()) {
$networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network'];
});
} else {
// Standalone networks
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
});
}
// Service networks
foreach ($server->services()->get() as $service) {
$networks->push($service->networks());
@@ -41,16 +47,30 @@ function connectProxyToNetworks(Server $server)
$networks->push($network);
}
$networks = collect($networks)->flatten()->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify']);
if ($server->isSwarm()) {
if ($networks->count() === 0) {
$networks = collect(['coolify-overlay']);
}
$commands = $networks->map(function ($network) {
return [
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
];
});
} else {
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
$commands = $networks->map(function ($network) {
return [
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
];
});
}
$commands = $networks->map(function ($network) {
return [
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
];
});
return $commands->flatten();
}
function generate_default_proxy_configuration(Server $server)
@@ -60,14 +80,18 @@ function generate_default_proxy_configuration(Server $server)
$networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify-overlay']);
}
} else {
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
}
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
$array_of_networks = collect([]);
$networks->map(function ($network) use ($array_of_networks) {
$array_of_networks[$network] = [

View File

@@ -969,10 +969,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
}
} else {
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'service_id' => $resource->id,
])->first();
if ($value->contains(':-')) {
$key = $value->before(':');
$defaultValue = $value->after(':-');
@@ -989,6 +985,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$key = $value;
$defaultValue = null;
}
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'service_id' => $resource->id,
])->first();
if ($foundEnv) {
$defaultValue = data_get($foundEnv, 'value');
}
@@ -1330,7 +1330,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($value?->startsWith('$')) {
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'service_id' => $resource->id,
'application_id' => $resource->id,
'is_preview' => false,
])->first();
$value = Str::of(replaceVariables($value));
$key = $value;
@@ -1397,14 +1398,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$defaultValue = data_get($foundEnv, 'value');
}
$isBuildTime = data_get($foundEnv, 'is_build_time', false);
EnvironmentVariable::updateOrCreate([
'key' => $key->value(),
'application_id' => $resource->id,
], [
'value' => $defaultValue,
'is_build_time' => $isBuildTime,
'application_id' => $resource->id,
]);
if ($foundEnv) {
$foundEnv->update([
'key' => $key,
'application_id' => $resource->id,
'is_build_time' => $isBuildTime,
'value' => $defaultValue,
]);
} else {
EnvironmentVariable::create([
'key' => $key,
'value' => $defaultValue,
'is_build_time' => $isBuildTime,
'application_id' => $resource->id,
'is_preview' => false,
]);
}
}
}
}
@@ -1544,6 +1553,9 @@ function generateEnvValue(string $command)
case 'USER':
$generatedValue = Str::random(16);
break;
default:
$generatedValue = Str::random(16);
break;
}
return $generatedValue;
}

View File

@@ -128,11 +128,6 @@ function allowedPathsForUnsubscribedAccounts()
'logout',
'waitlist',
'force-password-reset',
// 'livewire/message/force-password-reset',
// 'livewire/message/check-license',
// 'livewire/message/switch-team',
// 'livewire/message/subscription.pricing-plans',
// 'livewire/message/help',
'livewire/update'
];
}
@@ -141,8 +136,6 @@ function allowedPathsForBoardingAccounts()
return [
...allowedPathsForUnsubscribedAccounts(),
'boarding',
// 'livewire/message/boarding.index',
// 'livewire/message/activity-monitor',
'livewire/update'
];
}
@@ -151,9 +144,6 @@ function allowedPathsForInvalidAccounts() {
'logout',
'verify',
'force-password-reset',
// 'livewire/message/force-password-reset',
// 'livewire/message/verify-email',
// 'livewire/message/help',
'livewire/update'
];
}

View File

@@ -216,7 +216,7 @@ return [
],
'long-running' => [
'autoScalingStrategy' => 'size',
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 2),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
@@ -231,7 +231,7 @@ return [
],
'long-running' => [
'autoScalingStrategy' => 'size',
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 2),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],

View File

@@ -3,11 +3,11 @@
return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://bea22abf110618b07252032aa2e07859@o1082494.ingest.sentry.io/4505347448045568',
'dsn' => 'https://1bbc8f762199a52aee39196adb3e8d1a@o1082494.ingest.sentry.io/4505347448045568',
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.158',
'release' => '4.0.0-beta.169',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.158';
return '4.0.0-beta.169';

View File

@@ -1,23 +0,0 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\StandaloneMongodb>
*/
class StandaloneMongodbFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,71 @@
<?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->softDeletes();
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('service_applications', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('service_databases', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropSoftDeletes();
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->dropSoftDeletes();
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->dropSoftDeletes();
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->dropSoftDeletes();
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->dropSoftDeletes();
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->dropSoftDeletes();
});
Schema::table('service_applications', function (Blueprint $table) {
$table->dropSoftDeletes();
});
Schema::table('service_databases', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

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

View File

@@ -0,0 +1,36 @@
<?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->integer('swarm_replicas')->default(1);
$table->text('swarm_placement_constraints')->nullable();
});
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('is_swarm_only_worker_nodes')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('swarm_replicas');
$table->dropColumn('swarm_placement_constraints');
});
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_swarm_only_worker_nodes');
});
}
};

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

View File

@@ -56,7 +56,7 @@ services:
ports:
- "${FORWARD_SOKETI_PORT:-6001}:6001"
environment:
SOKETI_DEBUG: "true"
SOKETI_DEBUG: "false"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
@@ -70,6 +70,8 @@ services:
volumes:
- .:/var/www/html:cached
command: sh -c "npm install && npm run dev"
networks:
- coolify
testing-host:
<<: *testing-host-base
container_name: coolify-testing-host
@@ -77,15 +79,8 @@ services:
- /:/host
- /var/run/docker.sock:/var/run/docker.sock
- /data/coolify/:/data/coolify
# - coolify-data-dev:/data/coolify
# remote-host:
# <<: *testing-host-base
# container_name: coolify-remote-host
# volumes:
# - /:/host
# - /var/run/docker.sock:/var/run/docker.sock
# - /data/coolify/:/data/coolify
# # - coolify-data-dev:/data/coolify
networks:
- coolify
mailpit:
image: "axllent/mailpit:latest"
container_name: coolify-mail
@@ -115,3 +110,9 @@ volumes:
coolify-pg-data-dev:
coolify-redis-data-dev:
coolify-minio-data-dev:
networks:
coolify:
name: coolify
external: false

View File

@@ -2,7 +2,7 @@ FROM serversideup/php:8.2-fpm-nginx
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2023.8.2
ARG CLOUDFLARED_VERSION=2023.10.0
ARG POSTGRES_VERSION=15
RUN apt-get update

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1,5 @@
#!/command/execlineb -P
foreground { composer -d /var/www/html/ install }
foreground { php /var/www/html/artisan migrate --step }
foreground { php /var/www/html/artisan dev:init }

View File

@@ -15,7 +15,7 @@ FROM serversideup/php:8.2-fpm-nginx
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2023.8.2
ARG CLOUDFLARED_VERSION=2023.10.0
ARG POSTGRES_VERSION=15
WORKDIR /var/www/html

View File

@@ -1,3 +1,3 @@
#!/command/execlineb -P
s6-setuidgid webuser
php /var/www/html/artisan app:init
php /var/www/html/artisan app:init --cleanup

View File

@@ -5,25 +5,32 @@
html {
@apply text-neutral-400;
}
body {
@apply text-sm antialiased scrollbar;
}
button[isError] {
@apply bg-red-600 hover:bg-red-700;
}
.scrollbar {
@apply scrollbar-thumb-coollabs-100 scrollbar-track-coolgray-200 scrollbar-w-2;
}
.main {
@apply pt-4 pl-24 pr-10 mx-auto;
}
.custom-modal {
@apply flex flex-col gap-2 px-8 py-4 border bg-base-100 border-coolgray-200;
}
.label-text,
label {
@apply text-neutral-400;
}
.navbar-main {
@apply flex items-end gap-6 py-2 border-b-2 border-solid border-coolgray-200;
}
@@ -31,89 +38,117 @@ label {
.loading {
@apply w-4 text-warning;
}
h1 {
@apply text-3xl font-bold text-white;
}
h2 {
@apply text-2xl font-bold text-white;
}
h3 {
@apply text-xl font-bold text-white;
}
h4 {
@apply text-base font-bold text-white;
}
a {
@apply text-neutral-400 hover:text-white link link-hover hover:bg-transparent;
}
.kbd-custom {
@apply px-2 text-xs border border-dashed rounded border-neutral-700 text-warning;
}
.icon {
@apply w-6 h-6;
}
.icon:hover {
@apply text-white;
}
.box {
@apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
}
.box-without-bg {
@apply flex p-2 transition-colors h-16 min-h-full hover:text-white hover:no-underline min-h-[4rem];
@apply flex p-2 transition-colors min-h-full hover:text-white hover:no-underline min-h-[4rem];
}
.description {
@apply pt-2 text-xs font-bold text-neutral-500 group-hover:text-white;
}
.lds-heart {
animation: lds-heart 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
}
@keyframes lds-heart {
0% {
transform: scale(1);
}
5% {
transform: scale(1.2);
}
39% {
transform: scale(0.85);
}
45% {
transform: scale(1);
}
60% {
transform: scale(0.95);
}
100% {
transform: scale(0.9);
}
}
.bg-coollabs-gradient {
@apply text-transparent text-white bg-gradient-to-r from-purple-500 via-pink-500 to-red-500;
}
.text-helper {
@apply inline-block font-bold text-warning;
}
table {
@apply min-w-full divide-y divide-coolgray-200;
}
thead {
@apply uppercase;
}
tbody {
@apply divide-y divide-coolgray-200;
}
tr {
@apply text-neutral-400;
}
tr th {
@apply px-3 py-3.5 text-left text-white;
}
tr th:first-child {
@apply py-3.5 pl-4 pr-3 sm:pl-6;
}
tr td {
@apply px-3 py-4 whitespace-nowrap;
}
tr td:first-child {
@apply pl-4 pr-3 font-bold sm:pl-6;
}
@@ -121,12 +156,15 @@ tr td:first-child {
.buyme {
@apply block px-3 py-2 mt-10 text-sm font-semibold leading-6 text-center text-white rounded-md shadow-sm bg-coolgray-200 hover:bg-coolgray-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-coolgray-200 hover:no-underline;
}
.subtitle {
@apply pt-2 pb-10;
}
.fullscreen {
@apply fixed top-0 left-0 w-full h-full z-[9999] bg-coolgray-100 overflow-y-auto scrollbar pb-4 ;
@apply fixed top-0 left-0 w-full h-full z-[9999] bg-coolgray-100 overflow-y-auto scrollbar pb-4;
}
input.input-sm {
@apply pr-10;
}

View File

@@ -24,20 +24,20 @@
<form action="/login" method="POST" class="flex flex-col gap-2">
@csrf
@env('local')
<x-forms.input value="test@example.com" type="email" name="email" required
label="{{ __('input.email') }}" autofocus />
<x-forms.input value="test@example.com" type="email" name="email" required
label="{{ __('input.email') }}" autofocus />
<x-forms.input value="password" type="password" name="password" required
label="{{ __('input.password') }}" />
<a href="/forgot-password" class="text-xs">
{{ __('auth.forgot_password') }}?
</a>
@else
<x-forms.input type="email" name="email" required label="{{ __('input.email') }}" autofocus />
<x-forms.input type="password" name="password" required label="{{ __('input.password') }}" />
<a href="/forgot-password" class="text-xs">
{{ __('auth.forgot_password') }}?
</a>
<x-forms.input value="password" type="password" name="password" required
label="{{ __('input.password') }}" />
<a href="/forgot-password" class="text-xs">
{{ __('auth.forgot_password') }}?
</a>
@else
<x-forms.input type="email" name="email" required label="{{ __('input.email') }}" autofocus />
<x-forms.input type="password" name="password" required label="{{ __('input.password') }}" />
<a href="/forgot-password" class="text-xs">
{{ __('auth.forgot_password') }}?
</a>
@endenv
<x-forms.button type="submit">{{ __('auth.login') }}</x-forms.button>
@if (!$is_registration_enabled)

View File

@@ -1,13 +1,13 @@
<x-layout-simple>
<div class="min-h-screen hero">
<div>
<div class="flex flex-col items-center pb-8">
<div class="flex flex-col items-center ">
<a href="{{ route('dashboard') }}">
<div class="text-5xl font-bold tracking-tight text-center text-white">Coolify</div>
</a>
</div>
<div class="flex items-center justify-center pb-4 text-center">
<h2>{{ __('auth.reset_password') }}</h2>
{{ __('auth.reset_password') }}
</div>
<div>
<form action="/reset-password" method="POST" class="flex flex-col gap-2">

View File

@@ -23,20 +23,22 @@
@if (data_get($application, 'build_pack') === 'dockercompose')
@foreach (collect(json_decode($this->application->docker_compose_domains)) as $fqdn)
@if (data_get($fqdn, 'domain'))
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getFqdnWithoutPort(data_get($fqdn, 'domain')) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ getFqdnWithoutPort(data_get($fqdn, 'domain')) }}
</a>
</li>
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getFqdnWithoutPort($domain) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ getFqdnWithoutPort($domain) }}
</a>
</li>
@endforeach
@endif
@endforeach
@endif

View File

@@ -3,10 +3,12 @@
href="{{ route('project.application.configuration', $parameters) }}">
<button>Configuration</button>
</a>
@if(!$application->destination->server->isSwarm())
<a wire:navigate class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}">
<button>Execute Command</button>
</a>
@endif
<a wire:navigate class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button>
@@ -19,24 +21,26 @@
<div class="flex-1"></div>
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@elseif ($application->destination->server->isSwarm() && str($application->docker_registry_image_name)->isEmpty())
Swarm Deployments requires a Docker Image in a Registry.
@else
<x-applications.advanced :application="$application" />
@if (!$application->destination->server->isSwarm())
<x-applications.advanced :application="$application" />
@endif
@if ($application->status !== 'exited')
<button title="With rolling update if possible" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@if (!$application->destination->server->isSwarm())
<button title="With rolling update if possible" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@endif
@if ($application->build_pack !== 'dockercompose')
<button title="Restart without rebuilding" wire:click='restart'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
@@ -47,9 +51,13 @@
<path d="M20 4v5h-5" />
</g>
</svg>
@if ($application->destination->server->isSwarm())
Update Service
@else
Restart
@endif
</button>
@if (isDev())
{{-- @if (isDev())
<button title="Restart without rebuilding" wire:click='restartNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -61,7 +69,7 @@
</svg>
Restart (new)
</button>
@endif
@endif --}}
@endif
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
@@ -83,7 +91,7 @@
</svg>
Deploy
</button>
@if (isDev())
{{-- @if (isDev())
<button wire:click='deployNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
@@ -94,7 +102,7 @@
</svg>
Deploy (new)
</button>
@endif
@endif --}}
@endif
@endif
</div>

View File

@@ -2,7 +2,7 @@
<label class="flex gap-4 px-0 cursor-pointer label">
<span class="flex gap-2 label-text min-w-fit">
@if ($label)
{{ $label }}
{!! $label !!}
@else
{{ $id }}
@endif

View File

@@ -22,18 +22,20 @@
</svg>
</div>
@endif
<input {{ $attributes->merge(['class' => $defaultClass . ' pl-10']) }} @required($required)
wire:model={{ $id }} wire:dirty.class.remove='text-white' wire:dirty.class="input-warning"
wire:loading.attr="disabled" type="{{ $type }}" @readonly($readonly) @disabled($disabled)
id="{{ $id }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
<input value="{{ $value }}" {{ $attributes->merge(['class' => $defaultClass . ' pl-10']) }}
@required($required) @if ($id !== 'null') wire:model={{ $id }} @endif
wire:dirty.class.remove='text-white' wire:dirty.class="input-warning" wire:loading.attr="disabled"
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}"
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
aria-placeholder="{{ $attributes->get('placeholder') }}">
</div>
@else
<input {{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
wire:model={{ $id }} wire:dirty.class.remove='text-white' wire:dirty.class="input-warning"
<input @if ($value) value="{{ $value }}" @endif
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
@if ($id !== 'null') wire:model={{ $id }} @endif wire:dirty.class.remove='text-white' wire:dirty.class="input-warning"
wire:loading.attr="disabled" type="{{ $type }}" @disabled($disabled)
id="{{ $id }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}">
@if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}">
@endif
@if (!$label && $helper)
<x-helper :helper="$helper" />

View File

@@ -2,7 +2,7 @@
<livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2">
<h1>Server</h1>
@if ($server->proxyType() !== 'NONE')
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker())
<livewire:server.proxy.status :server="$server" />
@endif
</div>
@@ -20,25 +20,30 @@
]) }}">
<button>Private Key</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
href="{{ route('server.proxy', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Proxy</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}"
href="{{ route('server.destinations', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Destinations</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Log Drains</button>
</a>
@if (!$server->isSwarmWorker())
<a wire:navigate class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
href="{{ route('server.proxy', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Proxy</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}"
href="{{ route('server.destinations', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Destinations</button>
</a>
<a wire:navigate class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Log Drains</button>
</a>
@endif
<div class="flex-1"></div>
<livewire:server.proxy.deploy :server="$server" />
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker())
<livewire:server.proxy.deploy :server="$server" />
@endif
</nav>
</div>

View File

@@ -1,4 +1,4 @@
<div class="navbar-main">
<div class="navbar-main" x-data>
<a wire:navigate class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button>
@@ -27,6 +27,16 @@
</button>
@endif
@if (serviceStatus($service) === 'running')
<button wire:click='restart' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Pull Latest Images & Restart
</button>
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
@@ -58,3 +68,11 @@
</button>
@endif
</div>
@script
<script>
$wire.on('image-pulled', () => {
startService.showModal();
});
</script>
@endscript

View File

@@ -1,6 +1,6 @@
@if (Str::of($status)->startsWith('running'))
<x-status.running :status="$status" />
@elseif(Str::of($status)->startsWith('restarting'))
@elseif(Str::of($status)->startsWith('restarting') || Str::of($status)->startsWith('starting'))
<x-status.restarting :status="$status" />
@else
<x-status.stopped :status="$status" />

View File

@@ -1,3 +1,3 @@
<x-layout>
<livewire:destination.new.standalone-docker :servers="$servers" :server_id="$server_id" />
<livewire:destination.new.docker :servers="$servers" :server_id="$server_id" />
</x-layout>

View File

@@ -1,6 +1,7 @@
@extends('layouts.base')
@section('body')
@parent
<x-navbar-subscription />
<main>
{{ $slot }}
</main>

View File

@@ -69,7 +69,7 @@
@if ($servers->count() === 1)
<div class="grid grid-cols-1 gap-2">
@else
<div class="grid grid-cols-3 gap-2">
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
@endif
@foreach ($servers as $server)
<a wire:navigate href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"

View File

@@ -12,6 +12,7 @@
<option value="{{ $server->id }}">{{ $server->name }}</option>
@endforeach
</x-forms.select>
{{-- <x-forms.checkbox type="checkbox" id="is_swarm" label="Is it a Swarm network?" /> --}}
<x-forms.button type="submit">
Save Destination
</x-forms.button>

View File

@@ -11,7 +11,16 @@
<div class="flex gap-2 ">
Available for using:
@forelse ($server->standaloneDockers as $docker)
<a wire:navigate href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<a wire:navigate
href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a>
@empty
<div class="">N/A</div>
@endforelse
@forelse ($server->swarmDockers as $docker)
<a wire:navigate
href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a>
@empty

View File

@@ -1,12 +1,12 @@
<div class="min-h-screen hero">
<div class="w-96 min-w-fit">
<div class="flex flex-col items-center pb-8">
<div class="flex flex-col items-center">
<a href="{{ route('dashboard') }}">
<div class="text-5xl font-bold tracking-tight text-center text-white">Coolify</div>
</a>
</div>
<div class="flex items-center justify-center pb-4 text-center">
<h2>Set your initial password</h2>
Set your initial password
</div>
<form class="flex flex-col gap-2" wire:submit='submit'>
<x-forms.input id="email" type="email" placeholder="Email" readonly label="Email" />

View File

@@ -5,6 +5,11 @@
<div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'general' && 'text-white'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
@if ($application->destination->server->isSwarm())
<a :class="activeTab === 'swarm' && 'text-white'"
@click.prevent="activeTab = 'swarm'; window.location.hash = 'swarm'" href="#">Swarm
Configuration</a>
@endif
<a :class="activeTab === 'advanced' && 'text-white'"
@click.prevent="activeTab = 'advanced'; window.location.hash = 'advanced'" href="#">Advanced</a>
@if ($application->build_pack !== 'static')
@@ -13,6 +18,7 @@
href="#">Environment
Variables</a>
@endif
@if ($application->git_based())
<a :class="activeTab === 'source' && 'text-white'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@@ -56,6 +62,9 @@
<div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.application.general :application="$application" />
</div>
<div x-cloak x-show="activeTab === 'swarm'" class="h-full">
<livewire:project.application.swarm :application="$application" />
</div>
<div x-cloak x-show="activeTab === 'advanced'" class="h-full">
<livewire:project.application.advanced :application="$application" />
</div>

View File

@@ -70,8 +70,11 @@
@if ($application->build_pack !== 'dockercompose')
<h3>Docker Registry</h3>
@if ($application->destination->server->isSwarm())
<div>Docker Swarm requires the image to be available in a registry. More info <a class="underline"
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
@if ($application->build_pack !== 'dockerimage')
<div>Docker Swarm requires the image to be available in a registry. More info <a
class="underline" href="https://coolify.io/docs/docker-registries"
target="_blank">here</a>.</div>
@endif
@else
@if ($application->build_pack !== 'dockerimage')
<div>Push the built image to a docker registry. More info <a class="underline"
@@ -126,37 +129,58 @@
</div>
@endif
@endif
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
label="Dockerfile Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>" />
@endif
@if ($application->build_pack === 'dockercompose')
<span wire:init='loadComposeFile(true)'></span>
<x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_location"
label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
{{-- <x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_pr_location"
label="Docker Compose Location For Pull Requests"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_pr_location, '/') }}</span>" /> --}}
@endif
@if ($application->build_pack === 'dockerfile')
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target"
helper="Useful if you have multi-staged dockerfile." />
@endif
@if ($application->could_set_build_commands())
@if ($application->settings->is_static)
<x-forms.input placeholder="/dist" id="application.publish_directory"
label="Publish Directory" required />
@else
<x-forms.input placeholder="/" id="application.publish_directory"
label="Publish Directory" />
@if ($application->build_pack === 'dockercompose')
<div class="flex flex-col gap-2" wire:init='loadComposeFile(true)'>
<div class="flex gap-2">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
<x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_location"
label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
</div>
<div class="pt-4">The following commands are for advanced use cases. Only modify them if you
know what are
you doing.</div>
<div class="flex gap-2">
<x-forms.input placeholder="docker compose build"
id="application.docker_compose_custom_build_command"
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} build</span>"
label="Custom Build Command" />
<x-forms.input placeholder="docker compose up -d"
id="application.docker_compose_custom_start_command"
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} up -d</span>"
label="Custom Start Command" />
{{-- <x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_pr_location"
label="Docker Compose Location For Pull Requests"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_pr_location, '/') }}</span>" /> --}}
</div>
</div>
@else
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
label="Dockerfile Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>" />
@endif
@endif
</div>
@if ($application->build_pack === 'dockerfile')
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target"
helper="Useful if you have multi-staged dockerfile." />
@endif
@if ($application->could_set_build_commands())
@if ($application->settings->is_static)
<x-forms.input placeholder="/dist" id="application.publish_directory"
label="Publish Directory" required />
@else
<x-forms.input placeholder="/" id="application.publish_directory"
label="Publish Directory" />
@endif
@endif
</div>
@endif
@endif
@if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
@@ -179,8 +203,10 @@
required
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." />
@endif
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
@if (!$application->destination->server->isSwarm())
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
@endif
</div>
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>

View File

@@ -0,0 +1,24 @@
<div>
<form wire:submit='submit' class="flex flex-col">
<div class="flex items-center gap-2">
<h2>Swarm Configuration</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
{{-- <div>Advanced Swarm Configuration</div> --}}
<div class="flex flex-col gap-2 py-4">
<div class="flex flex-col items-end gap-2 xl:flex-row">
<x-forms.input id="application.swarm_replicas" label="Replicas" required />
<x-forms.checkbox instantSave helper="If turned off, this resource will start on manager nodes too."
id="application.settings.is_swarm_only_worker_nodes" label="Only Start on Worker nodes" />
</div>
<x-forms.textarea id="swarm_placement_constraints" rows="7"
label="Custom Placement Constraints"
placeholder="placement:
constraints:
- 'node.role == worker'" />
</div>
</form>
</div>

View File

@@ -1,36 +1,36 @@
<form wire:submit='clone'>
<div class="flex flex-col">
<div class="flex gap-2">
<h1>Clone</h1>
<x-forms.button type="submit">Clone to a New Project</x-forms.button>
</div>
<div class="subtitle ">Quickly clone a project</div>
<h1>Clone</h1>
<div class="subtitle ">Quickly clone all resources to a new project</div>
</div>
<div class="flex items-end gap-2">
<x-forms.input required id="newProjectName" label="New Project Name" />
<x-forms.button type="submit">Clone</x-forms.button>
</div>
<x-forms.input required id="newProjectName" label="New Project Name" />
<h3 class="pt-4 pb-2">Servers</h3>
<div class="grid gap-2 lg:grid-cols-3">
@foreach ($servers as $srv)
<div wire:click="selectServer('{{ $srv->id }}')"
class="cursor-pointer box-without-bg bg-coolgray-200 group"
:class="'{{ $selectedServer === $srv->id }}' && 'bg-coollabs'">
<div class="flex flex-col mx-6">
<div :class="'{{ $selectedServer === $srv->id }}' && 'text-white'"> {{ $srv->name }}</div>
@isset($selectedServer)
<div :class="'{{ $selectedServer === $srv->id }}' && 'text-white pt-2 text-xs font-bold'">
{{ $srv->description }}</div>
@else
<div class="description">
{{ $srv->description }}</div>
@endisset
<div class="flex flex-col gap-4">
@foreach ($servers->sortBy('id') as $server)
<div class="p-4 border border-coolgray-500">
<h3>{{ $server->name }}</h3>
<h5>{{ $server->description }}</h5>
<div class="pt-4 pb-2">Docker Networks</div>
<div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4">
@foreach ($server->destinations() as $destination)
<div class="cursor-pointer box-without-bg bg-coolgray-200 group"
:class="'{{ $selectedDestination === $destination->id }}' && 'bg-coollabs text-white'"
wire:click="selectServer('{{ $server->id }}', '{{ $destination->id }}')">
{{ $destination->name }}
</div>
@endforeach
</div>
</div>
@endforeach
</div>
<h3 class="pt-4 pb-2">Resources</h3>
<div class="grid grid-cols-1 gap-2">
<div class="grid grid-cols-1 gap-2 p-4 border border-coolgray-500">
@foreach ($environment->applications->sortBy('name') as $application)
<div class="p-2 border border-coolgray-200">
<div>
<div class="flex flex-col">
<div class="font-bold text-white">{{ $application->name }}</div>
<div class="description">{{ $application->description }}</div>
@@ -38,7 +38,7 @@
</div>
@endforeach
@foreach ($environment->databases()->sortBy('name') as $database)
<div class="p-2 border border-coolgray-200">
<div>
<div class="flex flex-col">
<div class="font-bold text-white">{{ $database->name }}</div>
<div class="description">{{ $database->description }}</div>
@@ -46,7 +46,7 @@
</div>
@endforeach
@foreach ($environment->services->sortBy('name') as $service)
<div class="p-2 border border-coolgray-200">
<div>
<div class="flex flex-col">
<div class="font-bold text-white">{{ $service->name }}</div>
<div class="description">{{ $service->description }}</div>

View File

@@ -202,6 +202,14 @@
<li class="step step-secondary">Select a Server</li>
<li class="step">Select a Destination</li>
</ul>
{{-- @if ($isDatabase)
<div class="flex items-center justify-center pt-4">
<x-forms.checkbox instantSave wire:model="includeSwarm"
helper="Swarm clusters are excluded from this list by default. For database, services or complex compose deployments with databases to work with Swarm,
you need to set a few things on the server. Read more <a class='text-white underline' href='https://coolify.io/docs/swarm#database-requirements' target='_blank'>here</a>."
label="Include Swarm Clusters" />
</div>
@endif --}}
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap">
@forelse($servers as $server)
<div class="box group" wire:click="setServer({{ $server }})">
@@ -230,36 +238,39 @@
<li class="step step-secondary">Select a Server</li>
<li class="step step-secondary">Select a Destination</li>
</ul>
<a wire:navigate href="{{ route('destination.new', ['server_id' => $server_id]) }}"
class="items-center justify-center pb-10 text-center box-without-bg group bg-coollabs hover:bg-coollabs-100">
<div class="flex flex-col mx-6 ">
<div class="font-bold text-white">
+ Add New
</div>
</div>
</a>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap">
@foreach ($standaloneDockers as $standaloneDocker)
<div class="box group" wire:click="setDestination('{{ $standaloneDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Standalone Docker <span class="text-xs">({{ $standaloneDocker->name }})</span>
</div>
<div class="text-xs group-hover:text-white">
network: {{ $standaloneDocker->network }}</div>
</div>
</div>
@endforeach
@foreach ($swarmDockers as $swarmDocker)
<div class="box group" wire:click="setDestination('{{ $swarmDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Swarm Docker <span class="text-xs">({{ $swarmDocker->name }})</span>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap">
@if ($server->isSwarm())
@foreach ($swarmDockers as $swarmDocker)
<div class="box group" wire:click="setDestination('{{ $swarmDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Swarm Docker <span class="text-xs">({{ $swarmDocker->name }})</span>
</div>
</div>
</div>
@endforeach
@else
@foreach ($standaloneDockers as $standaloneDocker)
<div class="box group" wire:click="setDestination('{{ $standaloneDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Standalone Docker <span class="text-xs">({{ $standaloneDocker->name }})</span>
</div>
<div class="text-xs group-hover:text-white">
network: {{ $standaloneDocker->network }}</div>
</div>
</div>
@endforeach
@endif
<a wire:navigate href="{{ route('destination.new', ['server_id' => $server_id]) }}"
class="items-center justify-center pb-10 text-center box-without-bg group bg-coollabs hover:bg-coollabs-100">
<div class="flex flex-col mx-6 ">
<div class="font-bold text-white">
+ Add New
</div>
</div>
@endforeach
</a>
</div>
@endif
@if ($current_step === 'existing-postgresql')

View File

@@ -44,26 +44,44 @@
$application->status)->contains(['running']),
'border-l border-dashed border-warning' => Str::of(
$application->status)->contains(['starting']),
'flex gap-2 box group',
'flex gap-2 box-without-bg bg-coolgray-100 hover:text-neutral-300 group',
])>
<a wire:navigate class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $application->name]) }}">
@if ($application->human_name)
{{ Str::headline($application->human_name) }}
@else
{{ Str::headline($application->name) }}
@endif
@if ($application->configuration_required)
<span class="text-xs text-error">(configuration required)</span>
@endif
@if ($application->description)
<span class="text-xs">{{ Str::limit($application->description, 60) }}</span>
@endif
@if ($application->fqdn)
<span class="text-xs">{{ Str::limit($application->fqdn, 60) }}</span>
@endif
<div class="text-xs">{{ $application->status }}</div>
</a>
<div class="flex flex-row w-full">
<div class="flex flex-col flex-1">
<div class="pb-2">
@if ($application->human_name)
{{ Str::headline($application->human_name) }}
@else
{{ Str::headline($application->name) }}
@endif
<span class="text-xs">({{ $application->image }})</span>
</div>
@if ($application->configuration_required)
<span class="text-xs text-error">(configuration required)</span>
@endif
@if ($application->description)
<span class="text-xs">{{ Str::limit($application->description, 60) }}</span>
@endif
@if ($application->fqdn)
<span class="text-xs">{{ Str::limit($application->fqdn, 60) }}</span>
@endif
<div class="text-xs">{{ $application->status }}</div>
</div>
<div class="flex items-center px-4">
<a wire:navigate
class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $application->name]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</svg>
</a>
</div>
</div>
</div>
@endforeach
@foreach ($databases as $database)
@@ -74,23 +92,43 @@
$database->status)->contains(['running']),
'border-l border-dashed border-warning' => Str::of(
$database->status)->contains(['restarting']),
'flex gap-2 box group',
'flex gap-2 box-without-bg bg-coolgray-100 hover:text-neutral-300 group',
])>
<a wire:navigate class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}">
@if ($database->human_name)
{{ Str::headline($database->human_name) }}
@else
{{ Str::headline($database->name) }}
@endif
@if ($database->configuration_required)
<span class="text-xs text-error">(configuration required)</span>
@endif
@if ($database->description)
<span class="text-xs">{{ Str::limit($database->description, 60) }}</span>
@endif
<div class="text-xs">{{ $database->status }}</div>
</a>
<div class="flex flex-row w-full">
<div class="flex flex-col flex-1">
<div class="pb-2">
@if ($database->human_name)
{{ Str::headline($database->human_name) }}
@else
{{ Str::headline($database->name) }}
@endif
<span class="text-xs">({{ $database->image }})</span>
</div>
@if ($database->configuration_required)
<span class="text-xs text-error">(configuration required)</span>
@endif
@if ($database->description)
<span class="text-xs">{{ Str::limit($database->description, 60) }}</span>
@endif
<div class="text-xs">{{ $database->status }}</div>
</div>
<div class="flex items-center px-4">
<a wire:navigate
class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</svg>
</a>
</div>
</div>
</div>
@endforeach
</div>

View File

@@ -1,14 +1,28 @@
<dialog id="newStorage" class="modal">
<form method="dialog" class="flex flex-col gap-2 rounded modal-box" wire:submit='submit'>
<h3 class="text-lg font-bold">Add Storage Volume</h3>
<x-forms.input placeholder="pv-name" id="name" label="Name" required />
<x-forms.input placeholder="/root" id="host_path" label="Source Path" />
<x-forms.input placeholder="/tmp/root" id="mount_path" label="Destination Path" required />
<x-forms.button onclick="newStorage.close()" type="submit">
@if ($isSwarm)
<h5>Swarm Mode detected: You need to set a shared volume (EFS/NFS/etc) on all the worker nodes if you would like to use a persistent volumes.</h5>
@endif
<x-forms.input placeholder="pv-name" id="name" label="Name" required helper="Volume name." />
@if ($isSwarm)
<x-forms.input placeholder="/root" id="host_path" label="Source Path" required helper="Directory on the host system." />
@else
<x-forms.input placeholder="/root" id="host_path" label="Source Path" helper="Directory on the host system." />
@endif
<x-forms.input placeholder="/tmp/root" id="mount_path" label="Destination Path" required helper="Directory inside the container." />
<x-forms.button type="submit">
Save
</x-forms.button>
</form>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
<script>
document.addEventListener('livewire:initialized', () => {
Livewire.on('closeStorageModal', () => {
document.getElementById('newStorage').close()
})
})
</script>
</dialog>

View File

@@ -35,8 +35,10 @@
<div class="flex flex-col w-full gap-2 lg:flex-row">
<x-forms.input id="server.name" label="Name" required />
<x-forms.input id="server.description" label="Description" />
<x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain"
helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example:</span><br>In case you set:<span class='text-helper'>https://example.com</span> your applications will get:<br> <span class='text-helper'>https://randomId.example.com</span>" />
@if (!$server->settings->is_swarm_worker)
<x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain"
helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example:</span><br>In case you set:<span class='text-helper'>https://example.com</span> your applications will get:<br> <span class='text-helper'>https://randomId.example.com</span>" />
@endif
</div>
<div class="flex flex-col w-full gap-2 lg:flex-row">
@@ -52,11 +54,28 @@
<x-forms.checkbox instantSave
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>"
id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" />
@if ($server->isSwarm())
<div class="pt-6"> Swarm support is in alpha version. </div>
@endif
@if ($server->settings->is_swarm_worker)
<x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_manager"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>."
label="Is it a Swarm Manager?" />
@else
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_manager"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>."
label="Is it a Swarm Manager?" />
@endif
@if ($server->settings->is_swarm_manager)
<x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_worker"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>."
label="Is it a Swarm Worker?" />
@else
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_worker"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>."
label="Is it a Swarm Worker?" />
@endif
@endif
{{-- <x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_manager"
label="Is it a Swarm Manager?" /> --}}
{{-- <x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_worker"
label="Is it a Swarm Worker?" /> --}}
</div>
</div>

View File

@@ -25,14 +25,43 @@
@endif
@endforeach
</x-forms.select>
{{-- <div class="w-64">
<x-forms.checkbox type="checkbox" id="is_swarm_manager"
label="Is it a Swarm Manager?" />
</div> --}}
<div class="w-96">
<div class="pt-6"> Swarm support is in alpha version. Read the docs <a class='text-white' href='https://coolify.io/docs/swarm#deploy-with-persistent-storage' target='_blank'>here</a>.</div>
@if ($is_swarm_worker)
<x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_manager"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>."
label="Is it a Swarm Manager?" />
@else
<x-forms.checkbox type="checkbox" instantSave id="is_swarm_manager"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>."
label="Is it a Swarm Manager?" />
@endif
@if ($is_swarm_manager)
<x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_worker"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>."
label="Is it a Swarm Worker?" />
@else
<x-forms.checkbox type="checkbox" instantSave id="is_swarm_worker"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>."
label="Is it a Swarm Worker?" />
@endif
@if ($is_swarm_worker && count($swarm_managers) > 0)
<div class="py-4">
<x-forms.select label="Select a Swarm Cluster" id="selected_swarm_cluster" required>
@foreach ($swarm_managers as $server)
@if ($loop->first)
<option selected value="{{ $server->id }}">{{ $server->name }}</option>
@else
<option value="{{ $server->id }}">{{ $server->name }}</option>
@endif
@endforeach
</x-forms.select>
</div>
@endif
</div>
<x-forms.button type="submit">
Save New Server
Save Server
</x-forms.button>
</form>
@endif
</div>

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