Compare commits

...

243 Commits

Author SHA1 Message Date
Andras Bacsai
e260bfae02 Merge pull request #1443 from coollabsio/next
v4.0.0-beta.128
2023-11-13 15:49:51 +01:00
Andras Bacsai
5abd4a6d78 Update version and fix MINIO_BROWSER_REDIRECT_URL
and MINIO_SERVER_URL
2023-11-13 15:49:23 +01:00
Andras Bacsai
9dff1e5631 Merge pull request #1442 from coollabsio/next
v4.0.0-beta.127
2023-11-13 15:42:28 +01:00
Andras Bacsai
02332ade1b Fix URLs and remove unnecessary command in
ApplicationDeploymentJob.php
2023-11-13 15:41:49 +01:00
Andras Bacsai
486de58d5b Update database start commands 2023-11-13 15:27:33 +01:00
Andras Bacsai
606aeb2b61 Merge pull request #1441 from coollabsio/next
v4.0.0-beta.126
2023-11-13 15:21:45 +01:00
Andras Bacsai
3fc264560c Update dependencies and fix minor bugs. 2023-11-13 15:19:49 +01:00
Andras Bacsai
3dd9182281 Add sponsorship notification and disable option,
update dependencies
2023-11-13 14:44:54 +01:00
Andras Bacsai
c838ff7198 Update version numbers to 4.0.0-beta.126 2023-11-13 13:38:50 +01:00
Andras Bacsai
ca6db9c1a9 Merge pull request #1440 from coollabsio/next
v4.0.0-beta.125
2023-11-13 13:21:00 +01:00
Andras Bacsai
f27e00e80e Update version.json to include v4.0.0-beta.125 2023-11-13 13:20:28 +01:00
Andras Bacsai
60cf296f31 Update preview application deployment labels and version 2023-11-13 13:20:12 +01:00
Andras Bacsai
ea64e9d5ad Merge pull request #1439 from coollabsio/next
v4.0.0-beta.124
2023-11-13 13:03:46 +01:00
Andras Bacsai
55846c5635 Fix service retrieval and add error handling 2023-11-13 12:59:59 +01:00
Andras Bacsai
7763594e6e Add pull_latest_image function and update
build_image function to use it. Also add check for
dockerfile existence in start_by_compose_file
function.
2023-11-13 12:30:25 +01:00
Andras Bacsai
6b5339c1c1 Remove ray debug statement and refactor random
name generator
2023-11-13 11:44:13 +01:00
Andras Bacsai
f2980738e4 Fix documentation link in service-templates.json 2023-11-13 11:30:20 +01:00
Andras Bacsai
f0e3ad0461 Merge pull request #1432 from AlejandroAkbal/main
fix(fider template): use the correct docs url
2023-11-13 11:29:39 +01:00
Andras Bacsai
187050e098 Merge pull request #1435 from AshikNesin/main
Fix typo in onboarding page
2023-11-13 11:29:02 +01:00
Andras Bacsai
9e7823795d Fix null check for MINIO_BROWSER_REDIRECT_URL and
MINIO_SERVER_URL in generateServiceSpecificFqdns
function
2023-11-13 11:17:49 +01:00
Andras Bacsai
239459dfa8 Remove commented out code for minio service 2023-11-13 11:13:16 +01:00
Andras Bacsai
ce0f560c44 Add service-specific configuration fields and save
them to the database
2023-11-13 11:09:21 +01:00
Andras Bacsai
95baec99dd Fix typo in General.php component 2023-11-13 09:04:19 +01:00
Andras Bacsai
363e8fc0b5 Update code with bug fixes and improvements 2023-11-13 08:46:43 +01:00
Andras Bacsai
e49caba920 Add STRIPE_EXCLUDED_PLANS to services in
docker-compose.prod.yml
2023-11-13 08:46:17 +01:00
Ashik Nesin
30db2b2a09 Update typo in onboarding screen 2023-11-12 19:30:20 +00:00
Andras Bacsai
285666e181 Merge pull request #1434 from coollabsio/next
v4.0.0-beta.123
2023-11-12 19:11:31 +01:00
Andras Bacsai
003934ee1d disable service confs for now 2023-11-12 19:10:54 +01:00
Andras Bacsai
44c7958aa6 make fqdn super long 2023-11-12 19:09:38 +01:00
Alejandro Akbal
35b1a81dfe fix(fider template): use the correct docs url 2023-11-12 12:10:53 +00:00
Andras Bacsai
e40f397cc7 fix: service updates 2023-11-11 21:32:41 +01:00
Andras Bacsai
9fd8cd7e6c Merge pull request #1430 from coollabsio/next
v4.0.0-beta.122
2023-11-11 10:19:28 +01:00
Andras Bacsai
a94b7ee611 fix: container status jobs for old pr deployments 2023-11-11 10:18:40 +01:00
Andras Bacsai
fc68bf50b5 save 2023-11-10 22:04:04 +01:00
Andras Bacsai
0f99ee787c Merge pull request #1429 from coollabsio/next
v4.0.0-beta.121
2023-11-10 21:30:49 +01:00
Andras Bacsai
95777e978e fix: revert workdir to basedir 2023-11-10 21:02:39 +01:00
Andras Bacsai
fb0b9dbfed Add subscription exclusion for certain plans in
webhook handling
2023-11-10 15:41:44 +01:00
Andras Bacsai
9617000daa Add stripe_excluded_plans config variable and
handle excluded plans in webhook
2023-11-10 15:36:02 +01:00
Andras Bacsai
1818404172 Refactor application configuration blade file to
conditionally display tabs based on build pack
2023-11-10 13:46:14 +01:00
Andras Bacsai
d9a966fd98 Fix broken link to framework specific docs in
general.blade.php
2023-11-10 13:42:17 +01:00
Andras Bacsai
763ce5fc14 Update version numbers and deployment logs styling 2023-11-10 13:38:29 +01:00
Andras Bacsai
df021760a7 Merge pull request #1423 from coollabsio/next
v4.0.0-beta.120
2023-11-10 12:06:55 +01:00
Andras Bacsai
fb2598f2e4 Update UI elements and add new build pack option (static) 2023-11-10 11:33:15 +01:00
Andras Bacsai
7af07b2718 Add logging to DockerCleanupJob 2023-11-10 10:55:23 +01:00
Andras Bacsai
23a94c9378 Refactor DockerCleanupJob and Application model 2023-11-10 10:34:28 +01:00
Andras Bacsai
ed34fc9645 Update defaultClass in Select component 2023-11-10 10:14:46 +01:00
Andras Bacsai
cafd9e0ab2 Convert cpus limits to integer in database and
application classes
2023-11-10 09:54:40 +01:00
Andras Bacsai
e882477e21 Refactor navbar and add help us link 2023-11-10 09:49:47 +01:00
Andras Bacsai
db0e3cfcc4 fix: database proxy for services
version++
tiny css modifications
2023-11-10 09:41:42 +01:00
Andras Bacsai
b3c4429028 Merge pull request #1422 from coollabsio/next
v4.0.0-beta.119
2023-11-09 15:10:56 +01:00
Andras Bacsai
87ab4bd71e fix: local ip address 2023-11-09 15:05:42 +01:00
Andras Bacsai
61e1fdede9 feat: make service databases public 2023-11-09 14:59:38 +01:00
Andras Bacsai
b189919f97 Merge pull request #1421 from coollabsio/next
v4.0.0-beta.118
2023-11-09 12:47:05 +01:00
Andras Bacsai
8f5b084931 Refactor environment variable saving logic. 2023-11-09 12:40:53 +01:00
Andras Bacsai
eb96a5ae7b Update user authentication logic to use bcrypt
hashing algorithm
2023-11-09 12:29:03 +01:00
Andras Bacsai
f0fb9dbb94 Update Sentry and version configs to
4.0.0-beta.118
2023-11-09 12:19:08 +01:00
Andras Bacsai
cb2d4b4a0a Merge pull request #1420 from coollabsio/next
v4.0.0-beta.117
2023-11-09 11:59:01 +01:00
Andras Bacsai
3aace2d4f9 Update email recipient in SendEmail.php 2023-11-09 11:58:12 +01:00
Andras Bacsai
8c2ed75653 Update Docker images and add Directus service with
PostgreSQL.
2023-11-09 11:52:51 +01:00
Andras Bacsai
8c8aafbc65 Update version and fix directory path in
deployment job
2023-11-09 11:33:37 +01:00
Andras Bacsai
a2c39fd07e Merge pull request #1416 from coollabsio/next
v4.0.0-beta.116
2023-11-08 15:42:03 +01:00
Andras Bacsai
9698a051d9 Refactored code for better container management 2023-11-08 15:40:06 +01:00
Andras Bacsai
51423394ba Add deployment logs button to Telegram
notification
2023-11-08 14:37:01 +01:00
Andras Bacsai
8e5e36dd5b Update version numbers to 4.0.0-beta.116 and change docs link 2023-11-08 12:54:13 +01:00
Andras Bacsai
c1dd05dcd8 Merge pull request #1413 from coollabsio/next
Refactored database backup job to handle missing
2023-11-08 12:46:14 +01:00
Andras Bacsai
dd1ce6ee6c Refactor database backup job to simplify code 2023-11-08 12:45:48 +01:00
Andras Bacsai
fe4c6d396c Refactored database backup job to handle missing
POSTGRES_DB environment variable
2023-11-08 12:45:31 +01:00
Andras Bacsai
8db54ec069 Merge pull request #1412 from coollabsio/next
Fix database type check in service show blade file
2023-11-08 12:42:35 +01:00
Andras Bacsai
3abc720926 Fix database type check in service show blade file 2023-11-08 12:42:20 +01:00
Andras Bacsai
64cc0b63f1 Merge pull request #1411 from coollabsio/next
v4.0.0-beta.115
2023-11-08 12:41:25 +01:00
Andras Bacsai
c78068466b Add custom PostgreSQL configuration to
StandalonePostgresql
2023-11-08 12:40:05 +01:00
Andras Bacsai
88e407756d Update version numbers and database URLs 2023-11-08 12:26:57 +01:00
Andras Bacsai
1538116e6e Merge pull request #1410 from coollabsio/next
v4.0.0-beta.114
2023-11-08 11:31:40 +01:00
Andras Bacsai
aba47d58a4 Add customRepository property to
ApplicationDeploymentJob class
Fix weird image names in case of custom git
2023-11-08 11:30:54 +01:00
Andras Bacsai
e7f184dd82 Add conditional check for backups tab in service
show view
2023-11-08 11:07:44 +01:00
Andras Bacsai
2ad8d7812b Refactor database backup job to improve code
readability and maintainability.
2023-11-08 11:05:57 +01:00
Andras Bacsai
8212bb99a1 Update database backup job and version number 2023-11-08 10:47:39 +01:00
Andras Bacsai
5fc382d09d Merge pull request #1406 from coollabsio/next
v4.0.0-beta.113
2023-11-08 10:28:57 +01:00
Andras Bacsai
78a80c46da Add nixpacks environment variables to deployment
job
2023-11-08 10:13:20 +01:00
Andras Bacsai
c365d132af Fix empty public port in database configuration 2023-11-08 09:30:38 +01:00
Andras Bacsai
4dc3db3845 Update versions and fix database replication (init values are changeable) in
CloneProject.php
2023-11-08 09:07:30 +01:00
Andras Bacsai
b9427d2ec1 Merge pull request #1398 from coollabsio/next
v4.0.0-beta.112
2023-11-07 15:01:56 +01:00
Andras Bacsai
332a0b9e04 Remove ANSI colors from console output. 2023-11-07 14:40:58 +01:00
Andras Bacsai
18e98aaf52 Add S3 storage to Livewire components and fix
backup job network issue
2023-11-07 14:09:24 +01:00
Andras Bacsai
a7f9fad627 Add support for Dockerfile target build 2023-11-07 13:49:15 +01:00
Andras Bacsai
b01f6ac414 Fix docker network connection in StartService.php 2023-11-07 13:29:05 +01:00
Andras Bacsai
e1bc2cc406 Fix docker network connection issue in
StartService.php
2023-11-07 13:28:48 +01:00
Andras Bacsai
74830b12f3 Fix Docker network creation command in
StartService.php
2023-11-07 13:28:10 +01:00
Andras Bacsai
56a977c676 update n8n 2023-11-07 12:50:18 +01:00
Andras Bacsai
a0bb5733e6 lol n8n with umami db name 2023-11-07 12:30:37 +01:00
Andras Bacsai
516e10ddf2 feat: service database backups 2023-11-07 12:11:47 +01:00
Andras Bacsai
2976c72e09 fix: ui 2023-11-07 10:18:28 +01:00
Andras Bacsai
7377e9e415 fix: dockercleanupjob should be released back 2023-11-07 09:51:48 +01:00
Andras Bacsai
d77c55148b fix: github source view 2023-11-07 09:47:25 +01:00
Andras Bacsai
ad7aa2eed6 fix: github source view 2023-11-07 09:44:47 +01:00
Andras Bacsai
5cec50efbe update install script 2023-11-06 21:14:32 +01:00
Andras Bacsai
ef8686d4da Merge pull request #1383 from krsilas/fix/check-docker-installation
Check if docker installation was successful
2023-11-06 21:13:29 +01:00
Andras Bacsai
581cc73cd4 Merge pull request #1396 from coollabsio/next
v4.0.0-beta.111
2023-11-06 21:08:16 +01:00
Andras Bacsai
358fbf6b3d cleanup not forced 2023-11-06 21:08:02 +01:00
Andras Bacsai
ca0535c285 update cleanup 2023-11-06 20:58:03 +01:00
Andras Bacsai
9007a645a6 fix: build_image not found 2023-11-06 20:53:51 +01:00
Andras Bacsai
68b1b9774d Merge pull request #1385 from theh2so4/next
[+] Templates: NextCloud and Gitea
2023-11-06 19:13:51 +01:00
Andras Bacsai
b9b4c23d5b update init 2023-11-06 18:15:23 +01:00
Andras Bacsai
149fee2452 fix: deletions 2023-11-06 18:04:18 +01:00
Andras Bacsai
87af9e46a6 fix:ui 2023-11-06 17:27:22 +01:00
Andras Bacsai
d6f87d3fb6 fix: ui for labels 2023-11-06 17:25:54 +01:00
Andras Bacsai
493af61233 fix 2023-11-06 15:51:27 +01:00
Andras Bacsai
ab03908f1d updates 2023-11-06 15:48:15 +01:00
Andras Bacsai
9ef7cf3c12 update service templates 2023-11-06 15:43:56 +01:00
Andras Bacsai
eab7fd44d4 fix: service dockercompose predefined networks
version++
fix: modal of changing service stack
fix: appwrite template
2023-11-06 15:22:11 +01:00
Andras Bacsai
0d1d25a945 Merge pull request #1393 from coollabsio/next
v4.0.0-beta.110
2023-11-06 14:13:38 +01:00
Andras Bacsai
534372c29c fix: env variables
fix: revert custom network for a bit
2023-11-06 14:12:22 +01:00
Andras Bacsai
1ccb239797 version++ 2023-11-06 13:54:00 +01:00
Andras Bacsai
66287b43d0 fix: container logs are now followable in full-screen and sorted by timestamp 2023-11-06 13:53:05 +01:00
Andras Bacsai
143e4e0d23 lol 2023-11-06 13:30:37 +01:00
Andras Bacsai
73f3a09157 oops 2023-11-06 13:29:44 +01:00
Andras Bacsai
5ce449aa08 Merge pull request #1381 from coollabsio/next
v4.0.0-beta.109
2023-11-06 13:16:55 +01:00
Andras Bacsai
6203804713 handle 2023-11-06 13:07:29 +01:00
Andras Bacsai
0858faf628 fix: remove filter 2023-11-06 12:53:43 +01:00
Andras Bacsai
a84f3e0577 fix link 2023-11-06 12:46:58 +01:00
Andras Bacsai
8d571a5eab fix: add nixpacks info 2023-11-06 12:40:53 +01:00
Andras Bacsai
7a117c61c4 fix: separate delete with validation of server 2023-11-06 12:31:02 +01:00
Andras Bacsai
8b034f15fc fix: delete resource if server is not functional
fix: set status to exited on all resources
2023-11-06 11:51:20 +01:00
Andras Bacsai
b4a6499c83 fix: port number should be int 2023-11-06 10:58:00 +01:00
Andras Bacsai
c083acaeef fix: resourcesdelete command 2023-11-06 10:55:46 +01:00
Andras Bacsai
9c6d8320d8 fix: UI 2023-11-06 10:54:11 +01:00
Andras Bacsai
0e7a304610 fix: private key not found error 2023-11-06 10:53:01 +01:00
Andras Bacsai
83993cbbb2 fix: telegram text 2023-11-06 10:49:35 +01:00
Andras Bacsai
6840ddd3e6 fix: no environments 2023-11-06 10:48:30 +01:00
Andras Bacsai
2c6ece62bb fixes 2023-11-06 10:45:06 +01:00
Andras Bacsai
3f8514050e fix: set default from/sender names 2023-11-06 10:26:56 +01:00
Andras Bacsai
a4a653603e fix: missing $mailMessage 2023-11-06 10:23:51 +01:00
Andras Bacsai
b6d8851c99 fix: no id found 2023-11-06 10:22:46 +01:00
Andras Bacsai
bcd7697f50 fix: delete destination 2023-11-06 10:20:13 +01:00
Andras Bacsai
f1da735c40 fix: gh webhook response 200 to installation_repositories 2023-11-06 10:16:21 +01:00
Andras Bacsai
01331c287b fix: notification url in containerstatusjob 2023-11-06 10:10:40 +01:00
Andras Bacsai
3320de787a fix: network service parse 2023-11-06 09:55:22 +01:00
Andras Bacsai
2bddb09384 fix: set labels on generate domain 2023-11-06 09:27:00 +01:00
Andras Bacsai
6f673d7a07 fixes 2023-11-05 09:49:23 +01:00
Andras Bacsai
0a5a101ef4 update github actions 2023-11-03 18:01:17 +01:00
Andras Bacsai
88590fbf0f fix: dockerfile build pack fix 2023-11-03 17:55:53 +01:00
Andras Bacsai
90291b2edf fix: deployments ui 2023-11-03 17:45:30 +01:00
Andras Bacsai
070573f0df fix: local dev repo 2023-11-03 15:11:06 +01:00
Andras Bacsai
e583beb753 fix: invoice.paid should sleep for 5 seconds 2023-11-03 14:51:29 +01:00
Andras Bacsai
d31683df61 update 2023-11-03 14:39:11 +01:00
Andras Bacsai
0a83ed82fa tinkerwell 2023-11-03 14:38:34 +01:00
TheH2SO4
4031e477ee [+] Template: Gitea (PostgreSQL) 2023-11-03 13:55:14 +01:00
TheH2SO4
0c1991d1de [+] Template: Gitea MariaDB + (fix) 2023-11-03 13:40:36 +01:00
TheH2SO4
05b697b18c [+] Template: MySQL + (Fix) 2023-11-03 13:40:07 +01:00
TheH2SO4
061aeba605 [+] Template: Gitea (MariaDB) 2023-11-03 13:38:50 +01:00
TheH2SO4
f446e784cc [+] Template: Gitea (MySQL) 2023-11-03 12:21:15 +01:00
TheH2SO4
72fe24d98e [+] Template: Gitea 2023-11-03 12:04:01 +01:00
Andras Bacsai
0cd3a3d848 fix: increase polling time for services
fix: allow domain as ip address
2023-11-03 10:57:58 +01:00
TheH2SO4
126b2dc65b [+] Template: NextCloud
🆕 **New Template**:

