Compare commits

...

67 Commits

Author SHA1 Message Date
Andras Bacsai
1c90f46f2a Merge pull request #1572 from coollabsio/next
v4.0.0-beta.173
2023-12-27 17:44:07 +01:00
Andras Bacsai
e78d851c85 fix: button title 2023-12-27 17:14:18 +01:00
Andras Bacsai
52d05005ed fix: deploy instead of restart in case swarm is used 2023-12-27 17:12:09 +01:00
Andras Bacsai
f03aa57758 fix: routing, switch back to old one 2023-12-27 16:45:01 +01:00
Andras Bacsai
8c20c833ba fix: add source commit to final envs 2023-12-27 13:06:59 +01:00
Andras Bacsai
2fe6766b7f fix: cpu limit to float from int 2023-12-27 13:01:57 +01:00
Andras Bacsai
3f453ba7c0 Merge branch 'main' into next 2023-12-21 14:09:13 +01:00
Andras Bacsai
bd02c3055a update readme 2023-12-21 14:08:39 +01:00
andrasbacsai
7ea7d85d15 Deploying to main from @ coollabsio/coolify@d38350c282 🚀 2023-12-21 12:58:10 +00:00
Andras Bacsai
d38350c282 update 2023-12-21 13:57:52 +01:00
andrasbacsai
a83c70004c Deploying to main from @ coollabsio/coolify@49e1404a2c 🚀 2023-12-21 12:56:00 +00:00
Andras Bacsai
49e1404a2c test gh auto sponsor update 2023-12-21 13:55:40 +01:00
Andras Bacsai
76f23e7dbf do not check server ready on server status job 2023-12-21 10:43:39 +01:00
Andras Bacsai
ad8653f54d Merge pull request #1564 from coollabsio/next
v4.0.0-beta.172
2023-12-21 10:35:40 +01:00
Andras Bacsai
8939d77051 fix 2023-12-21 10:28:02 +01:00
Andras Bacsai
37be4a1796 fix 2023-12-21 10:00:41 +01:00
Andras Bacsai
e4c923e358 fix 2023-12-21 09:57:39 +01:00
Andras Bacsai
62ca3ffaa5 fix 2023-12-21 09:55:16 +01:00
Andras Bacsai
9af3ce4be5 fail job instead of runtime exception 2023-12-21 09:49:18 +01:00
Andras Bacsai
fe143ef8a5 Merge pull request #1563 from coollabsio/next
v4.0.0-beta.171
2023-12-21 09:35:43 +01:00
Andras Bacsai
5fb5845e90 redirect false on some urls 2023-12-21 09:33:11 +01:00
Andras Bacsai
794cfbd8eb execute handle on containerstatusjob 2023-12-21 09:28:47 +01:00
Andras Bacsai
29ee9915f3 fix: check proxy after mount on server view
fix: change realtime console log
version++
2023-12-21 09:28:39 +01:00
Andras Bacsai
331d485213 Merge pull request #1562 from coollabsio/next
v4.0.0-beta.170
2023-12-21 08:49:22 +01:00
Andras Bacsai
665e3761c4 fix: stay tuned 2023-12-21 08:48:53 +01:00
Andras Bacsai
ac19f0e34f enable docker image swarms 2023-12-21 08:46:48 +01:00
Andras Bacsai
d7cfa0578f Comment out handle() method call in
ContainerStatusJob constructor
2023-12-20 20:01:45 +01:00
Andras Bacsai
694169bb84 fix: why?! 2023-12-20 18:09:01 +01:00
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
117 changed files with 1895 additions and 712 deletions

View File

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

View File