-> ℹ️ **NextCloud**: NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management.
2023-11-03 08:34:24 +01:00
Andras Bacsai
a0031efce0 resale license check needs to be updated 2023-11-02 14:10:29 +01:00
Andras Bacsai
3bffe3f010 fix: missing environment variables prevewi on service 2023-11-02 14:03:02 +01:00
Andras Bacsai
b9a37233a2 disable license check for now 2023-11-02 11:45:43 +01:00
Silas Krause
8ae18f49dc Add missing fi 2023-11-01 22:13:25 +01:00
Silas Krause
4feb99cbe0 Check if docker installation was successful 2023-11-01 21:52:08 +01:00
Andras Bacsai
aab122d97e add cache-key nixpacks 2023-11-01 21:05:24 +01:00
Andras Bacsai
658d608f55 ok, it is not nixpacks problem 2023-11-01 21:02:05 +01:00
Andras Bacsai
0838343841 fix: pull requests
feat: add follow for full screen logs
2023-11-01 20:55:21 +01:00
Andras Bacsai
b557ea1e1d revert nixpacks version 2023-11-01 20:54:50 +01:00
Andras Bacsai
4520070df3 fix: pull requests deployments
feat: filter deployments logs by pull requests
2023-11-01 15:39:47 +01:00
Andras Bacsai
be8ea78b1b feat: deployment logs fullscreen 2023-11-01 14:06:15 +01:00
Andras Bacsai
1175d68ab5 feat: full screen logs
fix: logs are in order now
2023-11-01 13:47:40 +01:00
Andras Bacsai
f56d373ed2 update nixpacks 2023-11-01 12:54:49 +01:00
Andras Bacsai
c6253658ca feat: restart application
fix: a few things in application deployment job
2023-11-01 12:19:08 +01:00
Andras Bacsai
4249aec936 Merge branch 'main' into next 2023-11-01 10:56:03 +01:00
Andras Bacsai
25f80aba5f Merge pull request #1376 from mauvehed/fix/basedir-permissions 2023-10-31 09:18:22 +01:00
Andras Bacsai
4550983761 Update install.sh 2023-10-31 08:13:50 +01:00
Andras Bacsai
b0238372a2 Update install.sh
Do not change permission on /data
2023-10-31 08:13:06 +01:00
mauvehed
a021b71496 fix(install.sh): change ownership and permissions only for /data/coolify directory instead of /data
The ownership and permissions are now set only for the /data/coolify directory instead of the entire /data directory. This ensures that the ownership and permissions are applied only to the necessary directory and not to other directories within /data.
2023-10-29 10:05:16 -05:00
Andras Bacsai
e3958d9626 added a few services 2023-10-27 14:22:35 +02:00
Andras Bacsai
55891d7001 Merge pull request #1367 from itishermann/main
[+] Template: Kuzzle, Moodle, Sonarqube, RabbitMQ
2023-10-27 13:16:52 +02:00
Andras Bacsai
b12ac8bb29 Merge pull request #1364 from theh2so4/main
[+] Template: BudgE, Duplicati, Jellyfin, phpMyAdmin, Vaultwarden, Whoogle and FileBrowser
2023-10-27 12:57:00 +02:00
TheH2SO4
728a9f88eb [!] Template: FileBrowser 2023-10-27 12:23:16 +02:00
TheH2SO4
57267c3ee0 [+] Template: FileBrowser
🆕 **New Template**:

-> ℹ️ **FileBrowser**: FileBrowser simplifies file and folder management on various storage systems.
2023-10-27 12:21:14 +02:00
Andras Bacsai
d3d133ed1f version++ 2023-10-27 12:07:48 +02:00
Andras Bacsai
f5240abbe5 Merge pull request #1371 from coollabsio/next
v4.0.0-beta.108
2023-10-27 11:44:35 +02:00
Andras Bacsai
abf5840f97 fixing 2023-10-27 11:44:10 +02:00
Andras Bacsai
dc6d5af4aa Merge pull request #1370 from coollabsio/next
v4.0.0-beta.107
2023-10-27 11:24:03 +02:00
Andras Bacsai
0b88cd69f2 fix: remove coolify labels from ui 2023-10-27 11:23:29 +02:00
Andras Bacsai
ce165719d6 Merge pull request #1369 from coollabsio/next
v4.0.0-beta.106
2023-10-27 10:43:54 +02:00
Andras Bacsai
4f543ce20f remove ray 2023-10-27 10:43:05 +02:00
Andras Bacsai
55f957df21 fix: git ls-remote 2023-10-27 10:42:56 +02:00
Andras Bacsai
38f59b9410 revert 2023-10-27 10:30:15 +02:00
Andras Bacsai
ebe6655349 update invoice paid 2023-10-27 10:28:43 +02:00
Andras Bacsai
038ea08ca7 add payment_intent.payment_failed to subs 2023-10-27 10:26:35 +02:00
Andras Bacsai
ba424efd39 cloud: fix subs 2023-10-27 10:17:13 +02:00
Andras Bacsai
75aef0e60b Merge pull request #1366 from coollabsio/next
v4.0.0-beta.105
2023-10-27 09:31:06 +02:00
Andras Bacsai
eda8b34297 fix 2023-10-27 09:28:43 +02:00
Andras Bacsai
d8151ddb2e fix: add ssh options to git ls-remote 2023-10-27 09:25:15 +02:00
Hermann Kao
7925228f97 add service template for sonarqube 2023-10-27 01:41:07 +02:00
Hermann Kao
a7dc62aaa0 add service template for rabbitmq 2023-10-27 00:42:15 +02:00
Hermann Kao
632dbd155b add service template for moodle based on bitnami images 2023-10-27 00:18:06 +02:00
Hermann Kao
fe092bb7a5 add service template for kuzzle 2023-10-26 23:29:59 +02:00
Andras Bacsai
928345c8ea fix: force password reset on invited accounts 2023-10-26 20:45:38 +02:00
Andras Bacsai
52d6fb51d5 pocketbase 2023-10-26 15:53:42 +02:00
Andras Bacsai
06d7c69487 add nocodb 2023-10-26 13:32:23 +02:00
Andras Bacsai
756c7f81ca fix: if user is invited, that means its email is verified 2023-10-26 13:00:40 +02:00
Andras Bacsai
d7af57a95e fix: custom labels only should have non-coolify labels
fix: pull helper image every 10 minutes instead of every deployment
2023-10-26 11:38:37 +02:00
TheH2SO4
722ff15fbd [+] Template: Vaultwarden
🆕 **New Template**:

-> ℹ️ **Vaultwarden**: Vaultwarden is an open-source password manager that allows you to securely store and manage your passwords, helping you stay organized and protected.
2023-10-26 11:21:31 +02:00
Andras Bacsai
f9c469497e version++ 2023-10-26 11:15:37 +02:00
Andras Bacsai
7ecbedb48a Merge pull request #1365 from coollabsio/next
v4.0.0-beta.104
2023-10-26 11:11:59 +02:00
Andras Bacsai
76878f66b9 remove ray 2023-10-26 11:07:25 +02:00
Andras Bacsai
b9afef50c4 version++ 2023-10-26 10:35:14 +02:00
Andras Bacsai
83ebd1e649 feat: improve deployment time by a lot 2023-10-26 10:33:57 +02:00
Andras Bacsai
76431c3fd5 service updates 2023-10-26 10:02:51 +02:00
Andras Bacsai
96a4d0bbb0 fix: lock SERVICE_FQDN envs 2023-10-26 10:02:45 +02:00
Andras Bacsai
4cfc739730 add openblocks 2023-10-26 09:34:02 +02:00
TheH2SO4
fcd0d8d359 [+] Template: BudgE
🆕 **New Template**:

-> ℹ️ **BudgE**: Budge is an open-source 'budgeting with envelopes' personal finance app, helping you manage your finances effectively.
2023-10-25 22:22:20 +02:00
TheH2SO4
3fcac0ac35 [+] Template: phpMyAdmin
🆕 **New Template**:

-> ℹ️ **phpMyAdmin**: phpMyAdmin is a web-based database management tool for administering your MySQL and MariaDB databases through a user-friendly interface.
2023-10-25 21:57:03 +02:00
TheH2SO4
fcc8a7f0ed [!] Template Fix: Whoogle
🐛 **Bug Fix**:

ℹ️ **Whoogle**: Tags section was not added, it's now fixed.
2023-10-25 21:50:55 +02:00
TheH2SO4
6950ead041 [+] Template: Jellyfin
🆕 **New Template**:

-> ℹ️ **Jellyfin**: Jellyfin is an open-source media server for hosting and streaming your media collection, providing an alternative to proprietary media platforms.
2023-10-25 21:46:52 +02:00
TheH2SO4
f78c49fc82 [+] Template: Whoogle
🆕 **New Template**:

-> ℹ️ **Whoogle**: Whoogle is a self-hosted, privacy-focused search engine front-end for accessing Google search results without tracking and data collection.
2023-10-25 21:40:38 +02:00
TheH2SO4
5f2581020b [+] Template: Duplicati
🆕 **New Template**:

-> ℹ️ **Duplicati**: Duplicati is an open-source backup solution, allowing you to safeguard your data with ease through scheduled backups and encryption.
2023-10-25 21:36:57 +02:00
TheH2SO4
2fb674ae85 Merge pull request #4 from coollabsio/main
Update
2023-10-25 21:27:42 +02:00
Andras Bacsai
a95bd906bc Merge pull request #1363 from coollabsio/next
v4.0.0-beta.103
2023-10-25 20:21:27 +02:00
Andras Bacsai
21795cf788 fix: space in build args 2023-10-25 20:19:38 +02:00
Andras Bacsai
6e98fd9403 grafana + openblocks 2023-10-25 20:13:45 +02:00
Andras Bacsai
ead1edc2b9 Services 2023-10-25 15:44:34 +02:00
Andras Bacsai
db822cb876 Merge pull request #1357 from theh2so4/main
[+] Templates: Dashboard, Emby, EmbyStat and Grocy
2023-10-25 15:41:54 +02:00
Andras Bacsai
65bfce43c0 fix: server settings guarded 2023-10-25 11:50:22 +02:00
Andras Bacsai
50fc05ab52 update init script 2023-10-25 11:43:18 +02:00
Andras Bacsai
c9cf5c486f Merge pull request #1362 from coollabsio/next
v4.0.0-beta.102
2023-10-25 11:07:17 +02:00
Andras Bacsai
379f4b9dff feat: show webhook on ui
feat: n8n service
2023-10-25 10:43:07 +02:00
Andras Bacsai
aa02b8d433 fix: rate limit for api + add mariadb + mysql 2023-10-25 09:56:58 +02:00
Andras Bacsai
70ecb92e82 cleanup ssh dir on start 2023-10-25 09:41:41 +02:00
Andras Bacsai
d5cc2a2eed feat: download local backups 2023-10-25 09:28:26 +02:00
Andras Bacsai
2b91bd24c5 Merge pull request #1361 from coollabsio/next
v4.0.0-beta.101
2023-10-24 15:51:54 +02:00
Andras Bacsai
5e8ac1b48e fix: mongodb healtcheck command 2023-10-24 15:47:29 +02:00
Andras Bacsai
dc86170ef5 version++ 2023-10-24 15:41:44 +02:00
Andras Bacsai
0232cf5b4c feat: lock environment variables 2023-10-24 15:41:21 +02:00
Andras Bacsai
6e73f7f2e4 fix: encrypt mongodb password 2023-10-24 15:40:29 +02:00
TheH2SO4
6c5a1c317a [!] Mistake 2023-10-23 13:14:43 +02:00
TheH2SO4
b09a9f871e [+] Template: Fenrus
🆕 **New Template**:

-> ℹ️ **Fenrus**: A personal home page for quick access to all your personal apps/sites.
2023-10-23 13:12:50 +02:00
TheH2SO4
b5506f006b [+] Template: Dashboard
🆕 **New Template**:

-> ℹ️ **Dashboard**: A dashboard. Inspired by SUI, it offers simple customization through JSON-files and a handy search bar to help you browse the internet more efficiently.
2023-10-23 13:05:22 +02:00
TheH2SO4
a6c3594448 [+] Template: Grocy
🆕 **New Template**:

-> ℹ️ **Grocy**: Grocy is a self-hosted, web-based household management and grocery list application, designed to simplify your household chores and grocery shopping.
2023-10-23 12:48:13 +02:00
TheH2SO4
5dd3952230 [+] Template: EmbyStat
🆕 **New Template**:

-> ℹ️ **EmbyStat**: EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployement, all within your control.
2023-10-23 12:41:48 +02:00
TheH2SO4
22ec0f8826 [+] Template: Emby
🆕 **New Template**:

-> ℹ️ **Emby**: A media server software that allows you to organize, stream, and access your multimedia content effortlessly, making it easy to enjoy your favorite movies, TV shows, music, and more.
2023-10-23 11:30:01 +02:00
TheH2SO4
da6e04bb1a Merge pull request #3 from coollabsio/main
Update
2023-10-21 22:57:27 +02:00
TheH2SO4
1bfce6716c Merge pull request #2 from coollabsio/next
Next
2023-10-19 11:18:19 +02:00
206 changed files with 4647 additions and 1482 deletions

View File