@@ -17,6 +17,39 @@ https://coolify.io/sponsorships
Thank you so much! Thank you so much!
Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)!
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="appwrite logo" width="200"/></a>
<a href="https://appwrite.io" target="_blank"><img src="./other/logos/appwrite.svg" alt="appwrite logo" width="200"/></a>
## Github Sponsors ($15+)
<a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/Niki2k1"><img src="https://github.com/Niki2k1.png" width="60px" alt="Niklas Lausch" /></a>
<a href="https://github.com/pixelinfinito"><img src="https://github.com/pixelinfinito.png" width="60px" alt="Pixel Infinito" /></a>
<a href="https://github.com/whitesidest"><img src="https://avatars.githubusercontent.com/u/12365916?s=52&v=4" width="60px" alt="Tyler Whitesides" /></a>
<a href="https://github.com/aniftyco"><img src="https://github.com/aniftyco.png" width="60px" alt="NiftyCo" /></a>
<a href="https://github.com/iujlaki"><img src="https://github.com/iujlaki.png" width="60px" alt="Imre Ujlaki" /></a>
<a href="https://github.com/Illyism"><img src="https://github.com/Illyism.png" width="60px" alt="Ilias Ism" /></a>
<a href="https://github.com/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a>
<a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a>
<a href="https://github.com/cccareers"><img src="https://github.com/cccareers.png" width="60px" alt="Creating Coding Careers" /></a>
## Organizations
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/1/website"><img src="https://opencollective.com/coollabsio/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/2/website"><img src="https://opencollective.com/coollabsio/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/3/website"><img src="https://opencollective.com/coollabsio/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/4/website"><img src="https://opencollective.com/coollabsio/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/5/website"><img src="https://opencollective.com/coollabsio/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/6/website"><img src="https://opencollective.com/coollabsio/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/7/website"><img src="https://opencollective.com/coollabsio/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
## Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
# Cloud # Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
@@ -59,34 +92,6 @@ Contact us [here](https://coolify.io/docs/contact).
<a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
# 💰 Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)]
## Organizations
Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)!
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="appwrite logo" width="200"/></a>
<a href="https://appwrite.io" target="_blank"><img src="./other/logos/appwrite.svg" alt="appwrite logo" width="200"/></a>
Support this project with your organization. Your logo will show up here with a link to your website.
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/1/website"><img src="https://opencollective.com/coollabsio/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/2/website"><img src="https://opencollective.com/coollabsio/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/3/website"><img src="https://opencollective.com/coollabsio/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/4/website"><img src="https://opencollective.com/coollabsio/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/5/website"><img src="https://opencollective.com/coollabsio/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/6/website"><img src="https://opencollective.com/coollabsio/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/7/website"><img src="https://opencollective.com/coollabsio/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
## Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
# Star History # Star History
[![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date) [![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date)

View File

@@ -56,7 +56,7 @@ class StartMariadb
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus, 'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]

View File

@@ -63,7 +63,7 @@ class StartMongodb
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus, 'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]

View File

@@ -56,7 +56,7 @@ class StartMysql
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus, 'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]

View File

@@ -66,7 +66,7 @@ class StartPostgresql
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus, 'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]

View File

@@ -65,7 +65,7 @@ class StartRedis
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus, 'cpus' => (float) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]

View File

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

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'); $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; $own = Team::find(0)->servers;
$servers = $servers->merge($own); $servers = $servers->merge($own);
$containerServers = $servers->where('settings.is_swarm_worker', false);
} else { } else {
$servers = Server::all()->where('ip', '!=', '1.2.3.4'); $servers = Server::all()->where('ip', '!=', '1.2.3.4');
$containerServers = $servers->where('settings.is_swarm_worker', false);
} }
foreach ($servers as $server) { foreach ($containerServers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) { if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer(); $schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
} }
} }
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
}
} }
private function instance_auto_update($schedule) private function instance_auto_update($schedule)
{ {

View File

@@ -47,7 +47,7 @@ class Handler extends ExceptionHandler
if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) { if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) {
return response()->json(['message' => $exception->getMessage()], 401); return response()->json(['message' => $exception->getMessage()], 401);
} }
return redirect()->guest($exception->redirectTo() ?? route('login')); return redirect()->guest($exception->redirectTo() ?? route('login'));
} }
/** /**
* Register the exception handling callbacks for the application. * Register the exception handling callbacks for the application.

View File

@@ -2,6 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -16,7 +17,7 @@ class DecideWhatToDoWithUser
} }
if (!auth()->user() || !isCloud() || isInstanceAdmin()) { if (!auth()->user() || !isCloud() || isInstanceAdmin()) {
if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding'); return redirect()->route('boarding');
} }
return $next($request); return $next($request);
} }
@@ -24,27 +25,27 @@ class DecideWhatToDoWithUser
if ($request->path() === 'verify' || in_array($request->path(), allowedPathsForInvalidAccounts()) || $request->routeIs('verify.verify')) { if ($request->path() === 'verify' || in_array($request->path(), allowedPathsForInvalidAccounts()) || $request->routeIs('verify.verify')) {
return $next($request); return $next($request);
} }
return redirect('/verify'); return redirect()-route('verify.email');
} }
if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) { if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) {
if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) { if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) { if (Str::startsWith($request->path(), 'invitations')) {
return $next($request); return $next($request);
} }
return redirect('subscription'); return redirect()->route('subscription');
} }
} }
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) { if (Str::startsWith($request->path(), 'invitations')) {
return $next($request); return $next($request);
} }
return redirect('boarding'); return redirect()->route('boarding');
} }
if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') { if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') {
return redirect('/'); return redirect(RouteServiceProvider::HOME);
} }
if (isSubscriptionActive() && $request->path() === 'subscription') { if (isSubscriptionActive() && $request->path() === 'subscription') {
return redirect('/'); return redirect(RouteServiceProvider::HOME);
} }
return $next($request); return $next($request);
} }

View File

@@ -75,6 +75,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose_base64; private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile'; private string $dockerfile_location = '/Dockerfile';
private string $docker_compose_location = '/docker-compose.yml'; 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 $addHosts = null;
private ?string $buildTarget = null; private ?string $buildTarget = null;
private Collection $saved_outputs; private Collection $saved_outputs;
@@ -215,19 +217,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server)); 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(); $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->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true); $this->application->isConfigurationChanged(true);
@@ -299,6 +290,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"echo -n 'Image pushed to docker registry.'" "echo -n 'Image pushed to docker registry.'"
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
if ($this->application->destination->server->isSwarm()) {
throw $e;
}
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"], ["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')) { if (data_get($this->application, 'docker_compose_location')) {
$this->docker_compose_location = $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) { if ($this->pull_request_id === 0) {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
} else { } else {
@@ -454,7 +454,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]); ]);
$this->save_environment_variables(); $this->save_environment_variables();
// Build new container to limit downtime. // 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); $this->stop_running_container(force: true);
$networkId = $this->application->uuid; $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() private function deploy_dockerfile_buildpack()
{ {
@@ -575,7 +596,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update() private function rolling_update()
{ {
if ($this->server->isSwarm()) { 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 { } else {
if (count($this->application->ports_mappings_array) > 0) { if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command( $this->execute_remote_command(
@@ -674,10 +704,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
$this->stop_running_container(); $this->stop_running_container();
$this->execute_remote_command( if ($this->application->destination->server->isSwarm()) {
["echo -n 'Starting preview deployment.'"], ray("{$this->workdir}{$this->docker_compose_location}");
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], $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() private function create_workdir()
{ {
@@ -914,7 +954,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'memswap_limit' => $this->application->limits_memory_swap, 'memswap_limit' => $this->application->limits_memory_swap,
'mem_swappiness' => $this->application->limits_memory_swappiness, 'mem_swappiness' => $this->application->limits_memory_swappiness,
'mem_reservation' => $this->application->limits_memory_reservation, 'mem_reservation' => $this->application->limits_memory_reservation,
'cpus' => (int) $this->application->limits_cpus, 'cpus' => (float) $this->application->limits_cpus,
'cpuset' => $this->application->limits_cpuset, 'cpuset' => $this->application->limits_cpuset,
'cpu_shares' => $this->application->limits_cpu_shares, 'cpu_shares' => $this->application->limits_cpu_shares,
] ]
@@ -941,13 +981,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares'); data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares');
$docker_compose['services'][$this->container_name]['deploy'] = [ $docker_compose['services'][$this->container_name]['deploy'] = [
'placement' => [
'constraints' => [
'node.role == worker'
]
],
'mode' => 'replicated', 'mode' => 'replicated',
'replicas' => 1, 'replicas' => data_get($this->application, 'swarm_replicas', 1),
'update_config' => [ 'update_config' => [
'order' => 'start-first' 'order' => 'start-first'
], ],
@@ -966,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 { } else {
$docker_compose['services'][$this->container_name]['labels'] = $labels; $docker_compose['services'][$this->container_name]['labels'] = $labels;
} }
@@ -1084,6 +1129,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Add PORT if not exists, use the first port as default // Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) { if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
$environment_variables->push("PORT={$ports[0]}"); $environment_variables->push("PORT={$ports[0]}");
} if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('SOURCE_COMMIT'))->isEmpty()) {
if (!is_null($this->commit)) {
$environment_variables->push("SOURCE_COMMIT={$this->commit}");
}
} }
return $environment_variables->all(); return $environment_variables->all();
} }
@@ -1258,15 +1307,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} build"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} build"), "hidden" => true],
); );
} else { } else {
if ($this->docker_compose_location) { $this->execute_remote_command(
$this->execute_remote_command( [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
[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->application_deployment_queue->addLogEntry("New images built."); $this->application_deployment_queue->addLogEntry("New images built.");
} }

View File

@@ -17,20 +17,24 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Sleep;
class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 4;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping($this->server->id))->dontRelease()]; return [(new WithoutOverlapping($this->server->uuid))];
} }
public function uniqueId(): int public function uniqueId(): int
{ {
return $this->server->id; return $this->server->uuid;
} }
public function __construct(public Server $server) public function __construct(public Server $server)
@@ -40,13 +44,13 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle() public function handle()
{ {
if (!$this->server->isServerReady($this->tries)) {
return 'Server is not reachable.';
};
try { try {
if (!$this->server->isServerReady()) {
return;
};
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false); $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 { } else {
// Precheck for containers // Precheck for containers
$containers = instant_remote_process(["docker container ls -q"], $this->server, false); $containers = instant_remote_process(["docker container ls -q"], $this->server, false);
@@ -54,15 +58,15 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return; return;
} }
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = null; $containerReplicates = null;
} }
if (is_null($containers)) { if (is_null($containers)) {
return; return;
} }
$containers = format_docker_command_output_to_json($containers); $containers = format_docker_command_output_to_json($containers);
if ($containerReplicase) { if ($containerReplicates) {
$containerReplicase = format_docker_command_output_to_json($containerReplicase); $containerReplicates = format_docker_command_output_to_json($containerReplicates);
foreach ($containerReplicase as $containerReplica) { foreach ($containerReplicates as $containerReplica) {
$name = data_get($containerReplica, 'Name'); $name = data_get($containerReplica, 'Name');
$containers = $containers->map(function ($container) use ($name, $containerReplica) { $containers = $containers->map(function ($container) use ($name, $containerReplica) {
if (data_get($container, 'Spec.Name') === $name) { if (data_get($container, 'Spec.Name') === $name) {

View File

@@ -17,22 +17,26 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public ?int $disk_usage = null; public ?int $disk_usage = null;
public $tries = 4;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Server $server) public function __construct(public Server $server)
{ {
} }
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping($this->server->id))->dontRelease()]; return [(new WithoutOverlapping($this->server->uuid))];
} }
public function uniqueId(): int 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}");
try { try {
if ($this->server->isFunctional()) { if ($this->server->isFunctional()) {
$this->cleanup(notify: false); $this->cleanup(notify: false);
@@ -40,7 +44,7 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage()); send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage());
handleError($e); return handleError($e);
} }
} }
public function cleanup(bool $notify = false): void public function cleanup(bool $notify = false): void

View File

@@ -37,7 +37,7 @@ class CheckLicense extends Component
} catch (\Throwable $e) { } catch (\Throwable $e) {
session()->flash('error', 'Something went wrong. Please contact support. <br>Error: ' . $e->getMessage()); session()->flash('error', 'Something went wrong. Please contact support. <br>Error: ' . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage());
return $this->redirect('/settings/license', navigate: true); return redirect()->route('settings.license');
} }
} }
} }

View File

@@ -36,7 +36,7 @@ class Form extends Component
instant_remote_process(['docker network rm -f ' . $this->destination->network], $this->destination->server); instant_remote_process(['docker network rm -f ' . $this->destination->network], $this->destination->server);
} }
$this->destination->delete(); $this->destination->delete();
return $this->redirectRoute('dashboard', navigate: true); return redirect()->route('dashboard');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ class ForcePasswordReset extends Component
if ($firstLogin) { if ($firstLogin) {
send_internal_notification('First login for ' . auth()->user()->email); send_internal_notification('First login for ' . auth()->user()->email);
} }
return $this->redirectRoute('dashboard', navigate: true); return redirect()->route('dashboard');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -35,7 +35,7 @@ class Change extends Component
if ($this->private_key->isEmpty()) { if ($this->private_key->isEmpty()) {
$this->private_key->delete(); $this->private_key->delete();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get(); currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
return $this->redirectRoute('security.private-key.index', navigate: true); return redirect()->route('security.private-key.index');
} }
$this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.'); $this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -67,9 +67,9 @@ class Create extends Component
'team_id' => currentTeam()->id 'team_id' => currentTeam()->id
]); ]);
if ($this->from === 'server') { if ($this->from === 'server') {
return $this->redirectRoute('server.create', navigate: true); return redirect()->route('server.create');
} }
return $this->redirectRoute('security.private-key.show', ['private_key_uuid' => $private_key->uuid], navigate: true); return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -27,7 +27,7 @@ class AddEmpty extends Component
'description' => $this->description, 'description' => $this->description,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ]);
return $this->redirectRoute('project.show', $project->uuid, navigate: true); return redirect()->route('project.show', $project->uuid);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally { } finally {

View File

@@ -27,10 +27,10 @@ class AddEnvironment extends Component
'project_id' => $this->project->id, 'project_id' => $this->project->id,
]); ]);
return $this->redirectRoute('project.resources', [ return redirect()->route('project.resources', [
'project_uuid' => $this->project->uuid, 'project_uuid' => $this->project->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
], navigate: true); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
handleError($e, $this); handleError($e, $this);
} finally { } finally {

View File

@@ -15,15 +15,15 @@ class Configuration extends Component
{ {
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) { if (!$project) {
return $this->redirectRoute('dashboard', navigate: true); return redirect()->route('dashboard');
} }
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) { if (!$environment) {
return $this->redirectRoute('dashboard', navigate: true); return redirect()->route('dashboard');
} }
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first(); $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) { if (!$application) {
return $this->redirectRoute('dashboard', navigate: true); return redirect()->route('dashboard');
} }
$this->application = $application; $this->application = $application;
$mainServer = $this->application->destination->server; $mainServer = $this->application->destination->server;

View File

@@ -64,6 +64,8 @@ class General extends Component
'application.custom_labels' => 'nullable', 'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable', 'application.dockerfile_target_build' => 'nullable',
'application.settings.is_static' => 'boolean|required', 'application.settings.is_static' => 'boolean|required',
'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_command' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@@ -94,6 +96,8 @@ class General extends Component
'application.custom_labels' => 'Custom labels', 'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build', 'application.dockerfile_target_build' => 'Dockerfile target build',
'application.settings.is_static' => 'Is static', '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() public function mount()
{ {
@@ -109,7 +113,7 @@ class General extends Component
$this->application->isConfigurationChanged(true); $this->application->isConfigurationChanged(true);
} }
$this->isConfigurationChanged = $this->application->isConfigurationChanged(); $this->isConfigurationChanged = $this->application->isConfigurationChanged();
$this->customLabels = $this->application->parseContainerLabels(); $this->customLabels = $this->application->parseContainerLabels();
$this->initialDockerComposeLocation = $this->application->docker_compose_location; $this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->checkLabelUpdates(); $this->checkLabelUpdates();
} }
@@ -195,7 +199,8 @@ class General extends Component
public function submit($showToaster = true) public function submit($showToaster = true)
{ {
try { 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->loadComposeFile();
} }
$this->validate(); $this->validate();

View File

@@ -60,12 +60,12 @@ class Heading extends Component
force_rebuild: false, force_rebuild: false,
is_new_deployment: true, is_new_deployment: true,
); );
return $this->redirectRoute('project.application.deployment', [ return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'], 'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid, 'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'], 'environment_name' => $this->parameters['environment_name'],
], navigate: true); ]);
} }
public function deploy(bool $force_rebuild = false) public function deploy(bool $force_rebuild = false)
{ {
@@ -73,18 +73,22 @@ class Heading extends Component
$this->dispatch('error', 'Please load a Compose file first.'); $this->dispatch('error', 'Please load a Compose file first.');
return; 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(); $this->setDeploymentUuid();
queue_application_deployment( queue_application_deployment(
application_id: $this->application->id, application_id: $this->application->id,
deployment_uuid: $this->deploymentUuid, deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild, force_rebuild: $force_rebuild,
); );
$this->redirectRoute('project.application.deployment', [ return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'], 'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid, 'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'], 'environment_name' => $this->parameters['environment_name'],
], navigate: true); ]);
} }
protected function setDeploymentUuid() protected function setDeploymentUuid()
@@ -109,12 +113,12 @@ class Heading extends Component
restart_only: true, restart_only: true,
is_new_deployment: true, is_new_deployment: true,
); );
return $this->redirectRoute('project.application.deployment', [ return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'], 'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid, 'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'], 'environment_name' => $this->parameters['environment_name'],
], navigate: true); ]);
} }
public function restart() public function restart()
{ {
@@ -124,11 +128,11 @@ class Heading extends Component
deployment_uuid: $this->deploymentUuid, deployment_uuid: $this->deploymentUuid,
restart_only: true, restart_only: true,
); );
return $this->redirectRoute('project.application.deployment', [ return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'], 'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid, 'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'], 'environment_name' => $this->parameters['environment_name'],
], navigate: true); ]);
} }
} }

View File

@@ -52,12 +52,12 @@ class Previews extends Component
force_rebuild: true, force_rebuild: true,
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
); );
return $this->redirectRoute('project.application.deployment', [ return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'], 'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deployment_uuid, 'deployment_uuid' => $this->deployment_uuid,
'environment_name' => $this->parameters['environment_name'], 'environment_name' => $this->parameters['environment_name'],
], navigate: true); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -72,10 +72,14 @@ class Previews extends Component
public function stop(int $pull_request_id) public function stop(int $pull_request_id)
{ {
try { try {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id); if ($this->application->destination->server->isSwarm()) {
foreach ($containers as $container) { instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
$name = str_replace('/', '', $container['Names']); } else {
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false); $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(); ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
$this->application->refresh(); $this->application->refresh();

View File

@@ -29,12 +29,12 @@ class Rollback extends Component
commit: $commit, commit: $commit,
force_rebuild: false, force_rebuild: false,
); );
return $this->redirectRoute('project.application.deployment', [ return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'], 'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $deployment_uuid, 'deployment_uuid' => $deployment_uuid,
'environment_name' => $this->parameters['environment_name'], 'environment_name' => $this->parameters['environment_name'],
], navigate: true); ]);
} }
public function loadImages($showToast = false) public function loadImages($showToast = false)

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

@@ -152,10 +152,10 @@ class CloneProject extends Component
} }
$newService->parse(); $newService->parse();
} }
return $this->redirectRoute('project.resources', [ return redirect()->route('project.resources', [
'project_uuid' => $newProject->uuid, 'project_uuid' => $newProject->uuid,
'environment_name' => $newEnvironment->name, 'environment_name' => $newEnvironment->name,
], navigate: true); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -50,9 +50,9 @@ class BackupEdit extends Component
$url = $url->withoutQueryParameter('selectedBackupId'); $url = $url->withoutQueryParameter('selectedBackupId');
$url = $url->withFragment('backups'); $url = $url->withFragment('backups');
$url = $url->getPath() . "#{$url->getFragment()}"; $url = $url->getPath() . "#{$url->getFragment()}";
return $this->redirect($url,navigate: true); return redirect($url);
} else { } else {
return $this->redirectRoute('project.database.backups.all', $this->parameters); return redirect()->route('project.database.backups.all', $this->parameters);
} }
} }

View File

@@ -23,7 +23,7 @@ class DeleteEnvironment extends Component
$environment = Environment::findOrFail($this->environment_id); $environment = Environment::findOrFail($this->environment_id);
if ($environment->isEmpty()) { if ($environment->isEmpty()) {
$environment->delete(); $environment->delete();
return $this->redirectRoute('project.show', ['project_uuid' => $this->parameters['project_uuid']], navigate: true); return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]);
} }
return $this->dispatch('error', 'Environment has defined resources, please delete them first.'); return $this->dispatch('error', 'Environment has defined resources, please delete them first.');
} }

View File

@@ -25,6 +25,6 @@ class DeleteProject extends Component
return $this->dispatch('error', 'Project has resources defined, please delete them first.'); return $this->dispatch('error', 'Project has resources defined, please delete them first.');
} }
$project->delete(); $project->delete();
return $this->redirectRoute('projects', navigate: true); return redirect()->route('projects');
} }
} }

View File

@@ -69,7 +69,7 @@ class DockerCompose extends Component
$service->parse(isNew: true); $service->parse(isNew: true);
return $this->redirectRoute('project.service.configuration', [ return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid, 'service_uuid' => $service->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,

View File

@@ -64,11 +64,11 @@ class DockerImage extends Component
'name' => 'docker-image-' . $application->uuid, 'name' => 'docker-image-' . $application->uuid,
'fqdn' => $fqdn 'fqdn' => $fqdn
]); ]);
return $this->redirectRoute('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
], navigate: true); ]);
} }
public function render() public function render()
{ {

View File

@@ -13,6 +13,6 @@ class EmptyProject extends Component
'name' => generate_random_name(), 'name' => generate_random_name(),
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ]);
return $this->redirectRoute('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production'], navigate: true); return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production']);
} }
} }

View File

@@ -151,11 +151,11 @@ class GithubPrivateRepository extends Component
$application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid); $application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
$application->save(); $application->save();
return $this->redirectRoute('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
], navigate: true); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -132,11 +132,11 @@ class GithubPrivateRepositoryDeployKey extends Component
$application->name = generate_random_name($application->uuid); $application->name = generate_random_name($application->uuid);
$application->save(); $application->save();
return $this->redirectRoute('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
], navigate: true); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -184,11 +184,11 @@ class PublicGitRepository extends Component
$application->fqdn = $fqdn; $application->fqdn = $fqdn;
$application->save(); $application->save();
return $this->redirectRoute('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
], navigate: true); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -6,22 +6,24 @@ use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use Countable; use Countable;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Livewire\Component; use Livewire\Component;
class Select extends Component class Select extends Component
{ {
public $current_step = 'type'; public $current_step = 'type';
public ?int $server = null; public ?Server $server = null;
public string $type; public string $type;
public string $server_id; public string $server_id;
public string $destination_uuid; public string $destination_uuid;
public Countable|array|Server $allServers = [];
public Countable|array|Server $servers = []; public Countable|array|Server $servers = [];
public Collection|array $standaloneDockers = []; public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = []; public Collection|array $swarmDockers = [];
public array $parameters; public array $parameters;
public Collection|array $services = []; public Collection|array $services = [];
public Collection|array $allServices = []; public Collection|array $allServices = [];
public bool $isDatabase = false;
public bool $includeSwarm = true;
public bool $loadingServices = true; public bool $loadingServices = true;
public bool $loading = false; public bool $loading = false;
@@ -31,7 +33,7 @@ class Select extends Component
public ?string $search = null; public ?string $search = null;
protected $queryString = [ protected $queryString = [
'server', 'server_id',
'search' 'search'
]; ];
@@ -53,10 +55,10 @@ class Select extends Component
public function updatedSelectedEnvironment() public function updatedSelectedEnvironment()
{ {
return $this->redirectRoute('project.resources.new', [ return redirect()->route('project.resources.new', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->selectedEnvironment, 'environment_name' => $this->selectedEnvironment,
], navigate: true); ]);
} }
// public function addExistingPostgresql() // public function addExistingPostgresql()
@@ -97,21 +99,45 @@ class Select extends Component
$this->loadingServices = false; $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) public function setType(string $type)
{ {
$this->type = $type;
if ($this->loading) return; if ($this->loading) return;
$this->loading = true; $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')) {
$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") { if ($type === "existing-postgresql") {
$this->current_step = $type; $this->current_step = $type;
return; return;
} }
if (count($this->servers) === 1) { // if (count($this->servers) === 1) {
$server = $this->servers->first(); // $server = $this->servers->first();
$this->setServer($server); // $this->setServer($server);
} // }
if (!is_null($this->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) { if ($foundServer) {
return $this->setServer($foundServer); return $this->setServer($foundServer);
} }
@@ -122,6 +148,7 @@ class Select extends Component
public function setServer(Server $server) public function setServer(Server $server)
{ {
$this->server_id = $server->id; $this->server_id = $server->id;
$this->server = $server;
$this->standaloneDockers = $server->standaloneDockers; $this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers; $this->swarmDockers = $server->swarmDockers;
$this->current_step = 'destinations'; $this->current_step = 'destinations';
@@ -142,5 +169,6 @@ class Select extends Component
public function loadServers() public function loadServers()
{ {
$this->servers = Server::isUsable()->get(); $this->servers = Server::isUsable()->get();
$this->allServers = $this->servers;
} }
} }

View File

@@ -70,10 +70,10 @@ CMD ["nginx", "-g", "daemon off;"]
'fqdn' => $fqdn 'fqdn' => $fqdn
]); ]);
return $this->redirectRoute('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
], navigate: true); ]);
} }
} }

View File

@@ -41,7 +41,7 @@ class Application extends Component
try { try {
$this->application->delete(); $this->application->delete();
$this->dispatch('success', 'Application deleted successfully.'); $this->dispatch('success', 'Application deleted successfully.');
return $this->redirectRoute('project.service.configuration', $this->parameters, navigate: true); return redirect()->route('project.service.configuration', $this->parameters);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -25,10 +25,10 @@ class Danger extends Component
{ {
try { try {
DeleteResourceJob::dispatchSync($this->resource); DeleteResourceJob::dispatchSync($this->resource);
return $this->redirectRoute('project.resources', [ return redirect()->route('project.resources', [
'project_uuid' => $this->projectUuid, 'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName 'environment_name' => $this->environmentName
], navigate: true); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -65,7 +65,11 @@ class Show extends Component
public function delete() public function delete()
{ {
$this->env->delete(); try {
$this->dispatch('refreshEnvs'); $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 (!$refresh && $this->resource?->getMorphClass() === 'App\Models\Service') return;
if ($this->container) { if ($this->container) {
if ($this->showTimeStamps) { 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 { } 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) { if ($refresh) {
$this->outputs = ''; $this->outputs = '';

View File

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

View File

@@ -2,22 +2,26 @@
namespace App\Livewire\Project\Shared\Storages; namespace App\Livewire\Project\Shared\Storages;
use App\Models\Application;
use Livewire\Component; use Livewire\Component;
class Add extends Component class Add extends Component
{ {
public $uuid; public $uuid;
public $parameters; public $parameters;
public $isSwarm = false;
public string $name; public string $name;
public string $mount_path; public string $mount_path;
public string|null $host_path = null; public ?string $host_path = null;
protected $listeners = ['clearAddStorage' => 'clear']; public $rules = [
protected $rules = [
'name' => 'required|string', 'name' => 'required|string',
'mount_path' => 'required|string', 'mount_path' => 'required|string',
'host_path' => 'string|nullable', 'host_path' => 'string|nullable',
]; ];
protected $listeners = ['clearAddStorage' => 'clear'];
protected $validationAttributes = [ protected $validationAttributes = [
'name' => 'name', 'name' => 'name',
'mount_path' => 'mount', 'mount_path' => 'mount',
@@ -27,17 +31,33 @@ class Add extends Component
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $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() public function submit()
{ {
$this->validate(); try {
$name = $this->uuid . '-' . $this->name; $this->validate($this->rules);
$this->dispatch('addNewVolume', [ $name = $this->uuid . '-' . $this->name;
'name' => $name, $this->dispatch('addNewVolume', [
'mount_path' => $this->mount_path, 'name' => $name,
'host_path' => $this->host_path, 'mount_path' => $this->mount_path,
]); 'host_path' => $this->host_path,
]);
$this->dispatch('closeStorageModal');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function clear() public function clear()

View File

@@ -19,7 +19,7 @@ class Delete extends Component
return; return;
} }
$this->server->delete(); $this->server->delete();
return $this->redirectRoute('server.all', navigate: true); return redirect()->route('server.all');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -15,7 +15,7 @@ class Show extends Component
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) { if (is_null($this->server)) {
return $this->redirectRoute('server.all', navigate: true); return redirect()->route('server.all');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

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

View File

@@ -43,7 +43,7 @@ class LogDrains extends Component
try { try {
$server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($server)) { if (is_null($server)) {
return $this->redirectRoute('server.all', navigate: true); return redirect()->route('server.all');
} }
$this->server = $server; $this->server = $server;
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

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

View File

@@ -17,7 +17,7 @@ class Show extends Component
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) { if (is_null($this->server)) {
return $this->redirectRoute('server.all', navigate: true); return redirect()->route('server.all');
} }
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false); $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false);
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -15,7 +15,7 @@ class Logs extends Component
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) { if (is_null($this->server)) {
return $this->redirectRoute('server.all', navigate: true); return redirect()->route('server.all');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -20,7 +20,7 @@ class Show extends Component
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) { if (is_null($this->server)) {
return $this->redirectRoute('server.all', navigate: true); return redirect()->route('server.all');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -15,7 +15,6 @@ class Status extends Component
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling']; protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function mount() { public function mount() {
$this->checkProxy();
} }
public function startProxyPolling() public function startProxyPolling()
{ {

View File

@@ -17,7 +17,7 @@ class Show extends Component
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) { if (is_null($this->server)) {
return $this->redirectRoute('server.all', navigate: true); return redirect()->route('server.all');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -41,7 +41,7 @@ class Change extends Component
$github_app_uuid = request()->github_app_uuid; $github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::where('uuid', $github_app_uuid)->first(); $this->github_app = GithubApp::where('uuid', $github_app_uuid)->first();
if (!$this->github_app) { if (!$this->github_app) {
return $this->redirectRoute('source.all', navigate: true); return redirect()->route('source.all');
} }
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
@@ -67,7 +67,7 @@ class Change extends Component
$type = data_get($parameters, 'type'); $type = data_get($parameters, 'type');
$destination = data_get($parameters, 'destination'); $destination = data_get($parameters, 'destination');
session()->forget('from'); session()->forget('from');
return $this->redirectRoute($back, [ return redirect()->route($back, [
'environment_name' => $environment_name, 'environment_name' => $environment_name,
'project_uuid' => $project_uuid, 'project_uuid' => $project_uuid,
'type' => $type, 'type' => $type,
@@ -117,7 +117,7 @@ class Change extends Component
{ {
try { try {
$this->github_app->delete(); $this->github_app->delete();
return $this->redirectRoute('source.all', navigate: true); return redirect()->route('source.all');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -48,7 +48,7 @@ class Create extends Component
if (session('from')) { if (session('from')) {
session(['from' => session('from') + ['source_id' => $github_app->id]]); session(['from' => session('from') + ['source_id' => $github_app->id]]);
} }
return $this->redirectRoute('source.github.show', ['github_app_uuid' => $github_app->uuid], navigate: true); return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Subscription; namespace App\Livewire\Subscription;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Providers\RouteServiceProvider;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
@@ -11,7 +12,7 @@ class Show extends Component
public bool $alreadySubscribed = false; public bool $alreadySubscribed = false;
public function mount() { public function mount() {
if (!isCloud()) { if (!isCloud()) {
return $this->redirect('/', navigate: true); return redirect(RouteServiceProvider::HOME);
} }
$this->settings = InstanceSettings::get(); $this->settings = InstanceSettings::get();
$this->alreadySubscribed = currentTeam()->subscription()->exists(); $this->alreadySubscribed = currentTeam()->subscription()->exists();

View File

@@ -30,7 +30,7 @@ class Create extends Component
]); ]);
auth()->user()->teams()->attach($team, ['role' => 'admin']); auth()->user()->teams()->attach($team, ['role' => 'admin']);
refreshSession(); refreshSession();
return $this->redirectRoute('team.index', navigate: true); return redirect()->route('team.index');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -24,6 +24,6 @@ class Delete extends Component
}); });
refreshSession(); refreshSession();
return $this->redirectRoute('team.index', navigate: true); return redirect()->route('team.index');
} }
} }

View File

@@ -65,7 +65,7 @@ class Create extends Component
$this->storage->team_id = currentTeam()->id; $this->storage->team_id = currentTeam()->id;
$this->storage->testConnection(); $this->storage->testConnection();
$this->storage->save(); $this->storage->save();
return $this->redirectRoute('team.storages.show', $this->storage->uuid, navigate: true); return redirect()->route('team.storages.show', $this->storage->uuid);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -43,7 +43,7 @@ class Form extends Component
{ {
try { try {
$this->storage->delete(); $this->storage->delete();
return $this->redirectRoute('team.storages.all', navigate: true); return redirect()->route('team.storages.all');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -24,7 +24,7 @@ class Index extends Component
public function mount() public function mount()
{ {
if (config('coolify.waitlist') == false) { if (config('coolify.waitlist') == false) {
return $this->redirectRoute('register', navigate: true); return redirect()->route('register');
} }
$this->waitingInLine = Waitlist::whereVerified(true)->count(); $this->waitingInLine = Waitlist::whereVerified(true)->count();
$this->users = User::count(); $this->users = User::count();

View File

@@ -633,7 +633,7 @@ class Application extends BaseModel
'memswap_limit' => $this->limits_memory_swap, 'memswap_limit' => $this->limits_memory_swap,
'mem_swappiness' => $this->limits_memory_swappiness, 'mem_swappiness' => $this->limits_memory_swappiness,
'mem_reservation' => $this->limits_memory_reservation, 'mem_reservation' => $this->limits_memory_reservation,
'cpus' => (int) $this->limits_cpus, 'cpus' => (float) $this->limits_cpus,
'cpuset' => $this->limits_cpuset, 'cpuset' => $this->limits_cpuset,
'cpu_shares' => $this->limits_cpu_shares, 'cpu_shares' => $this->limits_cpu_shares,
] ]

View File

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

View File

@@ -54,7 +54,7 @@ class Revived extends Notification implements ShouldQueue
public function toDiscord(): string 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; return $message;
} }
public function toTelegram(): array public function toTelegram(): array

View File

@@ -31,9 +31,9 @@ class FortifyServiceProvider extends ServiceProvider
{ {
// First user (root) will be redirected to /settings instead of / on registration. // First user (root) will be redirected to /settings instead of / on registration.
if ($request->user()->currentTeam->id === 0) { if ($request->user()->currentTeam->id === 0) {
return redirect('/settings'); return redirect()->route('settings.configuration');
} }
return redirect('/'); return redirect(RouteServiceProvider::HOME);
} }
}); });
} }

View File

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

View File

@@ -15,10 +15,16 @@ function get_proxy_path()
} }
function connectProxyToNetworks(Server $server) function connectProxyToNetworks(Server $server)
{ {
// Standalone networks if ($server->isSwarm()) {
$networks = collect($server->standaloneDockers)->map(function ($docker) { $networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network']; return $docker['network'];
}); });
} else {
// Standalone networks
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
});
}
// Service networks // Service networks
foreach ($server->services()->get() as $service) { foreach ($server->services()->get() as $service) {
$networks->push($service->networks()); $networks->push($service->networks());
@@ -41,16 +47,30 @@ function connectProxyToNetworks(Server $server)
$networks->push($network); $networks->push($network);
} }
$networks = collect($networks)->flatten()->unique(); $networks = collect($networks)->flatten()->unique();
if ($networks->count() === 0) { if ($server->isSwarm()) {
$networks = collect(['coolify']); 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(); return $commands->flatten();
} }
function generate_default_proxy_configuration(Server $server) 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) { $networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network']; return $docker['network'];
})->unique(); })->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify-overlay']);
}
} else { } else {
$networks = collect($server->standaloneDockers)->map(function ($docker) { $networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network']; return $docker['network'];
})->unique(); })->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
} }
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
$array_of_networks = collect([]); $array_of_networks = collect([]);
$networks->map(function ($network) use ($array_of_networks) { $networks->map(function ($network) use ($array_of_networks) {
$array_of_networks[$network] = [ $array_of_networks[$network] = [

View File

@@ -1330,7 +1330,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($value?->startsWith('$')) { if ($value?->startsWith('$')) {
$foundEnv = EnvironmentVariable::where([ $foundEnv = EnvironmentVariable::where([
'key' => $key, 'key' => $key,
'service_id' => $resource->id, 'application_id' => $resource->id,
'is_preview' => false,
])->first(); ])->first();
$value = Str::of(replaceVariables($value)); $value = Str::of(replaceVariables($value));
$key = $value; $key = $value;
@@ -1397,14 +1398,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$defaultValue = data_get($foundEnv, 'value'); $defaultValue = data_get($foundEnv, 'value');
} }
$isBuildTime = data_get($foundEnv, 'is_build_time', false); $isBuildTime = data_get($foundEnv, 'is_build_time', false);
EnvironmentVariable::updateOrCreate([ if ($foundEnv) {
'key' => $key->value(), $foundEnv->update([
'application_id' => $resource->id, 'key' => $key,
], [ 'application_id' => $resource->id,
'value' => $defaultValue, 'is_build_time' => $isBuildTime,
'is_build_time' => $isBuildTime, 'value' => $defaultValue,
'application_id' => $resource->id, ]);
]); } else {
EnvironmentVariable::create([
'key' => $key,
'value' => $defaultValue,
'is_build_time' => $isBuildTime,
'application_id' => $resource->id,
'is_preview' => false,
]);
}
} }
} }
} }

991
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.164'; return '4.0.0-beta.173';

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

@@ -1,17 +1,19 @@
<div class="navbar-main"> <div class="navbar-main">
<a wire:navigate class="{{ request()->routeIs('project.application.configuration') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.application.configuration') ? 'text-white' : '' }}"
href="{{ route('project.application.configuration', $parameters) }}"> href="{{ route('project.application.configuration', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<a wire:navigate class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}" @if (!$application->destination->server->isSwarm())
href="{{ route('project.application.command', $parameters) }}"> <a class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}"
<button>Execute Command</button> href="{{ route('project.application.command', $parameters) }}">
</a> <button>Execute Command</button>
<a wire:navigate class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}" </a>
@endif
<a class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}"> href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button> <button>Logs</button>
</a> </a>
<a wire:navigate class="{{ request()->routeIs('project.application.deployments') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.application.deployments') ? 'text-white' : '' }}"
href="{{ route('project.application.deployments', $parameters) }}"> href="{{ route('project.application.deployments', $parameters) }}">
<button>Deployments</button> <button>Deployments</button>
</a> </a>
@@ -19,37 +21,54 @@
<div class="flex-1"></div> <div class="flex-1"></div>
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)) @if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div> <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 @else
<x-applications.advanced :application="$application" /> @if (!$application->destination->server->isSwarm())
<x-applications.advanced :application="$application" />
@endif
@if ($application->status !== 'exited') @if ($application->status !== 'exited')
<button title="With rolling update if possible" wire:click='deploy' @if (!$application->destination->server->isSwarm())
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <button title="With rolling update if possible" wire:click='deploy'
<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->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"> 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"> <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-width="2"> stroke-linejoin="round">
<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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 4v5h-5" /> <path
</g> 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> </svg>
Restart Redeploy
</button> </button>
@if (isDev()) @endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
<button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'
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>
Update Service
</button>
@else
<button title="Restart without rebuilding" 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>
Restart
</button>
@endif
{{-- @if (isDev())
<button title="Restart without rebuilding" wire:click='restartNew' <button title="Restart without rebuilding" wire:click='restartNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -61,7 +80,7 @@
</svg> </svg>
Restart (new) Restart (new)
</button> </button>
@endif @endif --}}
@endif @endif
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <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" <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 +102,7 @@
</svg> </svg>
Deploy Deploy
</button> </button>
@if (isDev()) {{-- @if (isDev())
<button wire:click='deployNew' <button wire:click='deployNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
@@ -94,7 +113,7 @@
</svg> </svg>
Deploy (new) Deploy (new)
</button> </button>
@endif @endif --}}
@endif @endif
@endif @endif
</div> </div>

View File

@@ -1,13 +1,13 @@
<div class="navbar-main"> <div class="navbar-main">
<a wire:navigate class="{{ request()->routeIs('project.database.configuration') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.database.configuration') ? 'text-white' : '' }}"
href="{{ route('project.database.configuration', $parameters) }}"> href="{{ route('project.database.configuration', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<a wire:navigate class="{{ request()->routeIs('project.database.command') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.database.command') ? 'text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}"> href="{{ route('project.database.command', $parameters) }}">
<button>Execute Command</button> <button>Execute Command</button>
</a> </a>
<a wire:navigate class="{{ request()->routeIs('project.database.logs') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.database.logs') ? 'text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}"> href="{{ route('project.database.logs', $parameters) }}">
<button>Logs</button> <button>Logs</button>
</a> </a>
@@ -16,7 +16,7 @@
$database->getMorphClass() === 'App\Models\StandaloneMongodb' || $database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$database->getMorphClass() === 'App\Models\StandaloneMysql' || $database->getMorphClass() === 'App\Models\StandaloneMysql' ||
$database->getMorphClass() === 'App\Models\StandaloneMariadb') $database->getMorphClass() === 'App\Models\StandaloneMariadb')
<a wire:navigate class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}"
href="{{ route('project.database.backups.all', $parameters) }}"> href="{{ route('project.database.backups.all', $parameters) }}">
<button>Backups</button> <button>Backups</button>
</a> </a>

View File

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

View File

@@ -4,7 +4,7 @@
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a> class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap"> <ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard"> <li title="Dashboard">
<a wire:navigate class="hover:bg-transparent" href="/"> <a class="hover:bg-transparent" href="/">
<svg xmlns="http://www.w3.org/2000/svg" class="{{ request()->is('/') ? 'text-warning icon' : 'icon' }}" <svg xmlns="http://www.w3.org/2000/svg" class="{{ request()->is('/') ? 'text-warning icon' : 'icon' }}"
fill="none" viewBox="0 0 24 24" stroke="currentColor"> fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -13,7 +13,7 @@
</a> </a>
</li> </li>
<li title="Servers"> <li title="Servers">
<a wire:navigate class="hover:bg-transparent" href="/servers"> <a class="hover:bg-transparent" href="/servers">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('server/*') || request()->is('servers') ? 'text-warning icon' : 'icon' }}" class="{{ request()->is('server/*') || request()->is('servers') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -28,7 +28,7 @@
</a> </a>
</li> </li>
<li title="Projects"> <li title="Projects">
<a wire:navigate class="hover:bg-transparent" href="/projects"> <a class="hover:bg-transparent" href="/projects">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('project/*') || request()->is('projects') ? 'text-warning icon' : 'icon' }}" class="{{ request()->is('project/*') || request()->is('projects') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -41,7 +41,7 @@
</a> </a>
</li> </li>
<li title="Command Center"> <li title="Command Center">
<a wire:navigate class="hover:bg-transparent" href="/command-center"> <a class="hover:bg-transparent" href="/command-center">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24" class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -53,7 +53,7 @@
</a> </a>
</li> </li>
<li title="Source"> <li title="Source">
<a wire:navigate class="hover:bg-transparent" href="{{ route('source.all') }}"> <a class="hover:bg-transparent" href="{{ route('source.all') }}">
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" <path fill="currentColor"
d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" /> d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" />
@@ -61,7 +61,7 @@
</a> </a>
</li> </li>
<li title="Security"> <li title="Security">
<a wire:navigate class="hover:bg-transparent" href="{{ route('security.private-key.index') }}"> <a class="hover:bg-transparent" href="{{ route('security.private-key.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
@@ -70,7 +70,7 @@
</a> </a>
</li> </li>
<li title="Teams"> <li title="Teams">
<a wire:navigate class="hover:bg-transparent" href="{{ route('team.index') }}"> <a class="hover:bg-transparent" href="{{ route('team.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
@@ -103,7 +103,7 @@
</a> </a>
</li> </li>
<li title="Profile"> <li title="Profile">
<a wire:navigate class="hover:bg-transparent" href="/profile"> <a class="hover:bg-transparent" href="/profile">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
@@ -116,7 +116,7 @@
@if (isInstanceAdmin()) @if (isInstanceAdmin())
<li title="Settings" class="mt-auto"> <li title="Settings" class="mt-auto">
<a wire:navigate class="hover:bg-transparent" href="/settings"> <a class="hover:bg-transparent" href="/settings">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24" class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"

View File

@@ -13,7 +13,7 @@
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"></path>
</svg> </svg>
<a wire:navigate class="text-xs truncate lg:text-sm" <a class="text-xs truncate lg:text-sm"
href="{{ route('project.resources', ['environment_name' => $this->parameters['environment_name'], 'project_uuid' => $this->parameters['project_uuid']]) }}">{{ $this->parameters['environment_name'] }}</a> href="{{ route('project.resources', ['environment_name' => $this->parameters['environment_name'], 'project_uuid' => $this->parameters['project_uuid']]) }}">{{ $this->parameters['environment_name'] }}</a>
</div> </div>
</li> </li>

View File

@@ -10,10 +10,10 @@
</ol> </ol>
</nav> </nav>
<nav class="navbar-main"> <nav class="navbar-main">
<a wire:navigate href="{{ route('security.private-key.index') }}"> <a href="{{ route('security.private-key.index') }}">
<button>Private Keys</button> <button>Private Keys</button>
</a> </a>
<a wire:navigate href="{{ route('security.api-tokens') }}"> <a href="{{ route('security.api-tokens') }}">
<button>API tokens</button> <button>API tokens</button>
</a> </a>
<div class="flex-1"></div> <div class="flex-1"></div>

View File

@@ -2,43 +2,48 @@
<livewire:server.proxy.modal :server="$server" /> <livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Server</h1> <h1>Server</h1>
@if ($server->proxyType() !== 'NONE') @if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker())
<livewire:server.proxy.status :server="$server" /> <livewire:server.proxy.status :server="$server" />
@endif @endif
</div> </div>
<div class="subtitle ">{{ data_get($server, 'name') }}</div> <div class="subtitle ">{{ data_get($server, 'name') }}</div>
<nav class="navbar-main"> <nav class="navbar-main">
<a wire:navigate class="{{ request()->routeIs('server.show') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('server.show') ? 'text-white' : '' }}"
href="{{ route('server.show', [ href="{{ route('server.show', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
<button>General</button> <button>General</button>
</a> </a>
<a wire:navigate class="{{ request()->routeIs('server.private-key') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('server.private-key') ? 'text-white' : '' }}"
href="{{ route('server.private-key', [ href="{{ route('server.private-key', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
<button>Private Key</button> <button>Private Key</button>
</a> </a>
<a wire:navigate class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}" @if (!$server->isSwarmWorker())
href="{{ route('server.proxy', [ <a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
'server_uuid' => data_get($parameters, 'server_uuid'), href="{{ route('server.proxy', [
]) }}"> 'server_uuid' => data_get($parameters, 'server_uuid'),
<button>Proxy</button> ]) }}">
</a> <button>Proxy</button>
<a wire:navigate class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}" </a>
href="{{ route('server.destinations', [ <a class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}"
'server_uuid' => data_get($parameters, 'server_uuid'), href="{{ route('server.destinations', [
]) }}"> 'server_uuid' => data_get($parameters, 'server_uuid'),
<button>Destinations</button> ]) }}">
</a> <button>Destinations</button>
<a wire:navigate class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}" </a>
href="{{ route('server.log-drains', [ <a class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
'server_uuid' => data_get($parameters, 'server_uuid'), href="{{ route('server.log-drains', [
]) }}"> 'server_uuid' => data_get($parameters, 'server_uuid'),
<button>Log Drains</button> ]) }}">
</a> <button>Log Drains</button>
</a>
@endif
<div class="flex-1"></div> <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> </nav>
</div> </div>

View File

@@ -1,5 +1,5 @@
<div class="navbar-main" x-data> <div class="navbar-main" x-data>
<a wire:navigate class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
href="{{ route('project.service.configuration', $parameters) }}"> href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>

View File

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

View File

@@ -1,7 +1,7 @@
<div class="pb-6"> <div class="pb-6">
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<h1>Team</h1> <h1>Team</h1>
<a wire:navigate href="/team/new"><x-forms.button>+ New Team</x-forms.button></a> <a href="/team/new"><x-forms.button>+ New Team</x-forms.button></a>
</div> </div>
<nav class="flex pt-2 pb-10"> <nav class="flex pt-2 pb-10">
<ol class="inline-flex items-center"> <ol class="inline-flex items-center">
@@ -14,17 +14,17 @@
</ol> </ol>
</nav> </nav>
<nav class="navbar-main"> <nav class="navbar-main">
<a wire:navigate class="{{ request()->routeIs('team.index') ? 'text-white' : '' }}" href="{{ route('team.index') }}"> <a class="{{ request()->routeIs('team.index') ? 'text-white' : '' }}" href="{{ route('team.index') }}">
<button>General</button> <button>General</button>
</a> </a>
<a wire:navigate class="{{ request()->routeIs('team.members') ? 'text-white' : '' }}" href="{{ route('team.members') }}"> <a class="{{ request()->routeIs('team.members') ? 'text-white' : '' }}" href="{{ route('team.members') }}">
<button>Members</button> <button>Members</button>
</a> </a>
<a wire:navigate class="{{ request()->routeIs('team.storages.all') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('team.storages.all') ? 'text-white' : '' }}"
href="{{ route('team.storages.all') }}"> href="{{ route('team.storages.all') }}">
<button>S3 Storages</button> <button>S3 Storages</button>
</a> </a>
<a wire:navigate class="{{ request()->routeIs('team.notifications') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('team.notifications') ? 'text-white' : '' }}"
href="{{ route('team.notifications') }}"> href="{{ route('team.notifications') }}">
<button>Notifications</button> <button>Notifications</button>
</a> </a>

View File

@@ -1,3 +1,3 @@
<x-layout> <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> </x-layout>

View File

@@ -29,14 +29,14 @@
@foreach ($projects as $project) @foreach ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group"> <div class="gap-2 border border-transparent cursor-pointer box group">
@if (data_get($project, 'environments.0.name')) @if (data_get($project, 'environments.0.name'))
<a wire:navigate class="flex flex-col flex-1 mx-6 hover:no-underline" <a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.resources', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}"> href="{{ route('project.resources', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<div class="font-bold text-white">{{ $project->name }}</div> <div class="font-bold text-white">{{ $project->name }}</div>
<div class="description"> <div class="description">
{{ $project->description }}</div> {{ $project->description }}</div>
</a> </a>
@else @else
<a wire:navigate class="flex flex-col flex-1 mx-6 hover:no-underline" <a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
<div class="font-bold text-white">{{ $project->name }}</div> <div class="font-bold text-white">{{ $project->name }}</div>
<div class="description"> <div class="description">
@@ -44,11 +44,11 @@
</a> </a>
@endif @endif
<div class="flex items-center"> <div class="flex items-center">
<a wire:navigate class="mx-4 rounded group-hover:text-white hover:no-underline" <a class="mx-4 rounded group-hover:text-white hover:no-underline"
href="{{ route('project.resources.new', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}"> href="{{ route('project.resources.new', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<span class="font-bold hover:text-warning">+ New Resource</span> <span class="font-bold hover:text-warning">+ New Resource</span>
</a> </a>
<a wire:navigate class="mx-4 rounded group-hover:text-white" <a class="mx-4 rounded group-hover:text-white"
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24" <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-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -72,7 +72,7 @@
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2"> <div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
@endif @endif
@foreach ($servers as $server) @foreach ($servers as $server)
<a wire:navigate href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" <a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
@class([ @class([
'gap-2 border cursor-pointer box group', 'gap-2 border cursor-pointer box group',
'border-transparent' => $server->settings->is_reachable, 'border-transparent' => $server->settings->is_reachable,

View File

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

View File

@@ -2,7 +2,7 @@
@if ($server->isFunctional()) @if ($server->isFunctional())
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<h2>Destinations</h2> <h2>Destinations</h2>
<a wire:navigate href="{{ route('destination.new', ['server_id' => $server->id]) }}"> <a href="{{ route('destination.new', ['server_id' => $server->id]) }}">
<x-forms.button>Add a new destination</x-forms.button> <x-forms.button>Add a new destination</x-forms.button>
</a> </a>
<x-forms.button wire:click='scan'>Scan destinations on the server</x-forms.button> <x-forms.button wire:click='scan'>Scan destinations on the server</x-forms.button>
@@ -11,7 +11,16 @@
<div class="flex gap-2 "> <div class="flex gap-2 ">
Available for using: Available for using:
@forelse ($server->standaloneDockers as $docker) @forelse ($server->standaloneDockers as $docker)
<a wire:navigate href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}"> <a
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
href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button> <button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a> </a>
@empty @empty
@@ -25,7 +34,7 @@
<div class="flex flex-wrap gap-2 "> <div class="flex flex-wrap gap-2 ">
@foreach ($networks as $network) @foreach ($networks as $network)
<div> <div>
<a wire:navigate <a
href="{{ route('destination.new', ['server_id' => $server->id, 'network_name' => data_get($network, 'Name')]) }}"> href="{{ route('destination.new', ['server_id' => $server->id, 'network_name' => data_get($network, 'Name')]) }}">
<x-forms.button>+<x-highlighted text="{{ data_get($network, 'Name') }}" /> <x-forms.button>+<x-highlighted text="{{ data_get($network, 'Name') }}" />
</x-forms.button> </x-forms.button>

View File

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

View File

@@ -11,7 +11,7 @@
<x-forms.button type="submit">Filter</x-forms.button> <x-forms.button type="submit">Filter</x-forms.button>
</form> </form>
@forelse ($deployments as $deployment) @forelse ($deployments as $deployment)
<a wire:navigate @class([ <a @class([
'bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline', 'bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline',
'hover:bg-coolgray-200' => data_get($deployment, 'status') === 'queued', 'hover:bg-coolgray-200' => data_get($deployment, 'status') === 'queued',
'border-warning hover:bg-warning hover:text-black' => 'border-warning hover:bg-warning hover:text-black' =>

View File

@@ -70,8 +70,11 @@
@if ($application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockercompose')
<h3>Docker Registry</h3> <h3>Docker Registry</h3>
@if ($application->destination->server->isSwarm()) @if ($application->destination->server->isSwarm())
<div>Docker Swarm requires the image to be available in a registry. More info <a class="underline" @if ($application->build_pack !== 'dockerimage')
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div> <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 @else
@if ($application->build_pack !== 'dockerimage') @if ($application->build_pack !== 'dockerimage')
<div>Push the built image to a docker registry. More info <a class="underline" <div>Push the built image to a docker registry. More info <a class="underline"
@@ -126,37 +129,58 @@
</div> </div>
@endif @endif
@endif @endif
<div class="flex flex-col gap-2 xl:flex-row"> @if ($application->build_pack === 'dockercompose')
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory" <div class="flex flex-col gap-2" wire:init='loadComposeFile(true)'>
helper="Directory to use as root. Useful for monorepos." /> <div class="flex gap-2">
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile) <x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location" helper="Directory to use as root. Useful for monorepos." />
label="Dockerfile Location" <x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>" /> label="Docker Compose Location"
@endif helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
@if ($application->build_pack === 'dockercompose') </div>
<span wire:init='loadComposeFile(true)'></span> <div class="pt-4">The following commands are for advanced use cases. Only modify them if you
<x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_location" know what are
label="Docker Compose Location" you doing.</div>
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 class="flex gap-2">
{{-- <x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_pr_location" <x-forms.input placeholder="docker compose build"
label="Docker Compose Location For Pull Requests" id="application.docker_compose_custom_build_command"
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>" /> --}} 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>"
@endif label="Custom Build Command" />
@if ($application->build_pack === 'dockerfile') <x-forms.input placeholder="docker compose up -d"
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target" id="application.docker_compose_custom_start_command"
helper="Useful if you have multi-staged dockerfile." /> 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>"
@endif label="Custom Start Command" />
@if ($application->could_set_build_commands()) {{-- <x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_pr_location"
@if ($application->settings->is_static) label="Docker Compose Location For Pull Requests"
<x-forms.input placeholder="/dist" id="application.publish_directory" 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>" /> --}}
label="Publish Directory" required /> </div>
@else </div>
<x-forms.input placeholder="/" id="application.publish_directory" @else
label="Publish Directory" /> <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
@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 @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button> <x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
@@ -179,8 +203,10 @@
required 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." /> 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 @endif
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings" @if (!$application->destination->server->isSwarm())
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." /> <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> </div>
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea> <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> <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

@@ -202,6 +202,18 @@
<li class="step step-secondary">Select a Server</li> <li class="step step-secondary">Select a Server</li>
<li class="step">Select a Destination</li> <li class="step">Select a Destination</li>
</ul> </ul>
@if ($isDatabase)
<div class="text-center">Swarm clusters are excluded from this type of resource at the moment. It will
be activated soon. Stay tuned.</div>
@endif
{{-- @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"> <div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap">
@forelse($servers as $server) @forelse($servers as $server)
<div class="box group" wire:click="setServer({{ $server }})"> <div class="box group" wire:click="setServer({{ $server }})">
@@ -232,28 +244,30 @@
</ul> </ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap"> <div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap">
@if ($server->isSwarm())
@foreach ($standaloneDockers as $standaloneDocker) @foreach ($swarmDockers as $swarmDocker)
<div class="box group" wire:click="setDestination('{{ $standaloneDocker->uuid }}')"> <div class="box group" wire:click="setDestination('{{ $swarmDocker->uuid }}')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white"> <div class="font-bold group-hover:text-white">
Standalone Docker <span class="text-xs">({{ $standaloneDocker->name }})</span> Swarm Docker <span class="text-xs">({{ $swarmDocker->name }})</span>
</div> </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> </div>
</div> </div>
</div> @endforeach
@endforeach @else
<a wire:navigate href="{{ route('destination.new', ['server_id' => $server_id]) }}" @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 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"> 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="flex flex-col mx-6 ">
<div class="font-bold text-white"> <div class="font-bold text-white">

View File

@@ -68,7 +68,7 @@
<div class="text-xs">{{ $application->status }}</div> <div class="text-xs">{{ $application->status }}</div>
</div> </div>
<div class="flex items-center px-4"> <div class="flex items-center px-4">
<a wire:navigate <a
class="flex flex-col flex-1 group-hover:text-white hover:no-underline" class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $application->name]) }}"> href="{{ route('project.service.show', [...$parameters, 'service_name' => $application->name]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" <svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning"
@@ -115,7 +115,7 @@
<div class="text-xs">{{ $database->status }}</div> <div class="text-xs">{{ $database->status }}</div>
</div> </div>
<div class="flex items-center px-4"> <div class="flex items-center px-4">
<a wire:navigate <a
class="flex flex-col flex-1 group-hover:text-white hover:no-underline" class="flex flex-col flex-1 group-hover:text-white hover:no-underline"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}"> href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" <svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning"

View File

@@ -2,7 +2,7 @@
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" /> <livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex h-full pt-6"> <div class="flex h-full pt-6">
<div class="flex flex-col gap-4 min-w-fit"> <div class="flex flex-col gap-4 min-w-fit">
<a wire:navigate class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
href="{{ route('project.service.configuration', [...$parameters, 'service_name' => null]) }}"> href="{{ route('project.service.configuration', [...$parameters, 'service_name' => null]) }}">
<button><- Back</button> <button><- Back</button>
</a> </a>

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