@@ -13,7 +13,7 @@ env:
jobs:
amd64:
runs-on: [self-hosted, x64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io

View File

@@ -10,7 +10,7 @@ env:
jobs:
amd64:
runs-on: [self-hosted, x64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io

View File

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

View File

@@ -0,0 +1,28 @@
<?php
/**
* @label Send Email
* @description Send email to all users
*/
use App\Models\User;
use Illuminate\Support\Facades\Mail;
set_transanctional_email_settings();
$users = User::whereEmail('test@example.com');
foreach ($users as $user) {
Mail::send([], [], function ($message) use ($user) {
$message
->to($user->email)
->subject("Testing")
->text(
<<<EOF
Hello,
Welcome to Coolify Cloud.
Here is your user id: $user->id
EOF
);
});
}

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,7 @@ class StartMongodb
'healthcheck' => [
'test' => [
'CMD-SHELL',
'mongo --eval "printjson(db.serverStatus())" | grep uptime | grep -v grep'
'mongosh --eval "printjson(db.runCommand(\"ping\"))"'
],
'interval' => '5s',
'timeout' => '5s',
@@ -63,7 +63,7 @@ class StartMongodb
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
@@ -107,6 +107,8 @@ class StartMongodb
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server);

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,26 +4,26 @@ namespace App\Actions\License;
use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
class CheckResaleLicense
{
public function __invoke()
use AsAction;
public function handle()
{
try {
$settings = InstanceSettings::get();
$settings->update([
'is_resale_license_active' => false,
]);
if (isDev()) {
$settings->update([
'is_resale_license_active' => true,
]);
return;
}
if (!$settings->resale_license) {
return;
}
// if (!$settings->resale_license) {
// return;
// }
$base_url = config('coolify.license_url');
if (isDev()) {
$base_url = 'http://host.docker.internal:8787';
}
$instance_id = config('app.id');
ray("Checking license key against $base_url/lemon/validate");

View File

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

View File

@@ -4,7 +4,6 @@ namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service;
use App\Notifications\Application\StatusChanged;
class StopService
{

View File

@@ -6,23 +6,47 @@ use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class Init extends Command
{
protected $signature = 'app:init';
protected $signature = 'app:init {--cleanup}';
protected $description = 'Cleanup instance related stuffs';
public function handle()
{
ray()->clearAll();
$cleanup = $this->option('cleanup');
if ($cleanup) {
$this->cleanup_stucked_resources();
$this->cleanup_ssh();
}
$this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_resources();
}
private function cleanup_ssh()
{
try {
$files = Storage::allFiles('ssh/keys');
foreach ($files as $file) {
Storage::delete($file);
}
$files = Storage::allFiles('ssh/mux');
foreach ($files as $file) {
Storage::delete($file);
}
} catch (\Throwable $e) {
echo "Error in cleaning ssh: {$e->getMessage()}\n";
}
}
private function cleanup_in_progress_application_deployments()
{
// Cleanup any failed deployments
@@ -37,60 +61,135 @@ class Init extends Command
echo "Error: {$e->getMessage()}\n";
}
}
private function cleanup_stucked_resources() {
private function cleanup_stucked_resources()
{
// Cleanup any resources that are not attached to any environment or destination or server
try {
$applications = Application::all();
foreach($applications as $application) {
if (!$application->environment) {
foreach ($applications as $application) {
if (!data_get($application, 'environment')) {
ray('Application without environment', $application->name);
$application->delete();
}
if (!data_get($application, 'destination.server')) {
ray('Application without server', $application->name);
$application->delete();
}
if (!$application->destination()) {
ray('Application without destination', $application->name);
$application->delete();
}
}
} catch (\Throwable $e) {
echo "Error in application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::all();
foreach($postgresqls as $postgresql) {
if (!$postgresql->environment) {
foreach ($postgresqls as $postgresql) {
if (!data_get($postgresql, 'environment')) {
ray('Postgresql without environment', $postgresql->name);
$postgresql->delete();
}
if (!data_get($postgresql, 'destination.server')) {
ray('Postgresql without server', $postgresql->name);
$postgresql->delete();
}
if (!$postgresql->destination()) {
ray('Postgresql without destination', $postgresql->name);
$postgresql->delete();
}
}
} catch (\Throwable $e) {
echo "Error in postgresql: {$e->getMessage()}\n";
}
try {
$redis = StandaloneRedis::all();
foreach($redis as $redis) {
if (!$redis->environment) {
foreach ($redis as $redis) {
if (!data_get($redis, 'environment')) {
ray('Redis without environment', $redis->name);
$redis->delete();
}
if (!data_get($redis, 'destination.server')) {
ray('Redis without server', $redis->name);
$redis->delete();
}
if (!$redis->destination()) {
ray('Redis without destination', $redis->name);
$redis->delete();
}
}
} catch (\Throwable $e) {
echo "Error in redis: {$e->getMessage()}\n";
}
try {
$mongodbs = StandaloneMongodb::all();
foreach($mongodbs as $mongodb) {
if (!$mongodb->environment) {
foreach ($mongodbs as $mongodb) {
if (!data_get($mongodb, 'environment')) {
ray('Mongodb without environment', $mongodb->name);
$mongodb->delete();
}
if (!data_get($mongodb, 'destination.server')) {
ray('Mongodb without server', $mongodb->name);
$mongodb->delete();
}
if (!$mongodb->destination()) {
ray('Mongodb without destination', $mongodb->name);
$mongodb->delete();
}
}
} catch (\Throwable $e) {
echo "Error in mongodb: {$e->getMessage()}\n";
}
try {
$mysqls = StandaloneMysql::all();
foreach ($mysqls as $mysql) {
if (!data_get($mysql, 'environment')) {
ray('Mysql without environment', $mysql->name);
$mysql->delete();
}
if (!data_get($mysql, 'destination.server')) {
ray('Mysql without server', $mysql->name);
$mysql->delete();
}
if (!$mysql->destination()) {
ray('Mysql without destination', $mysql->name);
$mysql->delete();
}
}
} catch (\Throwable $e) {
echo "Error in mysql: {$e->getMessage()}\n";
}
try {
$mariadbs = StandaloneMariadb::all();
foreach ($mariadbs as $mariadb) {
if (!data_get($mariadb, 'environment')) {
ray('Mariadb without environment', $mariadb->name);
$mariadb->delete();
}
if (!data_get($mariadb, 'destination.server')) {
ray('Mariadb without server', $mariadb->name);
$mariadb->delete();
}
if (!$mariadb->destination()) {
ray('Mariadb without destination', $mariadb->name);
$mariadb->delete();
}
}
} catch (\Throwable $e) {
echo "Error in mariadb: {$e->getMessage()}\n";
}
try {
$services = Service::all();
foreach($services as $service) {
if (!$service->environment) {
foreach ($services as $service) {
if (!data_get($service, 'environment')) {
ray('Service without environment', $service->name);
$service->delete();
}
if (!$service->server) {
if (!data_get($service, 'server')) {
ray('Service without server', $service->name);
$service->delete();
}
@@ -100,7 +199,29 @@ class Init extends Command
}
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
echo "Error in service: {$e->getMessage()}\n";
}
try {
$serviceApplications = ServiceApplication::all();
foreach ($serviceApplications as $service) {
if (!data_get($service, 'service')) {
ray('ServiceApplication without service', $service->name);
$service->delete();
}
}
} catch (\Throwable $e) {
echo "Error in serviceApplications: {$e->getMessage()}\n";
}
try {
$serviceDatabases = ServiceDatabase::all();
foreach ($serviceDatabases as $service) {
if (!data_get($service, 'service')) {
ray('ServiceDatabase without service', $service->name);
$service->delete();
}
}
} catch (\Throwable $e) {
echo "Error in ServiceDatabases: {$e->getMessage()}\n";
}
}
}

View File

@@ -61,12 +61,14 @@ class ResourcesDelete extends Command
foreach ($serversToDelete as $server) {
$toDelete = $servers->where('id', $server)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
break;
if ($toDelete) {
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
break;
}
$toDelete->delete();
}
$toDelete->delete();
}
}
private function deleteApplication()
@@ -82,14 +84,15 @@ class ResourcesDelete extends Command
);
foreach ($applicationsToDelete as $application) {
ray($application);
$toDelete = $applications->where('id', $application)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources? ");
if (!$confirmed) {
break;
if ($toDelete) {
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources? ");
if (!$confirmed) {
break;
}
$toDelete->delete();
}
$toDelete->delete();
}
}
private function deleteDatabase()
@@ -106,12 +109,14 @@ class ResourcesDelete extends Command
foreach ($databasesToDelete as $database) {
$toDelete = $databases->where('id', $database)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
if ($toDelete) {
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
$toDelete->delete();
}
$toDelete->delete();
}
}
private function deleteService()
@@ -128,12 +133,14 @@ class ResourcesDelete extends Command
foreach ($servicesToDelete as $service) {
$toDelete = $services->where('id', $service)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
if ($toDelete) {
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
$toDelete->delete();
}
$toDelete->delete();
}
}
}

View File

@@ -8,6 +8,7 @@ use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ContainerStatusJob;
use App\Jobs\PullHelperImageJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
@@ -19,20 +20,35 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
// $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
// Server Jobs
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->cleanup_servers($schedule);
$this->check_scheduled_backups($schedule);
$this->pull_helper_image($schedule);
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
// $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
// Server Jobs
$this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->cleanup_servers($schedule);
$this->pull_helper_image($schedule);
}
}
private function pull_helper_image($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
foreach ($servers as $server) {
$schedule->job(new PullHelperImageJob($server))->everyTenMinutes()->onOneServer();
}
}
private function cleanup_servers($schedule)

View File

@@ -41,7 +41,7 @@ class ApplicationController extends Controller
if (!$application) {
return redirect()->route('dashboard');
}
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 8);
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 40);
return view('project.application.deployments', ['application' => $application, 'deployments' => $deployments, 'deployments_count' => $count]);
}

View File

@@ -39,6 +39,10 @@ class Controller extends BaseController
} else {
$team = $user->teams()->first();
}
if (is_null(data_get($user, 'email_verified_at'))){
$user->email_verified_at = now();
$user->save();
}
Auth::login($user);
session(['currentTeam' => $team]);
return redirect()->route('dashboard');

View File

@@ -32,8 +32,14 @@ class MagicController extends Controller
public function environments()
{
$project = Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first();
if (!$project) {
return response()->json([
'environments' => []
]);
}
return response()->json([
'environments' => Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first()->environments
'environments' => $project->environments
]);
}

View File

@@ -81,7 +81,9 @@ class ProjectController extends Controller
$oneClickService = data_get($services, "$oneClickServiceName.compose");
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/');
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
return !empty($value);
});
}
if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();

View File

@@ -164,7 +164,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{
$this->validate([
'remoteServerName' => 'required',
'remoteServerHost' => 'required|ip',
'remoteServerHost' => 'required',
'remoteServerPort' => 'required|integer',
'remoteServerUser' => 'required',
]);

View File

@@ -32,7 +32,7 @@ class CheckLicense extends Component
$this->settings->save();
if ($this->settings->resale_license) {
try {
resolve(CheckResaleLicense::class)();
CheckResaleLicense::run();
$this->emit('reloadWindow');
} catch (\Throwable $e) {
session()->flash('error', 'Something went wrong. Please contact support. <br>Error: ' . $e->getMessage());

View File

@@ -16,7 +16,7 @@ class Form extends Component
protected $validationAttributes = [
'destination.name' => 'name',
'destination.network' => 'network',
'destination.server.ip' => 'IP Address',
'destination.server.ip' => 'IP Address/Domain',
];
public function submit()

View File

@@ -3,24 +3,31 @@
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use Illuminate\Support\Collection;
use Livewire\Component;
class Deployments extends Component
{
public Application $application;
public $deployments = [];
public Array|Collection $deployments = [];
public int $deployments_count = 0;
public string $current_url;
public int $skip = 0;
public int $default_take = 8;
public int $default_take = 40;
public bool $show_next = false;
public ?string $pull_request_id = null;
protected $queryString = ['pull_request_id'];
public function mount()
{
$this->current_url = url()->current();
$this->show_pull_request_only();
$this->show_more();
}
private function show_pull_request_only() {
if ($this->pull_request_id) {
$this->deployments = $this->deployments->where('pull_request_id', $this->pull_request_id);
}
}
private function show_more()
{
if (count($this->deployments) !== 0) {
@@ -47,6 +54,7 @@ class Deployments extends Component
['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $take);
$this->deployments = $deployments;
$this->deployments_count = $count;
$this->show_pull_request_only();
$this->show_more();
}
}

View File

@@ -55,6 +55,7 @@ class General extends Component
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -77,6 +78,7 @@ class General extends Component
'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location',
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
];
public function mount()
@@ -152,7 +154,7 @@ class General extends Component
$fqdn = generateFqdn($server, $this->application->uuid);
$this->application->fqdn = $fqdn;
$this->application->save();
$this->emit('success', 'Application settings updated!');
$this->updatedApplicationFqdn();
}
}
public function resetDefaultLabels($showToaster = true)

View File

@@ -65,4 +65,18 @@ class Heading extends Component
$this->application->save();
$this->application->refresh();
}
public function restart() {
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);
return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
]);
}
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Project\Database;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
class BackupExecutions extends Component
@@ -18,11 +19,42 @@ class BackupExecutions extends Component
$this->emit('error', 'Backup execution not found.');
return;
}
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
$execution->delete();
$this->emit('success', 'Backup deleted successfully.');
$this->emit('refreshBackupExecutions');
}
public function download($exeuctionId)
{
try {
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
if (is_null($execution)) {
$this->emit('error', 'Backup execution not found.');
return;
}
$filename = data_get($execution, 'filename');
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = $execution->scheduledDatabaseBackup->database->service->destination->server;
} else {
$server = $execution->scheduledDatabaseBackup->database->destination->server;
}
$privateKeyLocation = savePrivateKeyToFs($server);
$disk = Storage::build([
'driver' => 'sftp',
'host' => $server->ip,
'port' => $server->port,
'username' => $server->user,
'privateKey' => $privateKeyLocation,
]);
return $disk->download($filename);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function refreshBackupExecutions(): void
{
$this->executions = $this->backup->executions;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,6 @@ use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
use Spatie\Url\Url;
class GithubPrivateRepository extends Component
{

View File

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

View File

@@ -13,7 +13,7 @@ class Index extends Component
public $databases;
public array $parameters;
public array $query;
protected $listeners = ["refreshStacks","checkStatus"];
protected $listeners = ["refreshStacks", "checkStatus"];
public function render()
{
return view('livewire.project.service.index');

View File

@@ -4,7 +4,6 @@ namespace App\Http\Livewire\Project\Service;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Jobs\ContainerStatusJob;
use App\Models\Service;
use Livewire\Component;
@@ -13,15 +12,14 @@ class Navbar extends Component
public Service $service;
public array $parameters;
public array $query;
protected $listeners = ["checkStatus"];
public function render()
{
return view('livewire.project.service.navbar');
}
public function checkStatus()
{
$this->emit('checkStatus');
public function checkStatus() {
$this->service->refresh();
}
public function deploy()
{
@@ -29,11 +27,15 @@ class Navbar extends Component
$activity = StartService::run($this->service);
$this->emit('newMonitorActivity', $activity->id);
}
public function stop()
public function stop(bool $forceCleanup = false)
{
StopService::run($this->service);
$this->service->refresh();
$this->emit('success', 'Service stopped successfully.');
$this->checkStatus();
if ($forceCleanup) {
$this->emit('success', 'Force cleanup service successfully.');
} else {
$this->emit('success', 'Service stopped successfully.');
}
$this->emit('checkStatus');
}
}

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Project\Shared;
use App\Jobs\StopResourceJob;
use App\Jobs\DeleteResourceJob;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -21,7 +21,7 @@ class Danger extends Component
public function delete()
{
try {
StopResourceJob::dispatchSync($this->resource);
DeleteResourceJob::dispatchSync($this->resource);
return redirect()->route('project.resources', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name']

View File

@@ -31,11 +31,17 @@ class All extends Component
public function getDevView()
{
$this->variables = $this->resource->environment_variables->map(function ($item) {
if ($item->is_shown_once) {
return "$item->key=(locked secret)";
}
return "$item->key=$item->value";
})->sort()->join('
');
if ($this->showPreview) {
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
if ($item->is_shown_once) {
return "$item->key=(locked secret)";
}
return "$item->key=$item->value";
})->sort()->join('
');
@@ -49,16 +55,21 @@ class All extends Component
{
if ($isPreview) {
$variables = parseEnvFormatToArray($this->variablesPreview);
$existingVariables = $this->resource->environment_variables_preview();
$this->resource->environment_variables_preview()->delete();
$this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
$existingVariables = $this->resource->environment_variables();
$this->resource->environment_variables()->delete();
$this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
}
foreach ($variables as $key => $variable) {
$found = $existingVariables->where('key', $key)->first();
if ($isPreview) {
$found = $this->resource->environment_variables_preview()->where('key', $key)->first();
} else {
$found = $this->resource->environment_variables()->where('key', $key)->first();
}
if ($found) {
if ($found->is_shown_once) {
continue;
}
$found->value = $variable;
$found->save();
continue;

View File

@@ -5,7 +5,6 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class Show extends Component
{
@@ -13,29 +12,45 @@ class Show extends Component
public ModelsEnvironmentVariable $env;
public ?string $modalId = null;
public bool $isDisabled = false;
public bool $isLocked = false;
public string $type;
protected $rules = [
'env.key' => 'required|string',
'env.value' => 'nullable',
'env.is_build_time' => 'required|boolean',
'env.is_shown_once' => 'required|boolean',
];
protected $validationAttributes = [
'key' => 'key',
'value' => 'value',
'is_build_time' => 'build',
'key' => 'Key',
'value' => 'Value',
'is_build_time' => 'Build Time',
'is_shown_once' => 'Shown Once',
];
public function mount()
{
$this->isDisabled = false;
if (Str::of($this->env->key)->startsWith('SERVICE_FQDN') || Str::of($this->env->key)->startsWith('SERVICE_URL')) {
$this->isDisabled = true;
}
$this->modalId = new Cuid2(7);
$this->parameters = get_route_parameters();
$this->checkEnvs();
}
public function checkEnvs()
{
$this->isDisabled = false;
if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL')) {
$this->isDisabled = true;
}
if ($this->env->is_shown_once) {
$this->isLocked = true;
}
}
public function lock()
{
$this->env->is_shown_once = true;
$this->env->save();
$this->checkEnvs();
$this->emit('refreshEnvs');
}
public function instantSave()
{
$this->submit();

View File

@@ -17,7 +17,7 @@ class GetLogs extends Component
public int $numberOfLines = 100;
public function doSomethingWithThisChunkOfOutput($output)
{
$this->outputs .= $output;
$this->outputs .= removeAnsiColors($output);
}
public function instantSave()
{
@@ -36,6 +36,13 @@ class GetLogs extends Component
Process::run($sshCommand, function (string $type, string $output) {
$this->doSomethingWithThisChunkOfOutput($output);
});
if ($this->showTimeStamps) {
$this->outputs = str($this->outputs)->split('/\n/')->sort(function ($a, $b) {
$a = explode(' ', $a);
$b = explode(' ', $b);
return $a[0] <=> $b[0];
})->join("\n");
}
}
}
public function render()

View File

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

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use Livewire\Component;
class Webhooks extends Component
{
public $resource;
public ?string $deploywebhook = null;
public function mount()
{
$this->deploywebhook = generateDeployWebhook($this->resource);
}
public function render()
{
return view('livewire.project.shared.webhooks');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Livewire\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Delete extends Component
{
use AuthorizesRequests;
public $server;
public function delete()
{
try {
$this->authorize('delete', $this->server);
if ($this->server->hasDefinedResources()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
}
$this->server->delete();
return redirect()->route('server.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.delete');
}
}

View File

@@ -4,12 +4,10 @@ namespace App\Http\Livewire\Server;
use App\Actions\Server\InstallDocker;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Form extends Component
{
use AuthorizesRequests;
public Server $server;
public bool $isValidConnection = false;
public bool $isValidDocker = false;
@@ -32,7 +30,7 @@ class Form extends Component
protected $validationAttributes = [
'server.name' => 'Name',
'server.description' => 'Description',
'server.ip' => 'IP address',
'server.ip' => 'IP address/Domain',
'server.user' => 'User',
'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
@@ -106,26 +104,12 @@ class Form extends Component
}
}
public function delete()
{
try {
$this->authorize('delete', $this->server);
if (!$this->server->isEmpty()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
}
$this->server->delete();
return redirect()->route('server.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
if(isCloud() && !isDev()) {
if (isCloud() && !isDev()) {
$this->validate();
$this->validate([
'server.ip' => 'required|ip',
'server.ip' => 'required',
]);
} else {
$this->validate();

View File

@@ -26,14 +26,14 @@ class ByIp extends Component
protected $rules = [
'name' => 'required|string',
'description' => 'nullable|string',
'ip' => 'required|ip',
'ip' => 'required',
'user' => 'required|string',
'port' => 'required|integer',
];
protected $validationAttributes = [
'name' => 'Name',
'description' => 'Description',
'ip' => 'IP Address',
'ip' => 'IP Address/Domain',
'user' => 'User',
'port' => 'Port',
];

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ class DecideWhatToDoWithUser
public function handle(Request $request, Closure $next): Response
{
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 $next($request);

View File

@@ -44,6 +44,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private int $pull_request_id;
private string $commit;
private bool $force_rebuild;
private bool $restart_only;
private ?string $dockerImage = null;
private ?string $dockerImageTag = null;
@@ -51,7 +52,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination;
private Server $server;
private ApplicationPreview|null $preview = null;
private ?ApplicationPreview $preview = null;
private string $container_name;
private ?string $currently_running_container_name = null;
@@ -68,6 +69,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile';
private ?string $addHosts = null;
private ?string $buildTarget = null;
private $log_model;
private Collection $saved_outputs;
@@ -76,6 +78,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private string $dockerConfigFileExists = 'NOK';
private int $customPort = 22;
private ?string $customRepository = null;
private ?string $fullRepoUrl = null;
private ?string $branch = null;
public $tries = 1;
public function __construct(int $application_deployment_queue_id)
@@ -91,6 +97,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
$this->restart_only = $this->application_deployment_queue->restart_only;
$source = data_get($this->application, 'source');
if ($source) {
@@ -112,11 +119,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
if ($this->application->fqdn) {
if (data_get($this->preview, 'fqdn')) {
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
if (str($this->application->fqdn)->contains(',')) {
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
} else {
$url = Url::fromString($this->application->fqdn);
if (data_get($this->preview, 'fqdn')) {
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
}
}
$template = $this->application->preview_url_template;
$url = Url::fromString($this->application->fqdn);
$host = $url->getHost();
$schema = $url->getScheme();
$random = new Cuid2(7);
@@ -132,14 +144,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void
{
// ray()->measure();
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id);
if ($containers->count() > 0) {
$this->currently_running_container_name = data_get($containers[0], 'Names');
}
if ($this->pull_request_id !== 0 && $this->pull_request_id !== null) {
$this->currently_running_container_name = $this->container_name;
}
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
@@ -166,6 +170,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return "--add-host $name:$ip";
})->implode(' ');
if ($this->application->dockerfile_target_build) {
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
}
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
@@ -176,15 +184,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->customPort = $matches[0];
$gitHost = str($this->application->git_repository)->before(':');
$gitRepo = str($this->application->git_repository)->after('/');
$this->application->git_repository = "$gitHost:$gitRepo";
$this->customRepository = "$gitHost:$gitRepo";
} else {
$this->customRepository = $this->application->git_repository;
}
try {
if ($this->application->dockerfile) {
if ($this->restart_only) {
$this->just_restart();
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile_buildpack();
} else if ($this->application->build_pack === 'static') {
$this->deploy_static_buildpack();
} else {
if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
@@ -220,6 +234,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
"hidden" => true,
"ignore_errors" => true,
]
);
$this->execute_remote_command(
[
"docker image prune -f >/dev/null 2>&1",
"hidden" => true,
"ignore_errors" => true,
]
);
}
@@ -239,7 +261,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml")
// ],
// );
// $this->build_image_name = Str::lower("{$this->application->git_repository}:build");
// $this->build_image_name = Str::lower("{$this->customRepository}:build");
// $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
// $this->save_environment_variables();
// $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
@@ -261,6 +283,48 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
// );
// }
private function generate_image_names()
{
if ($this->application->dockerfile) {
$this->build_image_name = Str::lower("{$this->application->uuid}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
} else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
} else if ($this->pull_request_id !== 0) {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
} else {
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
}
$this->build_image_name = Str::lower("{$this->application->uuid}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
}
}
private function just_restart()
{
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->generate_compose_file();
$this->rolling_update();
return;
}
$this->execute_remote_command([
"echo 'Cannot find image {$this->production_image_name} locally. Please redeploy the application.'",
]);
}
private function save_environment_variables()
{
$envs = collect([]);
@@ -288,9 +352,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile")
],
);
$this->build_image_name = Str::lower("{$this->application->git_repository}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->generate_image_names();
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
@@ -308,7 +370,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"
],
);
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
$this->generate_image_names();
$this->prepare_builder_image();
$this->generate_compose_file();
$this->rolling_update();
@@ -321,20 +383,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'"
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();
$this->set_base_dir();
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
}
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->generate_image_names();
$this->cleanup_git();
$this->generate_compose_file();
$this->generate_build_env_variables();
@@ -346,21 +402,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'"
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->clone_repository();
$this->check_git_if_build_needed();
$this->set_base_dir();
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
}
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->generate_image_names();
if (!$this->force_rebuild) {
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
@@ -379,6 +427,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]);
}
}
$this->clone_repository();
$this->cleanup_git();
$this->generate_nixpacks_confs();
$this->generate_compose_file();
@@ -387,12 +436,29 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_image();
$this->rolling_update();
}
private function deploy_static_buildpack()
{
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->clone_repository();
$this->cleanup_git();
$this->build_image();
$this->generate_compose_file();
$this->rolling_update();
}
private function rolling_update()
{
if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command(
["echo -n 'Application has ports mapped to the host system, rolling update is not supported. Stopping current container.'"],
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
);
$this->stop_running_container(force: true);
$this->start_by_compose_file();
@@ -453,11 +519,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function deploy_pull_request()
{
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->newVersionIsHealthy = true;
$this->generate_image_names();
$this->execute_remote_command([
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'",
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.'",
]);
$this->prepare_builder_image();
$this->clone_repository();
@@ -480,17 +545,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function prepare_builder_image()
{
$pull = "--pull=always";
$helperImage = config('coolify.helper_image');
if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
$this->execute_remote_command(
[
"echo -n 'Pulling helper image from $helperImage.'",
"echo -n 'Preparing container with helper image: $helperImage.'",
],
[
$runCommand,
@@ -510,27 +574,58 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
],
);
}
private function check_git_if_build_needed()
{
$this->generate_git_import_commands();
$private_key = data_get($this->application, 'private_key.private_key');
if ($private_key) {
$private_key = base64_encode($private_key);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh")
],
[
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa")
],
[
executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa")
],
[
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$this->branch}"),
"hidden" => true,
"save" => "git_commit_sha"
],
);
} else {
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$this->branch}"),
"hidden" => true,
"save" => "git_commit_sha"
],
);
}
if ($this->saved_outputs->get('git_commit_sha')) {
$this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t");
}
}
private function clone_repository()
{
$importCommands = $this->generate_git_import_commands();
$this->execute_remote_command(
[
"echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
"echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
],
[
$this->importing_git_repository(), "hidden" => true
],
[
executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git rev-parse HEAD"),
"hidden" => true,
"save" => "git_commit_sha"
],
$importCommands, "hidden" => true
]
);
$this->commit = $this->saved_outputs->get('git_commit_sha');
}
private function importing_git_repository()
private function generate_git_import_commands()
{
$this->branch = $this->application->git_branch;
$commands = collect([]);
$git_clone_command = "git clone -q -b {$this->application->git_branch}";
if ($this->pull_request_id !== 0) {
@@ -545,23 +640,31 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) {
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->basedir}";
$this->fullRepoUrl = "{$this->source->html_url}/{$this->customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
} else {
$github_access_token = generate_github_installation_token($this->source);
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->basedir}"));
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git {$this->basedir}"));
$this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git";
}
if ($this->pull_request_id !== 0) {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name"));
}
return $commands->implode(' && ');
}
}
if ($this->application->deploymentType() === 'deploy_key') {
$private_key = base64_encode($this->application->private_key->private_key);
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_repository} {$this->basedir}";
$this->fullRepoUrl = $this->customRepository;
$private_key = data_get($this->application, 'private_key.private_key');
if (is_null($private_key)) {
throw new Exception('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
@@ -572,10 +675,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return $commands->implode(' && ');
}
if ($this->application->deploymentType() === 'other') {
$git_clone_command = "{$git_clone_command} {$this->application->git_repository} {$this->basedir}";
$this->fullRepoUrl = $this->customRepository;
$git_clone_command = "{$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
ray($commands);
return $commands->implode(' && ');
}
}
@@ -623,7 +726,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function nixpacks_build_cmd()
{
$this->generate_env_variables();
$nixpacks_command = "nixpacks build --no-cache -o {$this->workdir} {$this->env_args} --no-error-without-start";
$nixpacks_command = "nixpacks build --cache-key '{$this->application->uuid}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
if ($this->application->build_command) {
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
}
@@ -661,10 +764,35 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables($ports);
$labels = generateLabelsApplication($this->application, $this->preview);
if (data_get($this->application, 'custom_labels')) {
$labels = str($this->application->custom_labels)->explode(',')->toArray();
$labels = collect(str($this->application->custom_labels)->explode(','));
$labels = $labels->filter(function ($value, $key) {
return !Str::startsWith($value, 'coolify.');
});
$this->application->custom_labels = $labels->implode(',');
$this->application->save();
} else {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
}
if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
// $newHostLabel = $newLabels->filter(function ($label) {
// return str($label)->contains('Host');
// });
// $labels = $labels->reject(function ($label) {
// return str($label)->contains('Host');
// });
// ray($labels,$newLabels);
// $labels = $labels->map(function ($label) {
// $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
// $replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
// $newLabel = preg_replace($pattern, $replacement, $label);
// return $newLabel;
// });
// $labels = $labels->merge($newHostLabel);
}
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
$docker_compose = [
'version' => '3.8',
'services' => [
@@ -692,7 +820,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'memswap_limit' => $this->application->limits_memory_swap,
'mem_swappiness' => $this->application->limits_memory_swappiness,
'mem_reservation' => $this->application->limits_memory_reservation,
'cpus' => $this->application->limits_cpus,
'cpus' => (int) $this->application->limits_cpus,
'cpuset' => $this->application->limits_cpuset,
'cpu_shares' => $this->application->limits_cpu_shares,
]
@@ -771,11 +899,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
foreach ($this->application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($this->application->nixpacks_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
} else {
// ray($this->application->runtime_environment_variables_preview)->green();
foreach ($this->application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
}
// Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
@@ -806,25 +940,41 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
return implode(' ', $generated_healthchecks_commands);
}
private function pull_latest_image($image)
{
$this->execute_remote_command(
["echo -n 'Pulling latest image ($image) from the registry.'"],
[
executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true
]
);
}
private function build_image()
{
$this->execute_remote_command([
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]);
if ($this->application->settings->is_static) {
if ($this->application->build_pack === 'static') {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
"echo -n 'Static deployment. Copying static assets to the image.'",
]);
} else {
$this->execute_remote_command([
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]);
}
$dockerfile = base64_encode("FROM {$this->application->static_image}
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
if ($this->application->static_image) {
$this->pull_latest_image($this->application->static_image);
}
if ($this->application->build_pack === 'static') {
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid}
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$nginx_config = base64_encode("server {
$nginx_config = base64_encode("server {
listen 80;
listen [::]:80;
server_name localhost;
@@ -840,43 +990,95 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
root /usr/share/nginx/html;
}
}");
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]);
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid}
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$nginx_config = base64_encode("server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}");
}
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile-prod")
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile")
],
[
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
],
[
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]
);
} else {
// Pure Dockerfile based deployment
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
executeInDocker($this->deployment_uuid, "docker build --pull $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
}
}
private function stop_running_container(bool $force = false)
{
if ($this->currently_running_container_name) {
if ($this->newVersionIsHealthy || $force) {
$this->execute_remote_command(
["echo -n 'Removing old version of your application.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true],
);
$this->execute_remote_command(["echo -n 'Removing old version of your application.'"]);
if ($this->newVersionIsHealthy || $force) {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id !== 0) {
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') === $this->container_name;
});
} else {
$this->execute_remote_command(
["echo -n 'New version is not healthy, rolling back to the old version.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true],
);
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name;
});
}
$containers->each(function ($container) {
$containerName = data_get($container, 'Names');
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
});
} else {
$this->execute_remote_command(
["echo -n 'New version is not healthy, rolling back to the old version.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
}
}
private function start_by_compose_file()
{
if (
!$this->application->dockerfile &&
(
$this->application->build_pack === 'dockerimage' ||
$this->application->build_pack === 'dockerfile')
) {
$this->execute_remote_command(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir}"), "hidden" => true],
);
}
$this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
@@ -885,14 +1087,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function generate_build_env_variables()
{
$this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->commit}"]);
$this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]);
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
$this->build_args->push("--build-arg {$env->key}={$env->value}");
$this->build_args->push("--build-arg {$env->key}=\"{$env->value}\"");
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
$this->build_args->push("--build-arg {$env->key}={$env->value}");
$this->build_args->push("--build-arg {$env->key}=\"{$env->value}\"");
}
}
@@ -937,8 +1139,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
{
$this->execute_remote_command(
["echo 'Oops something is not okay, are you okay? 😢'"],
["echo '{$exception->getMessage()}'"]
["echo '{$exception->getMessage()}'"],
["echo -n 'Deployment failed. Removing the new version of your application.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
);
$this->next(ApplicationDeploymentStatus::FAILED->value);
}
}

View File

@@ -21,7 +21,7 @@ class CheckResaleLicenseJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void
{
try {
resolve(CheckResaleLicense::class)();
CheckResaleLicense::run();
} catch (\Throwable $e) {
send_internal_notification('CheckResaleLicenseJob failed with: ' . $e->getMessage());
ray($e);

View File

@@ -18,7 +18,6 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
@@ -26,6 +25,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function __construct(public Server $server)
{
$this->handle();
}
public function middleware(): array
{
@@ -39,7 +39,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void
{
ray("checking server status for {$this->server->id}");
// ray("checking server status for {$this->server->id}");
try {
// ray()->clearAll();
$serverUptimeCheckNumber = $this->server->unreachable_count;
@@ -58,6 +58,23 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$this->server->update([
'unreachable_count' => 0,
]);
// Update all applications, databases and services to exited
foreach ($this->server->applications() as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->server->databases() as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->server->services() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
}
}
return;
}
$result = $this->server->validateConnection();
@@ -138,11 +155,13 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$containerStatus = "$containerStatus ($containerHealth)";
$labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId');
if ($labelId) {
if (str_contains($labelId, '-pr-')) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
$applicationId = (int) Str::before($labelId, '-pr-');
$applicationId = data_get($labels, 'coolify.applicationId');
if ($applicationId) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
if ($pullRequestId) {
if (str($applicationId)->contains('-')) {
$applicationId = str($applicationId)->before('-');
}
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) {
$foundApplicationPreviews[] = $preview->id;
@@ -154,7 +173,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
//Notify user that this container should not be there.
}
} else {
$application = $applications->where('id', $labelId)->first();
$application = $applications->where('id', $applicationId)->first();
if ($application) {
$foundApplications[] = $application->id;
$statusFromDb = $application->status;
@@ -230,10 +249,15 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$name = data_get($exitedService, 'name');
$fqdn = data_get($exitedService, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($service, 'environment.project');
$environment = data_get($service, 'environment');
$projectUuid = data_get($service, 'environment.project.uuid');
$serviceUuid = data_get($service, 'uuid');
$environmentName = data_get($service, 'environment.name');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/service/" . $service->uuid;
if ($projectUuid && $serviceUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
} else {
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']);
}
@@ -251,10 +275,15 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($application, 'environment.project');
$environment = data_get($application, 'environment');
$projectUuid = data_get($application, 'environment.project.uuid');
$applicationUuid = data_get($application, 'uuid');
$environment = data_get($application, 'environment.name');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $application->uuid;
if ($projectUuid && $applicationUuid && $environment) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
} else {
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
@@ -271,10 +300,16 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($preview, 'application.environment.project');
$environment = data_get($preview, 'application.environment');
$projectUuid = data_get($preview, 'application.environment.project.uuid');
$environmentName = data_get($preview, 'application.environment.name');
$applicationUuid = data_get($preview, 'application.uuid');
if ($projectUuid && $applicationUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid;
} else {
$url = null;
}
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $preview->application->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
@@ -290,10 +325,15 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$containerName = $name;
$project = data_get($database, 'environment.project');
$environment = data_get($database, 'environment');
$projectUuid = data_get($database, 'environment.project.uuid');
$environmentName = data_get($database, 'environment.name');
$databaseUuid = data_get($database, 'uuid');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/database/" . $database->uuid;
if ($projectUuid && $databaseUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
} else {
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
} catch (\Throwable $e) {

View File

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

View File

@@ -19,7 +19,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class StopResourceJob implements ShouldQueue, ShouldBeEncrypted
class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@@ -32,6 +32,7 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted
try {
$server = $this->resource->destination->server;
if (!$server->isFunctional()) {
$this->resource->delete();
return 'Server is not functional';
}
switch ($this->resource->type()) {
@@ -57,11 +58,10 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted
StopService::run($this->resource);
break;
}
$this->resource->delete();
} catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());
throw $e;
} finally {
$this->resource->delete();
}
}
}

View File

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

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class PullHelperImageJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server)
{
}
public function handle(): void
{
try {
$helperImage = config('coolify.helper_image');
ray("Pulling {$helperImage}");
instant_remote_process(["docker pull -q {$helperImage}"], $this->server, false);
ray('PullHelperImageJob done');
} catch (\Throwable $e) {
send_internal_notification('PullHelperImageJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -46,11 +46,12 @@ class SendMessageToTelegramJob implements ShouldQueue, ShouldBeEncrypted
if (!empty($this->buttons)) {
foreach ($this->buttons as $button) {
$buttonUrl = data_get($button, 'url');
$text = data_get($button, 'text', 'Click here');
if ($buttonUrl && Str::contains($buttonUrl, 'http://localhost')) {
$buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl);
}
$inlineButtons[] = [
'text' => $button['text'],
'text' => $text,
'url' => $buttonUrl,
];
}

View File

@@ -34,8 +34,11 @@ class Application extends BaseModel
static::deleting(function ($application) {
$application->settings()->delete();
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false);
$server = data_get($application, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$application->persistentStorages()->delete();
$application->environment_variables()->delete();
@@ -210,6 +213,14 @@ class Application extends BaseModel
return $this->morphTo();
}
public function isDeploymentInprogress() {
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
if ($deployments > 0) {
return true;
}
return false;
}
public function deployments(int $skip = 0, int $take = 10)
{
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc');

View File

@@ -11,7 +11,7 @@ class EnvironmentVariable extends Model
{
protected $guarded = [];
protected $casts = [
"key" => 'string',
'key' => 'string',
'value' => 'encrypted',
'is_build_time' => 'boolean',
];
@@ -21,6 +21,10 @@ class EnvironmentVariable extends Model
static::created(function ($environment_variable) {
if ($environment_variable->application_id && !$environment_variable->is_preview) {
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first();
$application = Application::find($environment_variable->application_id);
if ($application->build_pack === 'dockerfile') {
return;
}
if (!$found) {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
@@ -33,7 +37,8 @@ class EnvironmentVariable extends Model
}
});
}
public function service() {
public function service()
{
return $this->belongsTo(Service::class);
}
protected function value(): Attribute
@@ -55,9 +60,9 @@ class EnvironmentVariable extends Model
$variable = Str::after($environment_variable, 'global.');
$variable = Str::before($variable, '}}');
$variable = Str::of($variable)->trim()->value;
// $environment_variable = GlobalEnvironmentVariable::where('name', $environment_variable)->where('team_id', $team_id)->first()?->value;
ray('global env variable');
return $environment_variable;
// $environment_variable = GlobalEnvironmentVariable::where('name', $environment_variable)->where('team_id', $team_id)->first()?->value;
ray('global env variable');
return $environment_variable;
}
return $environment_variable;
}
@@ -77,5 +82,4 @@ class EnvironmentVariable extends Model
set: fn (string $value) => Str::of($value)->trim(),
);
}
}

View File

@@ -109,11 +109,12 @@ class Server extends BaseModel
return $this->proxy->modelScope();
}
public function isEmpty()
public function hasDefinedResources()
{
$applications = $this->applications()->count() === 0;
$databases = $this->databases()->count() === 0;
if ($applications && $databases) {
$services = $this->services()->count() === 0;
if ($applications || $databases || $services) {
return true;
}
return false;

View File

@@ -6,10 +6,7 @@ use Illuminate\Database\Eloquent\Model;
class ServerSetting extends Model
{
protected $fillable = [
'server_id',
'is_usable',
];
protected $guarded = [];
public function server()
{

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\Yaml\Yaml;
use Illuminate\Support\Str;
@@ -23,31 +22,207 @@ class Service extends BaseModel
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
$application->persistentStorages()->delete();
}
foreach ($service->databases()->get() as $database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
$database->persistentStorages()->delete();
}
$service->environment_variables()->delete();
$service->applications()->delete();
$service->databases()->delete();
if ($storagesToDelete->count() > 0) {
$storagesToDelete->each(function ($storage) use ($service) {
instant_remote_process(["docker volume rm -f $storage->name"], $service->server, false);
$server = data_get($service, 'server');
if ($server && $storagesToDelete->count() > 0) {
$storagesToDelete->each(function ($storage) use ($server) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
});
}
instant_remote_process(["docker network rm {$service->uuid}"], $service->server, false);
});
}
public function type()
{
return 'service';
}
public function extraFields()
{
$fields = collect([]);
$applications = $this->applications()->get();
foreach ($applications as $application) {
$image = str($application->image)->before(':')->value();
switch ($image) {
case str($image)->contains('minio'):
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first();
$fields->put('MinIO', [
'Console URL' => [
'key' => data_get($console_url, 'key'),
'value' => data_get($console_url, 'value'),
'rules' => 'required|url',
],
'S3 API URL' => [
'key' => data_get($s3_api_url, 'key'),
'value' => data_get($s3_api_url, 'value'),
'rules' => 'required|url',
],
'Admin User' => [
'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value'),
'rules' => 'required',
],
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
break;
case str($image)->contains('weblate'):
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
$fields->put('Weblate', [
'Admin Email' => [
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
}
$databases = $this->databases()->get();
foreach ($databases as $database) {
$image = str($database->image)->before(':')->value();
switch ($image) {
case str($image)->contains('postgres'):
$userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
$passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
$dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
$postgres_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$postgres_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$postgres_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$fields->put('PostgreSQL', [
'User' => [
'key' => data_get($postgres_user, 'key'),
'value' => data_get($postgres_user, 'value'),
'rules' => 'required',
],
'Password' => [
'key' => data_get($postgres_password, 'key'),
'value' => data_get($postgres_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
'Database Name' => [
'key' => data_get($postgres_db_name, 'key'),
'value' => data_get($postgres_db_name, 'value'),
'rules' => 'required',
],
]);
break;
case str($image)->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mysql_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
$mysql_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$fields->put('MySQL', [
'User' => [
'key' => data_get($mysql_user, 'key'),
'value' => data_get($mysql_user, 'value'),
'rules' => 'required',
],
'Password' => [
'key' => data_get($mysql_password, 'key'),
'value' => data_get($mysql_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
'Root Password' => [
'key' => data_get($mysql_root_password, 'key'),
'value' => data_get($mysql_root_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
'Database Name' => [
'key' => data_get($mysql_db_name, 'key'),
'value' => data_get($mysql_db_name, 'value'),
'rules' => 'required',
],
]);
break;
case str($image)->contains('mariadb'):
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS'];
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA'];
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
$mariadb_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$fields->put('MariaDB', [
'User' => [
'key' => data_get($mariadb_user, 'key'),
'value' => data_get($mariadb_user, 'value'),
'rules' => 'required',
],
'Password' => [
'key' => data_get($mariadb_password, 'key'),
'value' => data_get($mariadb_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
'Root Password' => [
'key' => data_get($mariadb_root_password, 'key'),
'value' => data_get($mariadb_root_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
'Database Name' => [
'key' => data_get($mariadb_db_name, 'key'),
'value' => data_get($mariadb_db_name, 'value'),
'rules' => data_get($mariadb_db_name, 'value') && 'required',
],
]);
break;
}
}
return $fields;
}
public function saveExtraFields($fields)
{
foreach ($fields as $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$found = $this->environment_variables()->where('key', $key)->first();
if ($found) {
$found->value = $value;
$found->save();
} else {
$this->environment_variables()->create([
'key' => $key,
'value' => $value,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
}
}
public function documentation()
{
$services = getServiceTemplates();
@@ -90,6 +265,10 @@ class Service extends BaseModel
{
return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
}
public function environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
}
public function workdir()
{
return service_configuration_dir() . "/{$this->uuid}";
@@ -115,7 +294,7 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection
{
ray()->clearAll();
// ray()->clearAll();
if ($this->docker_compose_raw) {
try {
$yaml = Yaml::parse($this->docker_compose_raw);
@@ -254,13 +433,25 @@ class Service extends BaseModel
]);
}
}
$networks = $serviceNetworks->toArray();
foreach ($definedNetwork as $key => $network) {
$networks = array_merge($networks, [
$network
]);
$networks = collect();
foreach ($serviceNetworks as $key => $serviceNetwork) {
if (gettype($serviceNetwork) === 'string') {
// networks:
// - appwrite
$networks->put($serviceNetwork, null);
} else if (gettype($serviceNetwork) === 'array') {
// networks:
// default:
// ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null);
ray($key);
$networks->put($key, $serviceNetwork);
}
}
data_set($service, 'networks', $networks);
foreach ($definedNetwork as $key => $network) {
$networks->put($network, null);
}
data_set($service, 'networks', $networks->toArray());
// Collect/create/update volumes
if ($serviceVolumes->count() > 0) {
@@ -381,6 +572,7 @@ class Service extends BaseModel
$key = Str::of($variableName);
$value = Str::of($variable);
}
// TODO: here is the problem
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
@@ -438,15 +630,31 @@ class Service extends BaseModel
'service_id' => $this->id,
])->first();
if ($value->startsWith('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
$forService = $value->afterLast('_');
$generatedValue = null;
// Count _ in $value
$count = substr_count($value->value(), '_');
if ($count === 2) {
// SERVICE_FQDN_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
$forService = $value->afterLast('_');
$generatedValue = null;
$port = null;
}
if ($count === 3) {
// SERVICE_FQDN_UMAMI_1000
$command = $value->after('SERVICE_')->before('_');
$forService = $value->after('SERVICE_')->after('_')->before('_');
$generatedValue = null;
$port = $value->afterLast('_');
}
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($this->server, $containerName);
} else {
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value');
} else {
@@ -462,7 +670,7 @@ class Service extends BaseModel
]);
}
if (!$isDatabase) {
if ($command->value() === 'FQDN') {
if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
$savedService->fqdn = $fqdn;
$savedService->save();
}
@@ -533,7 +741,11 @@ class Service extends BaseModel
}
// Add labels to the service
$fqdns = collect(data_get($savedService, 'fqdns'));
if ($savedService->serviceType()) {
$fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
} else {
$fqdns = collect(data_get($savedService, 'fqdns'));
}
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) {

View File

@@ -11,10 +11,27 @@ class ServiceApplication extends BaseModel
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::deleting(function ($service) {
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
}
public function type()
{
return 'service';
}
public function serviceType()
{
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
return str($this->image)->before(':')->value() === $service;
})->first());
if ($found->isNotEmpty()) {
return $found;
}
return null;
}
public function service()
{
return $this->belongsTo(Service::class);

View File

@@ -9,10 +9,39 @@ class ServiceDatabase extends BaseModel
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::deleting(function ($service) {
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
}
public function type()
{
return 'service';
}
public function serviceType()
{
return null;
}
public function databaseType()
{
$image = str($this->image)->before(':');
if ($image->value() === 'postgres') {
$image = 'postgresql';
}
return "standalone-$image";
}
public function getServiceDatabaseUrl()
{
$port = $this->public_port;
$realIp = $this->service->server->ip;
if ($realIp === 'host.docker.internal' || isDev()) {
$realIp = base_ip();
}
$url = "{$realIp}:{$port}";
return $url;
}
public function service()
{
return $this->belongsTo(Service::class);
@@ -29,4 +58,8 @@ class ServiceDatabase extends BaseModel
{
getFilesystemVolumesFromServer($this, $isInit);
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -55,6 +55,6 @@ class StandaloneDocker extends BaseModel
public function attachedTo()
{
return $this->applications?->count() > 0 || $this->databases?->count() > 0;
return $this->applications?->count() > 0 || $this->databases()->count() > 0;
}
}

View File

@@ -30,8 +30,11 @@ class StandaloneMariadb extends BaseModel
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();

View File

@@ -32,16 +32,33 @@ class StandaloneMongodb extends BaseModel
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
});
}
public function mongoInitdbRootPassword(): Attribute
{
return Attribute::make(
get: function ($value) {
try {
return decrypt($value);
} catch (\Throwable $th) {
$this->mongo_initdb_root_password = encrypt($value);
$this->save();
return $value;
}
}
);
}
public function portsMappings(): Attribute
{
return Attribute::make(
@@ -63,7 +80,8 @@ class StandaloneMongodb extends BaseModel
{
return 'standalone-mongodb';
}
public function getDbUrl(bool $useInternal = false) {
public function getDbUrl(bool $useInternal = false)
{
if ($this->is_public && !$useInternal) {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
} else {

View File

@@ -30,8 +30,11 @@ class StandaloneMysql extends BaseModel
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();

View File

@@ -30,8 +30,11 @@ class StandalonePostgresql extends BaseModel
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();

View File

@@ -26,8 +26,11 @@ class StandaloneRedis extends BaseModel
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
@@ -55,8 +58,9 @@ class StandaloneRedis extends BaseModel
{
return 'standalone-redis';
}
public function getDbUrl(): string {
if ($this->is_public) {
public function getDbUrl(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://:{$this->redis_password}@{$this->uuid}:6379/0";

View File

@@ -39,14 +39,18 @@ class Subscription extends Model
if (!$subscription) {
return null;
}
$subscriptionPlanId = data_get($subscription,'stripe_plan_id');
$subscriptionPlanId = data_get($subscription, 'stripe_plan_id');
if (!$subscriptionPlanId) {
return null;
}
$subscriptionInvoicePaid = data_get($subscription, 'stripe_invoice_paid');
if (!$subscriptionInvoicePaid) {
return null;
}
$subscriptionConfigs = collect(config('subscription'));
$stripePlanId = null;
$subscriptionConfigs->map(function ($value, $key) use ($subscriptionPlanId, &$stripePlanId) {
if ($value === $subscriptionPlanId){
if ($value === $subscriptionPlanId) {
$stripePlanId = $key;
};
})->first();

View File

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

View File

@@ -34,7 +34,9 @@ class EmailChannel
if (isset($recepients)) {
$message .= implode(', ', $recepients);
}
$message .= " with subject: {$mailMessage->subject}";
if (isset($mailMessage)) {
$message .= " with subject: {$mailMessage->subject}";
}
send_internal_notification($message);
throw $e;
}
@@ -49,8 +51,8 @@ class EmailChannel
}
return;
}
config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address'));
config()->set('mail.from.name', data_get($notifiable, 'smtp_from_name'));
config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address', 'test@example.com'));
config()->set('mail.from.name', data_get($notifiable, 'smtp_from_name', 'Test'));
if (data_get($notifiable, 'resend_enabled')) {
config()->set('mail.default', 'resend');
config()->set('resend.api_key', data_get($notifiable, 'resend_api_key'));

View File

@@ -48,7 +48,7 @@ class RouteServiceProvider extends ServiceProvider
if ($request->path() === 'api/health') {
return Limit::perMinute(1000)->by($request->user()?->id ?: $request->ip());
}
return Limit::perMinute(30)->by($request->user()?->id ?: $request->ip());
return Limit::perMinute(200)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('5', function (Request $request) {
return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip());

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false)
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false)
{
$deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id,
@@ -12,6 +12,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu
'pull_request_id' => $pull_request_id,
'force_rebuild' => $force_rebuild,
'is_webhook' => $is_webhook,
'restart_only' => $restart_only,
'commit' => $commit,
]);
$queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at');

View File

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

View File

@@ -8,13 +8,26 @@ use Illuminate\Support\Str;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id): Collection
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection
{
ray($id, $pullRequestId);
$containers = collect([]);
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
if (!$containers) {
return collect([]);
}
return format_docker_command_output_to_json($containers);
$containers = format_docker_command_output_to_json($containers);
$containers = $containers->map(function ($container) use ($pullRequestId) {
$labels = data_get($container, 'Labels');
if (!str($labels)->contains("coolify.pullRequestId=")) {
data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}");
return $container;
}
if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
return $container;
}
return null;
});
$containers = $containers->filter();
ray($containers);
return $containers;
}
function format_docker_command_output_to_json($rawOutput): Collection
@@ -77,20 +90,6 @@ function executeInDocker(string $containerId, string $command)
// return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1; [ \$PIPESTATUS -eq 0 ] || exit \$PIPESTATUS'";
}
function getApplicationContainerStatus(Application $application)
{
$server = data_get($application, 'destination.server');
$id = $application->id;
if (!$server) {
return 'exited';
}
$containers = getCurrentApplicationContainerStatus($server, $id);
if ($containers->count() > 0) {
$status = data_get($containers[0], 'State', 'exited');
return $status;
}
return 'exited';
}
function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false)
{
$container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError);
@@ -138,20 +137,56 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
$labels->push("coolify." . $type . "Id=" . $id);
$labels->push("coolify.type=$type");
$labels->push('coolify.name=' . $name);
if ($pull_request_id !== 0) {
$labels->push('coolify.pullRequestId=' . $pull_request_id);
}
$labels->push('coolify.pullRequestId=' . $pull_request_id);
if ($type === 'service') {
$labels->push('coolify.service.subId=' . $subId);
$labels->push('coolify.service.subType=' . $subType);
}
return $labels;
}
function generateServiceSpecificFqdns($service, $forTraefik = false)
{
$variables = collect($service->service->environment_variables);
$type = $service->serviceType();
$payload = collect([]);
switch ($type) {
case $type->contains('minio'):
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
return $payload;
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
return $payload;
}
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([
"value" => generateFqdn($service->service->server, 'console-' . $service->uuid)
]);
}
if (is_null($MINIO_SERVER_URL?->value)) {
$MINIO_SERVER_URL?->update([
"value" => generateFqdn($service->service->server, 'minio-' . $service->uuid)
]);
}
if ($forTraefik) {
$payload = collect([
$MINIO_BROWSER_REDIRECT_URL->value . ':9001',
$MINIO_SERVER_URL->value . ':9000',
]);
} else {
$payload = collect([
$MINIO_BROWSER_REDIRECT_URL->value,
$MINIO_SERVER_URL->value,
]);
}
}
return $payload;
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
foreach ($domains as $loop => $domain) {
$uuid = new Cuid2(7);
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();
@@ -212,13 +247,11 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$onlyPort = $ports[0];
}
$pull_request_id = data_get($preview, 'pull_request_id', 0);
// $container_name = generateApplicationContainerName($application, $pull_request_id);
$appId = $application->id;
if ($pull_request_id !== 0 && $pull_request_id !== null) {
$appId = $appId . '-pr-' . $pull_request_id;
$appUuid = $application->uuid;
if ($pull_request_id !== 0) {
$appUuid = $appUuid . '-pr-' . $pull_request_id;
}
$labels = collect([]);
$labels = $labels->merge(defaultLabels($appId, $application->uuid, $pull_request_id));
if ($application->fqdn) {
if ($pull_request_id !== 0) {
$domains = Str::of(data_get($preview, 'fqdn'))->explode(',');
@@ -226,7 +259,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(',');
}
// Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($application->uuid, $domains, $application->settings->is_force_https_enabled, $onlyPort));
$labels = $labels->merge(fqdnLabelsForTraefik($appUuid, $domains, $application->settings->is_force_https_enabled, $onlyPort));
}
return $labels->all();
}

View File

@@ -174,8 +174,11 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
return $formatted;
}
function refresh_server_connection(PrivateKey $private_key)
function refresh_server_connection(?PrivateKey $private_key = null)
{
if (is_null($private_key)) {
return;
}
foreach ($private_key->servers as $server) {
Storage::disk('ssh-mux')->delete($server->muxFilename());
}

View File

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

View File

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

View File

@@ -148,6 +148,8 @@ function allowedPathsForInvalidAccounts() {
return [
'logout',
'verify',
'force-password-reset',
'livewire/message/force-password-reset',
'livewire/message/verify-email',
'livewire/message/help'
];

View File

@@ -21,6 +21,7 @@
"laravel/ui": "^4.2",
"lcobucci/jwt": "^5.0.0",
"league/flysystem-aws-s3-v3": "^3.0",
"league/flysystem-sftp-v3": "^3.0",
"livewire/livewire": "^v2.12.3",
"lorisleiva/laravel-actions": "^2.7",
"masmerise/livewire-toaster": "^1.2",

797
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->boolean('is_shown_once')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('is_shown_once');
});
}
};

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('application_deployment_queues', function (Blueprint $table) {
$table->boolean('restart_only')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->dropColumn('restart_only');
});
}
};

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