Compare commits

...

401 Commits

Author SHA1 Message Date
Andras Bacsai
9c69044da5 Merge pull request #1463 from coollabsio/next
v4.0.0-beta.139
2023-11-17 11:35:48 +01:00
Andras Bacsai
ebc4ab9af5 Remove unnecessary ray() statement 2023-11-17 11:33:46 +01:00
Andras Bacsai
57738198ad Add fluentd logging configuration for database & services 2023-11-17 11:32:52 +01:00
Andras Bacsai
b8252b85b0 Refactor logging configuration in ApplicationDeploymentJob.php 2023-11-17 11:13:16 +01:00
Andras Bacsai
479c2743bd Update Fluent Bit configuration file 2023-11-17 10:50:02 +01:00
Andras Bacsai
81e6482d7a Remove commented out code and fix indentation 2023-11-17 10:21:42 +01:00
Andras Bacsai
88c5d87084 Add log drain settings for New Relic,Highlight.io, and Axiom 2023-11-17 10:21:19 +01:00
Andras Bacsai
6c7e091e1b feat: log drain (wip) 2023-11-17 00:37:09 +01:00
Andras Bacsai
91e3d33c0b Add cleanup of stucked helper containers on servers 2023-11-16 20:48:25 +01:00
Andras Bacsai
aa00389824 Remove redundant sentence about cloud version in
README.md
2023-11-16 17:43:20 +01:00
Andras Bacsai
b4e54ab3e3 Improve Cloud version features and reduce
maintenance
2023-11-16 17:42:25 +01:00
Andras Bacsai
8f3c5d4bd3 Add donation link and update version numbers 2023-11-16 17:40:49 +01:00
Andras Bacsai
26668c71a1 Merge pull request #1460 from coollabsio/next
v4.0.0-beta.138
2023-11-16 15:28:48 +01:00
Andras Bacsai
bd7637c696 Add healthcheck URL to deployment job and update
version to beta.138
2023-11-16 15:23:07 +01:00
Andras Bacsai
cff54f48a3 Merge pull request #1459 from coollabsio/next
v4.0.0-beta.137
2023-11-16 14:39:48 +01:00
Andras Bacsai
5c0f239f62 Update server readiness check runtime to 1 minute 2023-11-16 14:36:43 +01:00
Andras Bacsai
d56c28c8d9 Remove unused notifications from
ContainerStatusJob
2023-11-16 14:29:23 +01:00
Andras Bacsai
2b666ff121 Refactor server and docker cleanup jobs 2023-11-16 14:29:01 +01:00
Andras Bacsai
fb42c43953 Add isLocalhost method to Server model and
conditionally show Cloudflare Tunnel checkbox in
server form view
2023-11-16 14:28:26 +01:00
Andras Bacsai
81437e6822 Fix high disk usage notification bug in
ServerStatusJob.php and HighDiskUsage.php
2023-11-16 13:49:08 +01:00
Andras Bacsai
2fe429fe92 Comment out logging configuration in
ApplicationDeploymentJob.php
2023-11-16 13:32:07 +01:00
Andras Bacsai
4f0b214042 Add timeout to ApplicationDeploymentJob 2023-11-16 13:27:51 +01:00
Andras Bacsai
c866213f34 fix: when to pull image 2023-11-16 13:22:12 +01:00
Andras Bacsai
7cec6330cf Update server status check and notifications 2023-11-16 11:53:37 +01:00
Andras Bacsai
f5de21a343 Add OTLP exporter and host metrics receiver
configuration to config.yaml.
2023-11-16 11:16:41 +01:00
Andras Bacsai
ecbfc4d790 Add Fluent Bit and New Relic configurations 2023-11-15 15:45:37 +01:00
Andras Bacsai
55ff00e028 Add logging configuration to compose file 2023-11-15 15:19:31 +01:00
Andras Bacsai
a0fc2bbb85 Merge pull request #1457 from coollabsio/next
v4.0.0-beta.136
2023-11-15 10:55:39 +01:00
Andras Bacsai
51a704b22a Remove middleware and uniqueId methods from
DockerCleanupJob
2023-11-15 10:37:55 +01:00
Andras Bacsai
6d49678842 Remove unnecessary echo and add alive message 2023-11-15 10:37:02 +01:00
Andras Bacsai
0459b3a115 Add init-script to prod-ssu Docker container 2023-11-15 10:31:48 +01:00
Andras Bacsai
82592c8222 Add alive request to Init command 2023-11-15 10:26:31 +01:00
Andras Bacsai
25bf8895e2 Add InstanceSettings to Init command 2023-11-15 10:20:48 +01:00
Andras Bacsai
f4f7bdf7d5 Update dependencies and add new feature 2023-11-15 10:18:41 +01:00
Andras Bacsai
c008564aa3 Merge pull request #1456 from coollabsio/next
v4.0.0-beta.135
2023-11-15 09:40:33 +01:00
Andras Bacsai
b825d98b2d Refactor storage connection handling and project
initialization
2023-11-15 09:34:27 +01:00
Andras Bacsai
1f711d9281 Update version and fix webhook generation 2023-11-15 09:15:49 +01:00
Andras Bacsai
1de850f640 Merge pull request #1453 from coollabsio/next
v4.0.0-beta.134
2023-11-14 19:31:24 +01:00
Andras Bacsai
f176247b02 Update application deployment and version numbers 2023-11-14 19:29:59 +01:00
Andras Bacsai
3f3a1283df Merge pull request #1452 from coollabsio/next
v4.0.0-beta.133
2023-11-14 15:07:15 +01:00
Andras Bacsai
087bfcad08 Update server model and version configurations 2023-11-14 15:06:03 +01:00
Andras Bacsai
efd2899ae3 Merge pull request #1450 from coollabsio/next
v4.0.0-beta.132
2023-11-14 14:22:17 +01:00
Andras Bacsai
e4b2195932 Fix manual Git webhook generation 2023-11-14 14:14:21 +01:00
Andras Bacsai
0590ed7b2e Update webhooks configuration and application search. 2023-11-14 14:07:48 +01:00
Andras Bacsai
3a3c9448a4 Add gitWebhook method to Application model and fix
Dockerfile input display
2023-11-14 14:07:42 +01:00
Andras Bacsai
36d65ad5a8 Fix Dockerfile location in deployment job 2023-11-14 14:07:33 +01:00
Andras Bacsai
8db66952e8 Add manual Git webhooks and migration files 2023-11-14 13:26:14 +01:00
Andras Bacsai
45fa88ca4d Add error handling for missing email settings in
EmailChannel.php
2023-11-14 11:04:45 +01:00
Andras Bacsai
84b74f0b57 Update version numbers to 4.0.0-beta.132 2023-11-14 10:59:02 +01:00
Andras Bacsai
423cf62d92 Add support for dynamic docker-compose file name
in ApplicationDeploymentJob.php
2023-11-14 08:52:17 +01:00
Andras Bacsai
c4d9deabef Add debugging statement to report exceptions in
development environment
2023-11-13 21:17:17 +01:00
Andras Bacsai
776b1cb68d Add unauthenticated method to handle
authentication exceptions
2023-11-13 21:16:48 +01:00
Andras Bacsai
fc3025398e Merge pull request #1447 from coollabsio/next
v4.0.0-beta.131
2023-11-13 19:35:43 +01:00
Andras Bacsai
457c16c4dc remove ray 2023-11-13 19:26:11 +01:00
Andras Bacsai
ccf63c67e8 fix: mariadb backups 2023-11-13 19:25:18 +01:00
Andras Bacsai
945157b30c Merge pull request #1446 from coollabsio/next
v4.0.0-beta.130
2023-11-13 17:09:10 +01:00
Andras Bacsai
13798392be fix: generate service fields 2023-11-13 17:06:43 +01:00
Andras Bacsai
0d05b0a3d6 Merge pull request #1445 from coollabsio/next
v4.0.0-beta.129
2023-11-13 16:48:18 +01:00
Andras Bacsai
e0d2f88d99 fix: fqdn for minio 2023-11-13 16:45:54 +01:00
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
Andras Bacsai
61c43804e3 Merge pull request #1360 from coollabsio/next
v4.0.0-beta.100
2023-10-24 14:44:31 +02:00
Andras Bacsai
72421d692b add slogans 2023-10-24 14:36:43 +02:00
Andras Bacsai
f801bb98cd feat: mysql, mariadb 2023-10-24 14:31:28 +02:00
Andras Bacsai
b2d111e49a feat: simple search functionality 2023-10-24 12:33:49 +02:00
Andras Bacsai
c82e02218f version++ 2023-10-24 11:08:59 +02:00
Andras Bacsai
29f64076de fix: syncbunny command 2023-10-24 11:08:15 +02:00
Andras Bacsai
393c334b12 version++ 2023-10-24 11:08:11 +02:00
Andras Bacsai
678b264688 fix: make sure coolfiy network exists on install 2023-10-24 11:08:05 +02:00
Andras Bacsai
2620bfbf08 Merge pull request #1359 from coollabsio/next
v4.0.0-beta.99
2023-10-24 10:59:36 +02:00
Andras Bacsai
18c32decad guarded 2023-10-24 10:43:34 +02:00
Andras Bacsai
a6f9e5f0af fixes 2023-10-24 10:42:33 +02:00
Andras Bacsai
f187040b7e fix: mongodb backup 2023-10-24 10:42:28 +02:00
Andras Bacsai
5510321776 syncbunny update 2023-10-24 10:22:36 +02:00
Andras Bacsai
69691b2ca7 fix: service template generator + appwrite 2023-10-24 10:19:12 +02:00
Andras Bacsai
8bfc1a7c06 fix: do not allow to delete env if a resource is defined 2023-10-24 10:11:21 +02:00
Andras Bacsai
554222abc7 fix: cleanup stucked resources on start 2023-10-24 10:10:55 +02:00
Andras Bacsai
b1a1aeeb75 fix: clone to with the same environment name 2023-10-24 10:10:45 +02:00
Andras Bacsai
91acd4cb6a fix: backups should be done with internal db url
fix: create default database on mongodb start with a collection
2023-10-24 09:34:35 +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
Andras Bacsai
aaeacad781 Merge pull request #1350 from coollabsio/next
v4.0.0-beta.98
2023-10-20 18:17:50 +02:00
Andras Bacsai
b539f40fa5 fix 2023-10-20 18:16:47 +02:00
Andras Bacsai
fae340afcb fix: boarding 2023-10-20 18:15:25 +02:00
Andras Bacsai
69ebff1a7a Merge pull request #1347 from coollabsio/next
v4.0.0-beta.97
2023-10-20 15:25:50 +02:00
Andras Bacsai
5d9cfc393e add s3 to magicbar 2023-10-20 15:02:40 +02:00
Andras Bacsai
e2a256b31c add api tokens to magic bar 2023-10-20 15:00:57 +02:00
Andras Bacsai
4855af7e57 feat: start all kinds of things 2023-10-20 14:58:00 +02:00
Andras Bacsai
a664174c02 feat: api tokens + deploy webhook 2023-10-20 14:51:01 +02:00
Andras Bacsai
c19c13b4e2 feat: cloning project 2023-10-20 12:34:53 +02:00
Andras Bacsai
266b99bc25 fix: port exposes change, shoud regenerate label 2023-10-20 12:34:25 +02:00
Andras Bacsai
51ef24e1fb fix 2023-10-20 09:38:21 +02:00
Andras Bacsai
33d38ccf40 fix: preselect s3 storage if available 2023-10-20 09:38:13 +02:00
Andras Bacsai
f470ebbbe0 ui: updates 2023-10-20 09:29:09 +02:00
Andras Bacsai
11bd46b200 wip: mongodb backup 2023-10-19 17:17:38 +02:00
Andras Bacsai
53f5674771 wip: mongodb backup 2023-10-19 13:46:15 +02:00
Andras Bacsai
c53d88902c feat: standalone mongodb 2023-10-19 13:32:03 +02:00
Andras Bacsai
e342c4fd65 fix: add PGUSER to prevent HC warning 2023-10-19 11:58:12 +02:00
Andras Bacsai
aab7bd5e28 Merge pull request #1345 from altinselimi/patch-1
Fix spelling error in README.md
2023-10-19 11:49:42 +02:00
Andras Bacsai
3adefb9e49 command: generate services 2023-10-19 11:28:25 +02:00
TheH2SO4
1bfce6716c Merge pull request #2 from coollabsio/next
Next
2023-10-19 11:18:19 +02:00
Altin Selimi
c904441787 Update README.md
Fix image -> imagine spelling error
2023-10-19 11:02:56 +02:00
Andras Bacsai
b7f79ae034 Merge pull request #1333 from theh2so4/main
[+] Templates: BabyBuddy, Code-Server, Dokuwiki, Heimdall, MeTube, SnapDrop and PairDrop
2023-10-19 10:51:24 +02:00
Andras Bacsai
2d63fcdc7f implement new service templates 2023-10-19 10:51:03 +02:00
Andras Bacsai
c1d0cabcfb fix: service docs links 2023-10-19 10:50:52 +02:00
Andras Bacsai
166419b13a update contribution guide 2023-10-19 10:50:47 +02:00
Andras Bacsai
cfc4d3acc7 Merge branch 'main' into next 2023-10-19 09:23:30 +02:00
Andras Bacsai
13a0c2cf43 Merge pull request #1344 from liweiyi88/github-trending-badge
Add github trending badge in readme recognitions
2023-10-19 09:23:01 +02:00
liweiyi88
6ef6975432 add github trending badge in readme recognitions 2023-10-19 09:41:54 +11:00
Andras Bacsai
2c40e93d3b wip: PAT by team 2023-10-18 18:02:09 +02:00
Andras Bacsai
a30ae4fb38 version++ 2023-10-18 15:49:58 +02:00
Andras Bacsai
5b8785d1a9 update prod compose 2023-10-18 15:49:50 +02:00
Andras Bacsai
f6f3364269 Merge pull request #1343 from coollabsio/next
v4.0.0-beta.96
2023-10-18 15:43:26 +02:00
Andras Bacsai
2f93f4450f fix: containerStatus job 2023-10-18 15:43:14 +02:00
Andras Bacsai
2ad7c2b1ce fix: remove custom port from git repo url 2023-10-18 15:33:07 +02:00
Andras Bacsai
6c848199ed fix: add custom port as ssh option to deploy_key based commands 2023-10-18 15:23:43 +02:00
Andras Bacsai
76aab722b8 fix: limit horizon processes to 2 by default 2023-10-18 15:07:04 +02:00
Andras Bacsai
12290304c4 Merge pull request #1342 from coollabsio/next
v4.0.0-beta.95
2023-10-18 14:47:05 +02:00
Andras Bacsai
3a27d13c3e fix 2023-10-18 14:46:26 +02:00
Andras Bacsai
4f588ced96 call handle not matter what 2023-10-18 14:43:48 +02:00
Andras Bacsai
e266c7cdec fix: email channel no recepients 2023-10-18 14:22:09 +02:00
Andras Bacsai
eedc3faba3 fix: labels 2023-10-18 14:14:40 +02:00
Andras Bacsai
2e2c932f07 Merge pull request #1341 from coollabsio/next
v4.0.0-beta.94
2023-10-18 12:49:11 +02:00
Andras Bacsai
e4aed185a2 fix: label generation 2023-10-18 12:48:29 +02:00
Andras Bacsai
dddbe40bbe fix dashboard ui on small screens 2023-10-18 11:35:36 +02:00
Andras Bacsai
59d6818f70 Merge pull request #1339 from coollabsio/next
v4.0.0-beta.93
2023-10-18 11:30:40 +02:00
Andras Bacsai
7678cd47df fix: add config_hash if its null (old deployments) 2023-10-18 11:26:01 +02:00
Andras Bacsai
b101fbacd4 fix: do not show configuration changed if config_hash is null 2023-10-18 11:22:56 +02:00
Andras Bacsai
a61a86dc3b feat: show if config is not applied 2023-10-18 11:20:40 +02:00
Andras Bacsai
0b3cde44c3 feat: able to customize docker labels on applications 2023-10-18 10:32:08 +02:00
TheH2SO4
618d5d837c [+] Template: Dokuwiki
🆕 **New Templates**:

-> ℹ️ **Dokuwiki**: A lightweight and easy-to-use wiki platform for creating and managing documentation and knowledge bases with simplicity and flexibility.
2023-10-18 10:01:37 +02:00
TheH2SO4
d234e8969d [+] Template: MeTube
🆕 **New Template**:

-> ℹ️ **MeTube**: A web GUI for youtube-dl with playlist support. It enables you to effortlessly download videos from YouTube and dozens of other sites.
2023-10-18 09:22:47 +02:00
TheH2SO4
1be77b3fea [+] Template: BabyBuddy
🆕 **New Template**:

-> ℹ️ **Heimdall**: Baby Buddy is an open-source web application that helps parents track their baby's daily activities, growth, and health with ease. It's a handy tool for new parents to keep a close eye on their little one's development.
2023-10-18 09:11:21 +02:00
Andras Bacsai
6b302ab786 Add restarting indicator to resources 2023-10-18 09:03:14 +02:00
TheH2SO4
5831dd6196 Merge pull request #1 from coollabsio/main
4.0.0-beta.92
2023-10-18 08:28:56 +02:00
Andras Bacsai
3c623f13e2 revert version 2023-10-17 20:54:54 +02:00
Andras Bacsai
da54c24e8d fix: setup:dev script & contribution guide 2023-10-17 20:54:26 +02:00
Andras Bacsai
1e39c3d5ab Merge pull request #1338 from coollabsio/next
v4.0.0-beta.92
2023-10-17 19:03:04 +02:00
Andras Bacsai
6071412986 fix: proxy start process 2023-10-17 19:00:23 +02:00
Andras Bacsai
ba7148206a Merge pull request #1336 from coollabsio/next
v4.0.0-beta.91
2023-10-17 15:41:30 +02:00
Andras Bacsai
59c5b22e6c fix: always start proxy if not NONE is selected 2023-10-17 15:40:47 +02:00
Andras Bacsai
be7f2ad9c4 ui: add helper to service domains 2023-10-17 15:34:20 +02:00
Andras Bacsai
62295ef573 Merge pull request #1335 from coollabsio/next
v4.0.0-beta.90
2023-10-17 14:45:26 +02:00
Andras Bacsai
ceb9fcf3b6 service: wordpress 2023-10-17 14:44:25 +02:00
Andras Bacsai
60282f7b6c fix: only include config.json if its exists and a file 2023-10-17 14:23:07 +02:00
Andras Bacsai
f14b0a3411 Merge pull request #1334 from coollabsio/next
v4.0.0-beta.89
2023-10-17 14:06:12 +02:00
Andras Bacsai
30af317bd9 fix: show docker build logs 2023-10-17 14:04:21 +02:00
Andras Bacsai
95faa1c3ad fix: noindex meta tag 2023-10-17 13:28:33 +02:00
TheH2SO4
423d31f227 Merge branch 'main' into main 2023-10-17 13:24:22 +02:00
TheH2SO4
fbb063030d [+] Templates: Heimdall, PairDrop and SnapDrop
🆕 **New Template**:

-> ℹ️ **Heimdall**: Heimdall is a self-hosted dashboard for managing and organizing your server applications, providing a centralized and efficient interface.
-> ℹ️ **PairDrop**: Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork.
-> ℹ️ **SnapDrop**: A self-hosted file-sharing service for secure and convenient file transfers, whether on a local network or the internet.
2023-10-17 13:22:37 +02:00
TheH2SO4
1968726cfe [+] Template: Code-Server
🆕 **New Template**:

-> ℹ️ **Code-Server**: Code-Server is a self-hosted, web-based code editor that enables remote coding and collaboration from any device, anywhere.
2023-10-17 12:53:44 +02:00
Andras Bacsai
fb280afe41 Merge pull request #1332 from coollabsio/next
v4.0.0-beta.88
2023-10-17 12:41:45 +02:00
Andras Bacsai
fd488a561a feat: use docker login credentials from server 2023-10-17 12:35:04 +02:00
Andras Bacsai
ab57a5d8ef Merge pull request #1331 from coollabsio/next
v4.0.0-beta.87
2023-10-17 12:14:03 +02:00
Andras Bacsai
f5ae222a6e fix: add internal domain names during build process 2023-10-17 11:23:49 +02:00
Andras Bacsai
5d95d8b79a fix: cancel any deployments + queue next 2023-10-17 11:10:33 +02:00
Andras Bacsai
fbb5f2ca2e fix: generate fqdn if you deleted a service app, but it requires fqdn 2023-10-17 10:37:39 +02:00
Andras Bacsai
16cbca36c1 add trademark policy 2023-10-17 10:37:26 +02:00
Andras Bacsai
24a578bedb Merge pull request #1327 from theh2so4/main
[+] Update
2023-10-17 10:31:25 +02:00
Andras Bacsai
36dc479772 fix: service status check is a bit better 2023-10-17 10:17:03 +02:00
Andras Bacsai
83d6e488e4 Merge pull request #1330 from seii/fix/raspbian-support
Add Raspbian support to install.sh
2023-10-17 09:50:57 +02:00
Seii
a4d358d512 Add Raspbian support to install.sh 2023-10-16 22:11:51 -06:00
TheH2SO4
c0c197101d [+] Update
💄 **Styling**:

-> ℹ️ **Alphabetical order**: Changed the order of the templates and set them on a alphabetical order.
-> ℹ️ **More accurate descriptions**: Created more accurate descriptions for the templates: `Appsmith, Appwrite, Fider, Ghost, Umami` and `Uptime Kuma`.
2023-10-15 23:48:33 +02:00
283 changed files with 9647 additions and 1987 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

@@ -6,13 +6,14 @@
You can ask for guidance anytime on our
[Discord server](https://coollabs.io/discord) in the `#contribution` channel.
## Code Contribution
## 1) Setup your development environment
### 1) Setup your development environment
- You need to have Docker Engine (or equivalent) [installed](https://docs.docker.com/engine/install/) on your system.
- For better DX, install [Spin](https://serversideup.net/open-source/spin/).
## 2) Set your environment variables
### 2) Set your environment variables
- Copy [.env.development.example](./.env.development.example) to .env.
@@ -23,7 +24,14 @@ You can ask for guidance anytime on our
- Run `./scripts/run setup:dev` - This will generate a secret key for you, delete any existing database layouts, migrate database to the new layout, and seed your database.
## 4) Start development
### 4) Start development
You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.
Mails are caught by Mailpit: `localhost:8025`
## New Service Contribution
Check out the docs [here](https://coolify.io/docs/how-to-add-a-service).

View File

@@ -4,42 +4,48 @@ Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Verc
It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything.
Image if you could have the ease of a cloud but with your own servers. That is **Coolify**.
Imagine if you could have the ease of a cloud but with your own servers. That is **Coolify**.
No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️
For more information, take a look at our landing page [here](https://coolify.io).
> If you are looking for previous (v3) version, it is [here](https://github.com/coollabsio/coolify/tree/v3).
# Donations
To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project.
https://coolify.io/sponsorships
Thank you so much!
# Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
You can easily attach your own servers, get all the automations, free email notifications, etc.
For more information & pricing, take a look at our landing page [here](https://coolify.io).
# Beta
## Why should I use the Cloud version?
The recommended way to use Coolify is to have one server for Coolify and one (or more) for the resources you are deploying. A server is around 4-5$/month.
The latest version (v4) is still in beta. That does not mean it is unstable. All the features that are available are stable enough be usable in real-life.
There are hundreds of people using it for managing their client's applications, freelancers, hobbyists, businesses.
By subscribing to the cloud version, you get the Coolify server for the same price, but with:
- High-availability
- Free email notifications
- Better support
- Less maintenance for you
# Installation
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
```
You can find the installation script source [here](./scripts/install.sh).
You can find the installation script [here](./scripts/install.sh).
## Support
# Support
Contact us [here](https://coolify.io/docs/contact).
## Recognitions
# Recognitions
<p>
<a href="https://news.ycombinator.com/item?id=26624341">
<img
style="width: 250px; height: 54px;" width="250" height="54"
@@ -47,14 +53,17 @@ Contact us [here](https://coolify.io/docs/contact).
src="https://hackernews-badge.vercel.app/api?id=26624341"
/>
</a>
</p>
<a href="https://www.producthunt.com/posts/coolify?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-coolify" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=338273&theme=light" alt="Coolify - An&#0032;open&#0045;source&#0032;&#0038;&#0032;self&#0045;hostable&#0032;Heroku&#0044;&#0032;Netlify&#0032;alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## 💰 Financial Contributors
<a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
# 💰 Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)]
### Organizations
## Organizations
Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)!
@@ -74,10 +83,10 @@ Support this project with your organization. Your logo will show up here with a
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
### Individuals
## Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
## Star History
# Star History
[![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date)

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,10 @@
namespace App\Actions\Database;
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 Lorisleiva\Actions\Concerns\AsAction;
@@ -11,15 +15,53 @@ class StartDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
{
$internalPort = null;
if ($database->getMorphClass()=== 'App\Models\StandaloneRedis') {
$internalPort = 6379;
} else if ($database->getMorphClass()=== 'App\Models\StandalonePostgresql') {
$internalPort = 5432;
$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 ($type === 'App\Models\StandalonePostgresql') {
$internalPort = 5432;
} else if ($type === 'App\Models\StandaloneMongodb') {
$internalPort = 27017;
} else if ($type === 'App\Models\StandaloneMysql') {
$internalPort = 3306;
} else if ($type === 'App\Models\StandaloneMariadb') {
$internalPort = 3306;
}
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
@@ -33,7 +75,7 @@ class StartDatabaseProxy
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:$internalPort;
proxy_pass $containerName:$internalPort;
}
}
EOF;
@@ -45,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' => [
@@ -72,9 +114,9 @@ class StartDatabaseProxy
]
],
'networks' => [
$database->destination->network => [
$network => [
'external' => true,
'name' => $database->destination->network,
'name' => $network,
'attachable' => true,
]
]
@@ -87,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} up --build -d >/dev/null",
], $database->destination->server);
"docker compose --project-directory {$configuration_dir} pull",
"docker compose --project-directory {$configuration_dir} up --build -d",
], $server);
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneMariadb;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartMariadb
{
use AsAction;
public StandaloneMariadb $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneMariadb $database)
{
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_mysql();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if ($this->database->destination->server->isDrainLogActivated()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->mariadb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
}
$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);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
$environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_USER'))->isEmpty()) {
$environment_variables->push("MARIADB_USER={$this->database->mariadb_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
}
return $environment_variables->all();
}
private function add_custom_mysql()
{
if (is_null($this->database->mariadb_conf)) {
return;
}
$filename = 'custom-config.cnf';
$content = $this->database->mariadb_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
}

View File

@@ -0,0 +1,190 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartMongodb
{
use AsAction;
public StandaloneMongodb $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneMongodb $database)
{
$this->database = $database;
$startCommand = "mongod";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_mongo_conf();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'command' => $startCommand,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'mongosh --eval "printjson(db.runCommand(\"ping\"))"'
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if ($this->database->destination->server->isDrainLogActivated()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->mongo_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/mongod.conf',
'target' => '/etc/mongo/mongod.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf';
}
$this->add_default_database();
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d',
'target' => '/docker-entrypoint-initdb.d',
'read_only' => true,
];
$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);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
}
return $environment_variables->all();
}
private function add_custom_mongo_conf()
{
if (is_null($this->database->mongo_conf)) {
return;
}
$filename = 'mongod.conf';
$content = $this->database->mongo_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
private function add_default_database()
{
$content = "db = db.getSiblingDB(\"{$this->database->mongo_initdb_database}\");db.createCollection('init_collection');db.createUser({user: \"{$this->database->mongo_initdb_root_username}\", pwd: \"{$this->database->mongo_initdb_root_password}\",roles: [{role:\"readWrite\",db:\"{$this->database->mongo_initdb_database}\"}]});";
$content_base64 = base64_encode($content);
$this->commands[] = "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d";
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js";
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneMysql;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartMysql
{
use AsAction;
public StandaloneMysql $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneMysql $database)
{
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_mysql();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p{$this->database->mysql_root_password}"],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if ($this->database->destination->server->isDrainLogActivated()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->mysql_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
}
$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);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
$environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_USER'))->isEmpty()) {
$environment_variables->push("MYSQL_USER={$this->database->mysql_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
}
return $environment_variables->all();
}
private function add_custom_mysql()
{
if (is_null($this->database->mysql_conf)) {
return;
}
$filename = 'custom-config.cnf';
$content = $this->database->mysql_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Actions\Database;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
@@ -17,7 +16,7 @@ class StartPostgresql
public array $init_scripts = [];
public string $configuration_dir;
public function handle(Server $server, StandalonePostgresql $database)
public function handle(StandalonePostgresql $database)
{
$this->database = $database;
$container_name = $this->database->uuid;
@@ -33,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' => [
@@ -65,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,
]
@@ -78,6 +79,16 @@ class StartPostgresql
]
]
];
if ($this->database->destination->server->isDrainLogActivated()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
@@ -97,14 +108,29 @@ 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, $server);
return remote_process($this->commands, $database->destination->server);
}
private function generate_local_persistent_volumes()
@@ -145,6 +171,9 @@ class StartPostgresql
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) {
$environment_variables->push("POSTGRES_USER={$this->database->postgres_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PGUSER'))->isEmpty()) {
$environment_variables->push("PGUSER={$this->database->postgres_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) {
$environment_variables->push("POSTGRES_PASSWORD={$this->database->postgres_password}");
@@ -169,4 +198,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

@@ -2,7 +2,6 @@
namespace App\Actions\Database;
use App\Models\Server;
use App\Models\StandaloneRedis;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
@@ -17,7 +16,7 @@ class StartRedis
public string $configuration_dir;
public function handle(Server $server, StandaloneRedis $database)
public function handle(StandaloneRedis $database)
{
$this->database = $database;
@@ -66,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,
]
@@ -79,6 +78,16 @@ class StartRedis
]
]
];
if ($this->database->destination->server->isDrainLogActivated()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
@@ -102,9 +111,11 @@ 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, $server);
return remote_process($this->commands, $database->destination->server);
}
private function generate_local_persistent_volumes()

View File

@@ -2,16 +2,18 @@
namespace App\Actions\Database;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Notifications\Application\StatusChanged;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
{
$server = $database->destination->server;
instant_remote_process(

View File

@@ -2,6 +2,10 @@
namespace App\Actions\Database;
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 Lorisleiva\Actions\Concerns\AsAction;
@@ -10,9 +14,13 @@ class StopDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $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();
if (isDev()) {
$settings->update([
'is_resale_license_active' => false,
'is_resale_license_active' => true,
]);
if (isDev()) {
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

@@ -2,41 +2,50 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Models\Activity;
class CheckProxy
{
use AsAction;
public function handle(Server $server)
public function handle(Server $server, $fromUI = false)
{
if (!$server->isProxyShouldRun()) {
throw new \Exception("Proxy should not run");
if ($fromUI) {
throw new \Exception("Proxy should not run. You selected the Custom Proxy.");
} else {
return false;
}
}
$status = getContainerStatus($server, 'coolify-proxy');
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
return 'OK';
return false;
}
$ip = $server->ip;
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$connection = @fsockopen($ip, '80');
$connection = @fsockopen($ip, '443');
$port80 = is_resource($connection) && fclose($connection);
$port443 = is_resource($connection) && fclose($connection);
ray($ip);
$connection80 = @fsockopen($ip, '80');
$connection443 = @fsockopen($ip, '443');
$port80 = is_resource($connection80) && fclose($connection80);
$port443 = is_resource($connection443) && fclose($connection443);
if ($port80) {
if ($fromUI) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
if ($port443) {
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>>");
if ($fromUI) {
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
return true;
}
}

View File

@@ -13,8 +13,6 @@ class StartProxy
public function handle(Server $server, bool $async = true): string|Activity
{
try {
CheckProxy::run($server);
$proxyType = $server->proxyType();
$commands = collect([]);
$proxy_path = get_proxy_path();

View File

@@ -0,0 +1,185 @@
<?php
namespace App\Actions\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Server;
class InstallLogDrain
{
use AsAction;
public function handle(Server $server, string $type)
{
try {
if ($type === 'none') {
$command = [
"echo 'Stopping old Fluent Bit'",
"docker rm -f coolify-log-drain || true",
];
return instant_remote_process($command, $server);
} else if ($type === 'newrelic') {
if (!$server->settings->is_logdrain_newrelic_enabled) {
throw new \Exception('New Relic log drain is not enabled.');
}
$config = base64_encode("
[SERVICE]
Flush 5
Daemon off
Tag container_logs
Log_Level debug
Parsers_File parsers.conf
[INPUT]
Name forward
Buffer_Chunk_Size 1M
Buffer_Max_Size 6M
[FILTER]
Name grep
Match *
Exclude log 127.0.0.1
[FILTER]
Name modify
Match *
Set server_name {$server->name}
[OUTPUT]
Name nrlogs
Match *
license_key \${LICENSE_KEY}
# https://log-api.eu.newrelic.com/log/v1 - EU
# https://log-api.newrelic.com/log/v1 - US
base_uri \${BASE_URI}
");
} else if ($type === 'highlight') {
if (!$server->settings->is_logdrain_highlight_enabled) {
throw new \Exception('Highlight log drain is not enabled.');
}
$config = base64_encode("
[SERVICE]
Flush 5
Daemon off
Log_Level debug
Parsers_File parsers.conf
[INPUT]
Name forward
tag \${HIGHLIGHT_PROJECT_ID}
Buffer_Chunk_Size 1M
Buffer_Max_Size 6M
[OUTPUT]
Name forward
Match *
Host otel.highlight.io
Port 24224
");
} else if ($type === 'axiom') {
if (!$server->settings->is_logdrain_axiom_enabled) {
throw new \Exception('Axiom log drain is not enabled.');
}
$config = base64_encode("
[SERVICE]
Flush 5
Daemon off
Log_Level debug
Parsers_File parsers.conf
[INPUT]
Name forward
Buffer_Chunk_Size 1M
Buffer_Max_Size 6M
[FILTER]
Name grep
Match *
Exclude log 127.0.0.1
[FILTER]
Name modify
Match *
Set server_name {$server->name}
[OUTPUT]
Name http
Match *
Host api.axiom.co
Port 443
URI /v1/datasets/\${AXIOM_DATASET_NAME}/ingest
# Authorization Bearer should be an API token
Header Authorization Bearer \${AXIOM_API_KEY}
compress gzip
format json
json_date_key _time
json_date_format iso8601
tls On
");
} else {
throw new \Exception('Unknown log drain type.');
}
$parsers = base64_encode("
[PARSER]
Name empty_line_skipper
Format regex
Regex /^(?!\s*$).+/
");
$compose = base64_encode("
services:
coolify-log-drain:
image: cr.fluentbit.io/fluent/fluent-bit:2.0
container_name: coolify-log-drain
command: -c /fluent-bit.conf
env_file:
- .env
volumes:
- ./fluent-bit.conf:/fluent-bit.conf
- ./parsers.conf:/parsers.conf
ports:
- 127.0.0.1:24224:24224
");
$readme = base64_encode('# New Relic Log Drain
This log drain is based on [Fluent Bit](https://fluentbit.io/) and New Relic Log Forwarder.
Files:
- `fluent-bit.conf` - configuration file for Fluent Bit
- `docker-compose.yml` - docker-compose file to run Fluent Bit
- `.env` - environment variables for Fluent Bit
');
$license_key = $server->settings->logdrain_newrelic_license_key;
$base_uri = $server->settings->logdrain_newrelic_base_uri;
$base_path = config('coolify.base_config_path');
$config_path = $base_path . '/log-drains';
$fluent_bit_config = $config_path . '/fluent-bit.conf';
$parsers_config = $config_path . '/parsers.conf';
$compose_path = $config_path . '/docker-compose.yml';
$readme_path = $config_path . '/README.md';
$command = [
"echo 'Saving configuration'",
"mkdir -p $config_path",
"echo '{$parsers}' | base64 -d > $parsers_config",
"echo '{$config}' | base64 -d > $fluent_bit_config",
"echo '{$compose}' | base64 -d > $compose_path",
"echo '{$readme}' | base64 -d > $readme_path",
"test -f $config_path/.env && rm $config_path/.env",
];
if ($type === 'newrelic') {
$add_envs_command = [
"echo LICENSE_KEY=$license_key >> $config_path/.env",
"echo BASE_URI=$base_uri >> $config_path/.env",
];
} else if ($type === 'highlight') {
$add_envs_command = [
"echo HIGHLIGHT_PROJECT_ID={$server->settings->logdrain_highlight_project_id} >> $config_path/.env",
];
} else if ($type === 'axiom') {
$add_envs_command = [
"echo AXIOM_DATASET_NAME={$server->settings->logdrain_axiom_dataset_name} >> $config_path/.env",
"echo AXIOM_API_KEY={$server->settings->logdrain_axiom_api_key} >> $config_path/.env",
];
}
$restart_command = [
"echo 'Stopping old Fluent Bit'",
"cd $config_path && docker rm -f coolify-log-drain || true",
"echo 'Starting Fluent Bit'",
"cd $config_path && docker compose up -d --remove-orphans",
];
$command = array_merge($command, $add_envs_command, $restart_command);
return instant_remote_process($command, $server);
} catch (\Throwable $e) {
return handleError($e);
}
}
}

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

@@ -3,19 +3,79 @@
namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\InstanceSettings;
use App\Models\Server;
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\Http;
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()
{
$this->alive();
$cleanup = $this->option('cleanup');
if ($cleanup) {
$this->cleanup_stucked_resources();
$this->cleanup_ssh();
}
$this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_helper_containers();
}
private function cleanup_stucked_helper_containers() {
$servers = Server::all();
foreach ($servers as $server) {
if ($server->isFunctional()) {
CleanupHelperContainersJob::dispatch($server);
}
}
}
private function alive()
{
$id = config('app.id');
$settings = InstanceSettings::get();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
echo "Skipping alive as do_not_track is enabled\n";
return;
}
try {
echo "I am alive!\n";
Http::get("https://get.coollabs.io/coolify/v4/alive?appId=$id");
} catch (\Throwable $e) {
echo "Error in alive: {$e->getMessage()}\n";
}
}
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
@@ -30,4 +90,167 @@ class Init extends Command
echo "Error: {$e->getMessage()}\n";
}
}
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 (!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 (!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 (!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 (!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 (!data_get($service, 'environment')) {
ray('Service without environment', $service->name);
$service->delete();
}
if (!data_get($service, 'server')) {
ray('Service without server', $service->name);
$service->delete();
}
if (!$service->destination()) {
ray('Service without destination', $service->name);
$service->delete();
}
}
} catch (\Throwable $e) {
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,6 +61,7 @@ class ResourcesDelete extends Command
foreach ($serversToDelete as $server) {
$toDelete = $servers->where('id', $server)->first();
if ($toDelete) {
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
@@ -69,6 +70,7 @@ class ResourcesDelete extends Command
$toDelete->delete();
}
}
}
private function deleteApplication()
{
$applications = Application::all();
@@ -82,8 +84,8 @@ class ResourcesDelete extends Command
);
foreach ($applicationsToDelete as $application) {
ray($application);
$toDelete = $applications->where('id', $application)->first();
if ($toDelete) {
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources? ");
if (!$confirmed) {
@@ -92,6 +94,7 @@ class ResourcesDelete extends Command
$toDelete->delete();
}
}
}
private function deleteDatabase()
{
$databases = StandalonePostgresql::all();
@@ -106,6 +109,7 @@ class ResourcesDelete extends Command
foreach ($databasesToDelete as $database) {
$toDelete = $databases->where('id', $database)->first();
if ($toDelete) {
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
@@ -114,6 +118,7 @@ class ResourcesDelete extends Command
$toDelete->delete();
}
}
}
private function deleteService()
{
$services = Service::all();
@@ -128,6 +133,7 @@ class ResourcesDelete extends Command
foreach ($servicesToDelete as $service) {
$toDelete = $services->where('id', $service)->first();
if ($toDelete) {
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
@@ -136,4 +142,5 @@ class ResourcesDelete extends Command
$toDelete->delete();
}
}
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Yaml\Yaml;
class ServicesGenerate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'services:generate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate service-templates.yaml based on /templates/compose directory';
/**
* Execute the console command.
*/
public function handle()
{
ray()->clearAll();
$files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
$files = array_filter($files, function ($file) {
return strpos($file, '.yaml') !== false;
});
$serviceTemplatesJson = [];
foreach ($files as $file) {
$parsed = $this->process_file($file);
if ($parsed) {
$name = data_get($parsed, 'name');
$parsed = data_forget($parsed, 'name');
$serviceTemplatesJson[$name] = $parsed;
}
}
$serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT);
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson);
}
private function process_file($file)
{
$serviceName = str($file)->before('.yaml')->value();
$content = file_get_contents(base_path("templates/compose/$file"));
// $this->info($content);
$ignore = collect(preg_grep('/^# ignore:/', explode("\n", $content)))->values();
if ($ignore->count() > 0) {
$ignore = (bool)str($ignore[0])->after('# ignore:')->trim()->value();
} else {
$ignore = false;
}
if ($ignore) {
$this->info("Ignoring $file");
return;
}
$this->info("Processing $file");
$documentation = collect(preg_grep('/^# documentation:/', explode("\n", $content)))->values();
if ($documentation->count() > 0) {
$documentation = str($documentation[0])->after('# documentation:')->trim()->value();
} else {
$documentation = 'https://coolify.io/docs';
}
$slogan = collect(preg_grep('/^# slogan:/', explode("\n", $content)))->values();
if ($slogan->count() > 0) {
$slogan = str($slogan[0])->after('# slogan:')->trim()->value();
} else {
$slogan = str($file)->headline()->value();
}
$env_file = collect(preg_grep('/^# env_file:/', explode("\n", $content)))->values();
if ($env_file->count() > 0) {
$env_file = str($env_file[0])->after('# env_file:')->trim()->value();
} else {
$env_file = null;
}
$tags = collect(preg_grep('/^# tags:/', explode("\n", $content)))->values();
if ($tags->count() > 0) {
$tags = str($tags[0])->after('# tags:')->trim()->explode(',')->map(function ($tag) {
return str($tag)->trim()->lower()->value();
})->values();
} else {
$tags = null;
}
$json = Yaml::parse($content);
$yaml = base64_encode(Yaml::dump($json, 10, 2));
$payload = [
'name' => $serviceName,
'documentation' => $documentation,
'slogan' => $slogan,
'compose' => $yaml,
'tags' => $tags,
];
if ($env_file) {
$env_file_content = file_get_contents(base_path("templates/compose/$env_file"));
$env_file_base64 = base64_encode($env_file_content);
$payload['envs'] = $env_file_base64;
}
return $payload;
}
}

View File

@@ -16,7 +16,7 @@ class SyncBunny extends Command
*
* @var string
*/
protected $signature = 'sync:bunny {--only-template} {--only-version}';
protected $signature = 'sync:bunny {--templates} {--release}';
/**
* The console command description.
@@ -31,8 +31,8 @@ class SyncBunny extends Command
public function handle()
{
$that = $this;
$only_template = $this->option('only-template');
$only_version = $this->option('only-version');
$only_template = $this->option('templates');
$only_version = $this->option('release');
$bunny_cdn = "https://cdn.coollabs.io";
$bunny_cdn_path = "coolify";
$bunny_cdn_storage_name = "coolcdn";

View File

@@ -2,12 +2,12 @@
namespace App\Console;
use App\Jobs\CheckResaleLicenseJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ContainerStatusJob;
use App\Jobs\PullHelperImageJob;
use App\Jobs\ServerStatusJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
@@ -19,30 +19,33 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
if (isDev()) {
// $schedule->job(new ContainerStatusJob(Server::find(0)))->everyTenMinutes()->onOneServer();
// $schedule->command('horizon:snapshot')->everyMinute();
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
// $schedule->job(new CheckResaleLicenseJob)->hourly();
// $schedule->job(new DockerCleanupJob)->everyOddHour();
// $this->instance_auto_update($schedule);
// $this->check_scheduled_backups($schedule);
// $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 cleanup_servers($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 DockerCleanupJob($server))->everyTenMinutes()->onOneServer();
$schedule->job(new PullHelperImageJob($server))->everyTenMinutes()->onOneServer();
}
}
private function check_resources($schedule)
@@ -53,6 +56,7 @@ class Kernel extends ConsoleKernel
$servers = Server::all();
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
}
}
@@ -68,7 +72,6 @@ class Kernel extends ConsoleKernel
}
private function check_scheduled_backups($schedule)
{
ray('check_scheduled_backups');
$scheduled_backups = ScheduledDatabaseBackup::all();
if ($scheduled_backups->isEmpty()) {
ray('no scheduled backups');

View File

@@ -4,6 +4,7 @@ namespace App\Exceptions;
use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Sentry\Laravel\Integration;
use Sentry\State\Scope;
@@ -40,6 +41,13 @@ class Handler extends ExceptionHandler
];
private InstanceSettings $settings;
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) {
return response()->json(['message' => $exception->getMessage()], 401);
}
return redirect()->guest($exception->redirectTo() ?? route('login'));
}
/**
* Register the exception handling callbacks for the application.
*/
@@ -47,6 +55,7 @@ class Handler extends ExceptionHandler
{
$this->reportable(function (Throwable $e) {
if (isDev()) {
ray($e);
return;
}
$this->settings = InstanceSettings::get();

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' => Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first()->environments
'environments' => []
]);
}
return response()->json([
'environments' => $project->environments
]);
}

View File

@@ -63,6 +63,12 @@ class ProjectController extends Controller
$database = create_standalone_postgresql($environment->id, $destination_uuid);
} else if ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid);
} else if ($type->value() === 'mongodb') {
$database = create_standalone_mongodb($environment->id, $destination_uuid);
} else if ($type->value() === 'mysql') {
$database = create_standalone_mysql($environment->id, $destination_uuid);
}else if ($type->value() === 'mariadb') {
$database = create_standalone_mariadb($environment->id, $destination_uuid);
}
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
@@ -70,12 +76,14 @@ class ProjectController extends Controller
'database_uuid' => $database->uuid,
]);
}
if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) {
if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) {
$oneClickServiceName = $type->after('one-click-service-')->value();
$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

@@ -33,7 +33,7 @@ class Index extends Component
public ?string $remoteServerUser = 'root';
public ?Server $createdServer = null;
public Collection|array $projects = [];
public Collection $projects;
public ?int $selectedExistingProject = null;
public ?Project $createdProject = null;
@@ -164,7 +164,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{
$this->validate([
'remoteServerName' => 'required',
'remoteServerHost' => 'required|ip',
'remoteServerHost' => 'required',
'remoteServerPort' => 'required|integer',
'remoteServerUser' => 'required',
]);
@@ -213,7 +213,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]);
$this->getProxyType();
} catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
// $this->dockerInstallationStarted = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
}
}

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

@@ -60,12 +60,16 @@ class DeploymentNavbar extends Component
$previous_logs[] = $new_log_entry;
$this->application_deployment_queue->update([
'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR),
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->application_deployment_queue->update([
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
queue_next_deployment($this->application);
}
}
}

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

@@ -3,12 +3,9 @@
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use App\Models\InstanceSettings;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Livewire\Component;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class General extends Component
{
@@ -22,6 +19,11 @@ class General extends Component
public string $git_branch;
public ?string $git_commit_sha = null;
public string $build_pack;
public ?string $ports_exposes = null;
public $customLabels;
public bool $labelsChanged = false;
public bool $isConfigurationChanged = false;
public bool $is_static;
public bool $is_git_submodules_enabled;
@@ -52,6 +54,8 @@ class General extends Component
'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -73,19 +77,53 @@ class General extends Component
'application.docker_registry_image_name' => 'Docker registry image name',
'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 updatedApplicationBuildPack(){
public function mount()
{
$this->ports_exposes = $this->application->ports_exposes;
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->application->isConfigurationChanged(true);
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if (is_null(data_get($this->application, 'custom_labels'))) {
$this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
} else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
}
if (data_get($this->application, 'settings')) {
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
$this->checkLabelUpdates();
}
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
$this->application->settings->is_static = $this->is_static = false;
$this->application->settings->save();
}
$this->submit();
}
public function checkLabelUpdates()
{
if (md5($this->application->custom_labels) !== md5(implode(",", generateLabelsApplication($this->application)))) {
$this->labelsChanged = true;
} else {
$this->labelsChanged = false;
}
}
public function instantSave()
{
// @TODO: find another way - if possible
$force_https = $this->application->settings->is_force_https_enabled;
$this->application->settings->is_static = $this->is_static;
if ($this->is_static) {
$this->application->ports_exposes = 80;
@@ -102,37 +140,43 @@ class General extends Component
$this->application->save();
$this->application->refresh();
$this->emit('success', 'Application settings updated!');
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if ($force_https !== $this->is_force_https_enabled) {
$this->resetDefaultLabels(false);
}
}
public function getWildcardDomain() {
public function getWildcardDomain()
{
$server = data_get($this->application, 'destination.server');
if ($server) {
$fqdn = generateFqdn($server, $this->application->uuid);
ray($fqdn);
$this->application->fqdn = $fqdn;
$this->application->save();
$this->emit('success', 'Application settings updated!');
$this->updatedApplicationFqdn();
}
}
public function mount()
public function resetDefaultLabels($showToaster = true)
{
if (data_get($this->application,'settings')) {
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
$this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
$this->ports_exposes = $this->application->ports_exposes;
$this->submit($showToaster);
}
public function submit()
public function updatedApplicationFqdn()
{
$this->resetDefaultLabels(false);
$this->emit('success', 'Labels reseted to default!');
}
public function submit($showToaster = true)
{
try {
$this->validate();
if (data_get($this->application,'build_pack') === 'dockerimage') {
if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false);
}
if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([
'application.docker_registry_image_name' => 'required',
'application.docker_registry_image_tag' => 'required',
@@ -156,10 +200,17 @@ class General extends Component
if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
}
if (gettype($this->customLabels) === 'string') {
$this->customLabels = str($this->customLabels)->replace(',', "\n");
}
$this->application->custom_labels = $this->customLabels->explode("\n")->implode(',');
$this->application->save();
$this->emit('success', 'Application settings updated!');
$showToaster && $this->emit('success', 'Application settings updated!');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
}
}
}

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

@@ -0,0 +1,159 @@
<?php
namespace App\Http\Livewire\Project;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class CloneProject extends Component
{
public string $project_uuid;
public string $environment_name;
public int $project_id;
public Project $project;
public $environments;
public $servers;
public ?Environment $environment = null;
public ?int $selectedServer = null;
public ?Server $server = null;
public $resources = [];
public string $newProjectName = '';
protected $messages = [
'selectedServer' => 'Please select a server.',
'newProjectName' => 'Please enter a name for the new project.',
];
public function mount($project_uuid)
{
$this->project_uuid = $project_uuid;
$this->project = Project::where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments->where('name', $this->environment_name)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()->servers;
$this->newProjectName = $this->project->name . ' (clone)';
}
public function render()
{
return view('livewire.project.clone-project');
}
public function selectServer($server_id)
{
$this->selectedServer = $server_id;
$this->server = $this->servers->where('id', $server_id)->first();
}
public function clone()
{
try {
$this->validate([
'selectedServer' => 'required',
'newProjectName' => 'required',
]);
$foundProject = Project::where('name', $this->newProjectName)->first();
if ($foundProject) {
throw new \Exception('Project with the same name already exists.');
}
$newProject = Project::create([
'name' => $this->newProjectName,
'team_id' => currentTeam()->id,
'description' => $this->project->description . ' (clone)',
]);
if ($this->environment->name !== 'production') {
$newProject->environments()->create([
'name' => $this->environment->name,
]);
}
$newEnvironment = $newProject->environments->where('name', $this->environment->name)->first();
// Clone Applications
$applications = $this->environment->applications;
$databases = $this->environment->databases();
$services = $this->environment->services;
foreach ($applications as $application) {
$uuid = (string)new Cuid2(7);
$newApplication = $application->replicate()->fill([
'uuid' => $uuid,
'fqdn' => generateFqdn($this->server, $uuid),
'status' => 'exited',
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
]);
$newApplication->save();
$environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([
'application_id' => $newApplication->id,
]);
$newEnvironmentVariable->save();
}
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newPersistentVolume = $volume->replicate()->fill([
'name' => $newApplication->uuid . '-' . str($volume->name)->afterLast('-'),
'resource_id' => $newApplication->id,
]);
$newPersistentVolume->save();
}
}
foreach ($databases as $database) {
$uuid = (string)new Cuid2(7);
$newDatabase = $database->replicate()->fill([
'uuid' => $uuid,
'status' => 'exited',
'started_at' => null,
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
]);
$newDatabase->save();
$environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [];
if ($database->type() === 'standalone-postgresql') {
$payload['standalone_postgresql_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone-redis') {
$payload['standalone_redis_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone-mongodb') {
$payload['standalone_mongodb_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone-mysql') {
$payload['standalone_mysql_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone-mariadb') {
$payload['standalone_mariadb_id'] = $newDatabase->id;
}
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
$newEnvironmentVariable->save();
}
}
foreach ($services as $service) {
$uuid = (string)new Cuid2(7);
$newService = $service->replicate()->fill([
'uuid' => $uuid,
'environment_id' => $newEnvironment->id,
'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', [
'project_uuid' => $newProject->uuid,
'environment_name' => $newEnvironment->name,
]);
} catch (\Exception $e) {
return handleError($e, $this);
}
}
}

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();
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

@@ -1,23 +0,0 @@
<?php
namespace App\Http\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackupExecution;
use Livewire\Component;
class BackupExecution extends Component
{
public ScheduledDatabaseBackupExecution $execution;
public function download()
{
}
public function delete(): void
{
delete_backup_locally($this->execution->filename, $this->execution->scheduledDatabaseBackup->database->destination->server);
$this->execution->delete();
$this->emit('success', 'Backup deleted successfully.');
$this->emit('refreshBackupExecutions');
}
}

View File

@@ -2,14 +2,59 @@
namespace App\Http\Livewire\Project\Database;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
class BackupExecutions extends Component
{
public $backup;
public $executions;
protected $listeners = ['refreshBackupExecutions'];
public $setDeletableBackup;
protected $listeners = ['refreshBackupExecutions', 'deleteBackup'];
public function deleteBackup($exeuctionId)
{
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
if (is_null($execution)) {
$this->emit('error', 'Backup execution not found.');
return;
}
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,6 +22,12 @@ class CreateScheduledBackup extends Component
'frequency' => 'Backup Frequency',
'save_s3' => 'Save to S3',
];
public function mount()
{
if ($this->s3s->count() > 0) {
$this->s3_storage_id = $this->s3s->first()->id;
}
}
public function submit(): void
{
@@ -43,9 +49,18 @@ class CreateScheduledBackup extends Component
];
if ($this->database->type() === 'standalone-postgresql') {
$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') {
$payload['databases_to_backup'] = $this->database->mariadb_database;
}
ScheduledDatabaseBackup::create($payload);
$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

@@ -2,6 +2,9 @@
namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase;
@@ -46,11 +49,19 @@ class Heading extends Component
public function start()
{
if ($this->database->type() === 'standalone-postgresql') {
$activity = StartPostgresql::run($this->database->destination->server, $this->database);
$activity = StartPostgresql::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
}
if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database->destination->server, $this->database);
} else if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
} else if ($this->database->type() === 'standalone-mongodb') {
$activity = StartMongodb::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
} else if ($this->database->type() === 'standalone-mysql') {
$activity = StartMysql::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
} else if ($this->database->type() === 'standalone-mariadb') {
$activity = StartMariadb::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace App\Http\Livewire\Project\Database\Mariadb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneMariadb;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneMariadb $database;
public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.mariadb_root_password' => 'required',
'database.mariadb_user' => 'required',
'database.mariadb_password' => 'required',
'database.mariadb_database' => 'required',
'database.mariadb_conf' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.mariadb_root_password' => 'Root Password',
'database.mariadb_user' => 'User',
'database.mariadb_password' => 'Password',
'database.mariadb_database' => 'Database',
'database.mariadb_conf' => 'MariaDB Configuration',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'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.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
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->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->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function render()
{
return view('livewire.project.database.mariadb.general');
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Http\Livewire\Project\Database\Mongodb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneMongodb;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneMongodb $database;
public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.mongo_conf' => 'nullable',
'database.mongo_initdb_root_username' => 'required',
'database.mongo_initdb_root_password' => 'required',
'database.mongo_initdb_database' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.mongo_conf' => 'Mongo Configuration',
'database.mongo_initdb_root_username' => 'Root Username',
'database.mongo_initdb_root_password' => 'Root Password',
'database.mongo_initdb_database' => 'Database',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'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;
}
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) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
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->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->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function render()
{
return view('livewire.project.database.mongodb.general');
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace App\Http\Livewire\Project\Database\Mysql;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneMysql;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneMysql $database;
public ?string $db_url = null;
public ?string $db_url_public = null;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.mysql_root_password' => 'required',
'database.mysql_user' => 'required',
'database.mysql_password' => 'required',
'database.mysql_database' => 'required',
'database.mysql_conf' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.mysql_root_password' => 'Root Password',
'database.mysql_user' => 'User',
'database.mysql_password' => 'Password',
'database.mysql_database' => 'Database',
'database.mysql_conf' => 'MySQL Configuration',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'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.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
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->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->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
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,14 +52,9 @@ class General extends Component
];
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
$this->db_url = $this->database->getDbUrl(true);
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
$this->db_url_public = $this->database->getDbUrl();
}
}
public function instantSave()
@@ -68,20 +66,24 @@ class General extends Component
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
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->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->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function save_init_script($script)
{
@@ -96,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();
@@ -140,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,7 +36,15 @@ class General extends Component
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function submit() {
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->redis_conf === "") {
@@ -56,16 +65,21 @@ class General extends Component
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
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->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->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
@@ -75,18 +89,6 @@ class General extends Component
$this->database->refresh();
}
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "redis://:{$this->database->redis_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/0";
} else {
$this->db_url = "redis://:{$this->database->redis_password}@{$this->database->uuid}:6379/0";
}
}
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
{
$this->parameters = get_route_parameters();
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

@@ -21,10 +21,10 @@ class DeleteEnvironment extends Component
'environment_id' => 'required|int',
]);
$environment = Environment::findOrFail($this->environment_id);
if ($environment->applications->count() > 0) {
return $this->emit('error', 'Environment has resources defined, please delete them first.');
}
if ($environment->isEmpty()) {
$environment->delete();
return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]);
}
return $this->emit('error', 'Environment has defined resources, please delete them first.');
}
}

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

@@ -21,14 +21,18 @@ class Select extends Component
public Collection|array $swarmDockers = [];
public array $parameters;
public Collection|array $services = [];
public Collection|array $allServices = [];
public bool $loadingServices = true;
public bool $loading = false;
public $environments = [];
public ?string $selectedEnvironment = null;
public ?string $existingPostgresqlUrl = null;
public ?string $search = null;
protected $queryString = [
'server',
'search'
];
public function mount()
@@ -41,6 +45,11 @@ class Select extends Component
$this->environments = Project::whereUuid($projectUuid)->first()->environments;
$this->selectedEnvironment = data_get($this->parameters, 'environment_name');
}
public function render()
{
$this->loadServices();
return view('livewire.project.new.select');
}
public function updatedSelectedEnvironment()
{
@@ -49,6 +58,7 @@ class Select extends Component
'environment_name' => $this->selectedEnvironment,
]);
}
// public function addExistingPostgresql()
// {
// try {
@@ -59,19 +69,28 @@ class Select extends Component
// }
// }
public function loadThings()
{
$this->loadServices();
$this->loadServers();
}
public function loadServices(bool $forceReload = false)
public function loadServices(bool $force = false)
{
try {
if ($forceReload) {
Cache::forget('services');
if (count($this->allServices) > 0 && !$force) {
if (!$this->search) {
$this->services = $this->allServices;
return;
}
$this->services = getServiceTemplates();
$this->services = $this->allServices->filter(function ($service, $key) {
$tags = collect(data_get($service, 'tags', []));
return str_contains(strtolower($key), strtolower($this->search)) || $tags->contains(function ($tag) {
return str_contains(strtolower($tag), strtolower($this->search));
});
});
} else {
$this->search = null;
$this->allServices = getServiceTemplates();
$this->services = $this->allServices->filter(function ($service, $key) {
return str_contains(strtolower($key), strtolower($this->search));
});;
$this->emit('success', 'Successfully loaded services.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {

View File

@@ -6,8 +6,8 @@ use Livewire\Component;
class ComposeModal extends Component
{
public string $raw;
public string $actual;
public ?string $raw = null;
public ?string $actual = null;
public function render()
{
return view('livewire.project.service.compose-modal');

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,10 +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();
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

@@ -6,16 +6,40 @@ 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 $service;
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;
@@ -78,6 +89,15 @@ class All extends Component
case 'standalone-redis':
$environment->standalone_redis_id = $this->resource->id;
break;
case 'standalone-mongodb':
$environment->standalone_mongodb_id = $this->resource->id;
break;
case 'standalone-mysql':
$environment->standalone_mysql_id = $this->resource->id;
break;
case 'standalone-mariadb':
$environment->standalone_mariadb_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;

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

@@ -13,10 +13,11 @@ class GetLogs extends Component
public Server $server;
public ?string $container = null;
public ?bool $streamLogs = false;
public ?bool $showTimeStamps = true;
public int $numberOfLines = 100;
public function doSomethingWithThisChunkOfOutput($output)
{
$this->outputs .= $output;
$this->outputs .= removeAnsiColors($output);
}
public function instantSave()
{
@@ -24,13 +25,24 @@ class GetLogs extends Component
public function getLogs($refresh = false)
{
if ($this->container) {
if ($this->showTimeStamps) {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
} else {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} {$this->container}");
}
if ($refresh) {
$this->outputs = '';
}
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

@@ -5,6 +5,9 @@ namespace App\Http\Livewire\Project\Shared;
use App\Models\Application;
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 Livewire\Component;
@@ -12,7 +15,7 @@ use Livewire\Component;
class Logs extends Component
{
public ?string $type = null;
public Application|StandalonePostgresql|Service|StandaloneRedis $resource;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
public Server $server;
public ?string $container = null;
public $parameters;
@@ -28,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');
}
@@ -37,10 +40,19 @@ class Logs extends Component
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
}
}
$this->resource = $resource;
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use Livewire\Component;
class Webhooks extends Component
{
public $resource;
public ?string $deploywebhook = null;
public ?string $githubManualWebhook = null;
public ?string $gitlabManualWebhook = null;
protected $rules = [
'resource.manual_webhook_secret_github' => 'nullable|string',
'resource.manual_webhook_secret_gitlab' => 'nullable|string',
];
public function saveSecret()
{
try {
$this->validate();
$this->resource->save();
$this->emit('success','Secret Saved.');
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function mount()
{
$this->deploywebhook = generateDeployWebhook($this->resource);
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
}
public function render()
{
return view('livewire.project.shared.webhooks');
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Livewire\Security;
use Livewire\Component;
class ApiTokens extends Component
{
public ?string $description = null;
public $tokens = [];
public function render()
{
return view('livewire.security.api-tokens');
}
public function mount()
{
$this->tokens = auth()->user()->tokens;
}
public function addNewToken()
{
try {
$this->validate([
'description' => 'required|min:3|max:255',
]);
$token = auth()->user()->createToken($this->description);
$this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function revoke(int $id)
{
$token = auth()->user()->tokens()->where('id', $id)->first();
$token->delete();
$this->tokens = auth()->user()->tokens;
}
}

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

@@ -0,0 +1,138 @@
<?php
namespace App\Http\Livewire\Server;
use App\Models\Server;
use Livewire\Component;
class LogDrains extends Component
{
public Server $server;
public $parameters = [];
protected $rules = [
'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean',
'server.settings.logdrain_newrelic_license_key' => 'required|string',
'server.settings.logdrain_newrelic_base_uri' => 'required|string',
'server.settings.is_logdrain_highlight_enabled' => 'required|boolean',
'server.settings.logdrain_highlight_project_id' => 'required|string',
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
'server.settings.logdrain_axiom_dataset_name' => 'required|string',
'server.settings.logdrain_axiom_api_key' => 'required|string',
];
protected $validationAttributes = [
'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain',
'server.settings.logdrain_newrelic_license_key' => 'New Relic license key',
'server.settings.logdrain_newrelic_base_uri' => 'New Relic base URI',
'server.settings.is_logdrain_highlight_enabled' => 'Highlight log drain',
'server.settings.logdrain_highlight_project_id' => 'Highlight project ID',
'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain',
'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name',
'server.settings.logdrain_axiom_api_key' => 'Axiom API key',
];
public function mount()
{
$this->parameters = get_route_parameters();
try {
$server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->first();
if (is_null($server)) {
return redirect()->route('server.all');
}
$this->server = $server;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function configureLogDrain()
{
try {
if ($this->server->settings->is_logdrain_newrelic_enabled) {
$this->server->logDrain('newrelic');
} else if ($this->server->settings->is_logdrain_highlight_enabled) {
$this->server->logDrain('highlight');
} else if ($this->server->settings->is_logdrain_axiom_enabled) {
$this->server->logDrain('axiom');
} else {
$this->server->logDrain('none');
$this->emit('serverRefresh');
$this->emit('success', 'Log drain service stopped.');
return;
}
$this->emit('serverRefresh');
$this->emit('success', 'Log drain service started successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave(string $type)
{
try {
$ok = $this->submit($type);
if (!$ok) {
return;
}
$this->configureLogDrain();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit(string $type)
{
try {
$this->resetErrorBag();
if ($type === 'newrelic') {
$this->validate([
'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean',
'server.settings.logdrain_newrelic_license_key' => 'required|string',
'server.settings.logdrain_newrelic_base_uri' => 'required|string',
]);
$this->server->settings->update([
'is_logdrain_highlight_enabled' => false,
'is_logdrain_axiom_enabled' => false,
]);
} else if ($type === 'highlight') {
$this->validate([
'server.settings.is_logdrain_highlight_enabled' => 'required|boolean',
'server.settings.logdrain_highlight_project_id' => 'required|string',
]);
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_axiom_enabled' => false,
]);
} else if ($type === 'axiom') {
$this->validate([
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
'server.settings.logdrain_axiom_dataset_name' => 'required|string',
'server.settings.logdrain_axiom_api_key' => 'required|string',
]);
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_highlight_enabled' => false,
]);
}
$this->server->settings->save();
$this->emit('success', 'Settings saved successfully.');
return true;
} catch (\Throwable $e) {
if ($type === 'newrelic') {
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
]);
} else if ($type === 'highlight') {
$this->server->settings->update([
'is_logdrain_highlight_enabled' => false,
]);
} else if ($type === 'axiom') {
$this->server->settings->update([
'is_logdrain_axiom_enabled' => false,
]);
}
handleError($e, $this);
return false;
}
}
public function render()
{
return view('livewire.server.log-drains');
}
}

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

@@ -39,7 +39,7 @@ class Deploy extends Component
public function checkProxy()
{
try {
CheckProxy::run($this->server);
CheckProxy::run($this->server, true);
$this->emit('startProxyPolling');
$this->emit('proxyChecked');
} catch (\Throwable $e) {

View File

@@ -34,7 +34,7 @@ class Status extends Component
}
$this->numberOfPolls++;
}
CheckProxy::run($this->server);
CheckProxy::run($this->server, true);
$this->emit('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') {
$this->polling = false;

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

@@ -64,21 +64,10 @@ class Create extends Component
}
$this->storage->team_id = currentTeam()->id;
$this->storage->testConnection();
$this->storage->is_usable = true;
$this->storage->save();
return redirect()->route('team.storages.show', $this->storage->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
private function test_s3_connection()
{
try {
$this->storage->testConnection();
return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -32,7 +32,7 @@ class Form extends Component
public function test_s3_connection()
{
try {
$this->storage->testConnection();
$this->storage->testConnection(shouldSave: true);
return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -53,10 +53,7 @@ class Form extends Component
{
$this->validate();
try {
$this->storage->testConnection();
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
$this->storage->save();
$this->emit('success', 'Storage settings saved.');
$this->test_s3_connection();
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -33,6 +33,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public $timeout = 3600;
public static int $batch_counter = 0;
private int $application_deployment_queue_id;
@@ -44,6 +46,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,10 +54,11 @@ 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 $git_type = null;
private string $container_name;
private string|null $currently_running_container_name = null;
private ?string $currently_running_container_name = null;
private string $basedir;
private string $workdir;
private ?string $build_pack = null;
@@ -67,14 +71,26 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose;
private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile';
private ?string $addHosts = null;
private ?string $buildTarget = null;
private $log_model;
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
private string $serverUser = 'root';
private string $serverUserHomeDir = '/root';
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)
{
ray()->clearScreen();
// ray()->clearScreen();
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id);
@@ -85,6 +101,9 @@ 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;
$this->git_type = data_get($this->application_deployment_queue, 'git_type');
$source = data_get($this->application, 'source');
if ($source) {
@@ -92,13 +111,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = $this->destination->server;
$this->serverUser = $this->server->user;
$this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
ray($this->basedir, $this->workdir);
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
@@ -107,11 +125,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 (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);
@@ -127,24 +150,61 @@ 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,
]);
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
$this->addHosts = $ips->map(function ($ip, $name) {
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);
// Check custom port
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
if (count($matches) === 1) {
$this->customPort = $matches[0];
$gitHost = str($this->application->git_repository)->before(':');
$gitRepo = str($this->application->git_repository)->after('/');
$this->customRepository = "$gitHost:$gitRepo";
} else {
$this->customRepository = $this->application->git_repository;
}
try {
if ($this->application->dockerfile) {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$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();
@@ -156,6 +216,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
} catch (Exception $e) {
ray($e);
$this->fail($e);
@@ -163,12 +224,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} finally {
if (isset($this->docker_compose_base64)) {
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml";
if ($this->pull_request_id !== 0) {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir"
],
[
"echo '{$this->docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml",
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
@@ -179,6 +244,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,
]
);
}
@@ -198,7 +271,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);
@@ -220,6 +293,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([]);
@@ -244,12 +359,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->prepare_builder_image();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile")
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir$this->dockerfile_location")
],
);
$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();
@@ -267,7 +380,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();
@@ -280,20 +393,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();
@@ -305,34 +412,32 @@ 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"
]);
if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->execute_remote_command([
"echo 'Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped...'"
"echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'",
]);
$this->generate_compose_file();
$this->rolling_update();
return;
}
if ($this->application->isConfigurationChanged()) {
$this->execute_remote_command([
"echo 'Configuration changed. Rebuilding image.'",
]);
}
}
$this->clone_repository();
$this->cleanup_git();
$this->generate_nixpacks_confs();
$this->generate_compose_file();
@@ -341,12 +446,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();
@@ -371,8 +493,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->execute_remote_command(
[
"echo 'Waiting for healthcheck to pass on the new version of your application.'"
],
]
);
if ($this->full_healthcheck_url) {
$this->execute_remote_command(
[
"echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
]
);
}
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
@@ -407,11 +536,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();
@@ -428,19 +556,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->stop_running_container();
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
}
private function prepare_builder_image()
{
$pull = "--pull=always";
$helperImage = config('coolify.helper_image');
$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}";
if ($this->dockerConfigFileExists === 'OK') {
$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 -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,
@@ -460,27 +591,58 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
],
);
}
private function clone_repository()
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(
[
"echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh")
],
[
$this->importing_git_repository(), "hidden" => true
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa")
],
[
executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git rev-parse HEAD"),
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"
],
);
$this->commit = $this->saved_outputs->get('git_commit_sha');
}
private function importing_git_repository()
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->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
],
[
$importCommands, "hidden" => true
]
);
}
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) {
@@ -495,42 +657,59 @@ 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) {
$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"));
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin $this->branch && git checkout $pr_branch_name"));
}
return $commands->implode(' && ');
}
}
if ($this->application->deploymentType() === 'deploy_key') {
$port = 22;
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
if (count($matches) === 1) {
$port = $matches[0];
$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($this->application->private_key->private_key);
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -p $port -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_repository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$private_key = base64_encode($private_key);
$git_clone_command_base = "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_base);
$commands = collect([
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_clone_command)
]);
if ($this->pull_request_id !== 0) {
ray($this->git_type);
if ($this->git_type === 'gitlab') {
$this->branch = "merge-requests/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && 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 fetch origin $this->branch && git checkout $pr_branch_name";
}
if ($this->git_type === 'github') {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && 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 fetch origin $this->branch && git checkout $pr_branch_name";
}
}
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
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(' && ');
}
}
@@ -578,7 +757,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}\"";
}
@@ -616,6 +795,35 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables($ports);
if (data_get($this->application, 'custom_labels')) {
$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' => [
@@ -624,7 +832,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'container_name' => $this->container_name,
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => generateLabelsApplication($this->application, $this->preview, $ports),
'labels' => $labels,
'expose' => $ports,
'networks' => [
$this->destination->network,
@@ -643,7 +851,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,
]
@@ -656,6 +864,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
]
];
if ($this->server->isDrainLogActivated()) {
$docker_compose['services'][$this->container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if ($this->application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck');
}
@@ -722,11 +940,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()) {
@@ -747,26 +971,75 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$health_check_port = $this->application->health_check_port;
}
if ($this->application->health_check_path) {
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}";
$generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null"
];
} else {
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/";
$generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/"
];
}
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()
{
if ($this->application->build_pack === 'static') {
$this->execute_remote_command([
"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.'",
]);
}
if ($this->application->settings->is_static) {
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 . .
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 {
listen 80;
listen [::]:80;
server_name localhost;
// real_ip_header X-Forwarded-For;
// proxy_set_header X-Real-IP \$remote_addr;
// proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
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;
}
}");
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
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}
@@ -791,59 +1064,82 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
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 --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 --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) {
$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 {
$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(
["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],
[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],
[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->build_pack === 'dockerimage') {
$this->execute_remote_command(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
} else {
$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 >/dev/null"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
}
}
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}\"");
}
}
@@ -853,7 +1149,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function add_build_env_variables_to_dockerfile()
{
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
]);
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
@@ -862,7 +1158,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/{$this->dockerfile_location}"),
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"),
"hidden" => true
]);
}
@@ -888,8 +1184,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

@@ -0,0 +1,40 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CleanupHelperContainersJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Server $server)
{
}
public function handle(): void
{
try {
ray('Cleaning up helper containers on ' . $this->server->name);
$containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
$containers = format_docker_command_output_to_json($containers);
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerId = data_get($container,'ID');
ray('Removing container ' . $containerId);
instant_remote_process(['docker container rm -f ' . $containerId], $this->server, false);
}
}
} catch (\Throwable $e) {
send_internal_notification('CleanupHelperContainersJob failed with error: ' . $e->getMessage());
ray($e->getMessage());
}
}
}

View File

@@ -2,105 +2,44 @@
namespace App\Jobs;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
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
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 120;
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)
{
if (isDev()) {
$this->handle();
}
}
public function handle()
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function handle(): void
{
// ray("checking server status for {$this->server->id}");
try {
ray("checking server status for {$this->server->id}");
// ray()->clearAll();
$serverUptimeCheckNumber = $this->server->unreachable_count;
$serverUptimeCheckNumberMax = 3;
// ray('checking # ' . $serverUptimeCheckNumber);
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
if ($this->server->unreachable_email_sent === false) {
ray('Server unreachable, sending notification...');
$this->server->team->notify(new Unreachable($this->server));
$this->server->update(['unreachable_email_sent' => true]);
}
$this->server->settings()->update([
'is_reachable' => false,
]);
$this->server->update([
'unreachable_count' => 0,
]);
return;
}
$result = $this->server->validateConnection();
if ($result) {
$this->server->settings()->update([
'is_reachable' => true,
]);
$this->server->update([
'unreachable_count' => 0,
]);
} else {
$serverUptimeCheckNumber++;
$this->server->settings()->update([
'is_reachable' => false,
]);
$this->server->update([
'unreachable_count' => $serverUptimeCheckNumber,
]);
return;
}
if (data_get($this->server, 'unreachable_email_sent') === true) {
ray('Server is reachable again, sending notification...');
$this->server->team->notify(new Revived($this->server));
$this->server->update(['unreachable_email_sent' => false]);
}
if (
data_get($this->server, 'settings.is_reachable') === false ||
data_get($this->server, 'settings.is_usable') === false
) {
$this->server->settings()->update([
'is_reachable' => true,
'is_usable' => true
]);
}
// $this->server->validateDockerEngine(true);
$this->server->checkServerRediness();
$containers = instant_remote_process(["docker container ls -q"], $this->server);
if (!$containers) {
return;
@@ -117,9 +56,16 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return data_get($value, 'Name') === '/coolify-proxy';
})->first();
if (!$foundProxyContainer) {
if ($this->server->isProxyShouldRun()) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
} else {
ray('Proxy could not be started.');
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
@@ -138,11 +84,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-')) {
$applicationId = data_get($labels, 'coolify.applicationId');
if ($applicationId) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
$applicationId = (int) Str::before($labelId, '-pr-');
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 +102,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 +178,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 +204,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 +229,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,16 +254,21 @@ 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) {
send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
return handleError($e);
handleError($e);
}
}
}

View File

@@ -2,10 +2,15 @@
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;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Notifications\Database\BackupFailed;
@@ -19,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
{
@@ -27,9 +33,10 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public ?Team $team = null;
public Server $server;
public ScheduledDatabaseBackup $backup;
public StandalonePostgresql $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;
@@ -43,10 +50,16 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
{
$this->backup = $backup;
$this->team = Team::find($backup->team_id);
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
{
@@ -61,30 +74,155 @@ 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;
}
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";
$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->mariadb_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->mariadb_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') {
$databasesToBackup = [$this->database->postgres_db];
} else if ($databaseType === 'standalone-mongodb') {
$databasesToBackup = ['*'];
} else if ($databaseType === 'standalone-mysql') {
$databasesToBackup = [$this->database->mysql_database];
} else if ($databaseType === 'standalone-mariadb') {
$databasesToBackup = [$this->database->mariadb_database];
} else {
return;
}
} else {
if ($databaseType === 'standalone-postgresql') {
// Format: db1,db2,db3
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
} else if ($databaseType === 'standalone-mongodb') {
// Format: db1:collection1,collection2|db2:collection3,collection4
$databasesToBackup = explode('|', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
ray($databasesToBackup);
} else if ($databaseType === 'standalone-mysql') {
// Format: db1,db2,db3
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
} else if ($databaseType === 'standalone-mariadb') {
// Format: db1,db2,db3
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
} else {
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";
}
@@ -92,6 +230,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$size = 0;
ray('Backing up ' . $database);
try {
if ($databaseType === 'standalone-postgresql') {
$this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
@@ -99,8 +238,46 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($databaseType === 'standalone-postgresql') {
$this->backup_standalone_postgresql($database);
} else if ($databaseType === 'standalone-mongodb') {
if ($database === '*') {
$database = 'all';
$databaseName = 'all';
} else {
if (str($database)->contains(':')) {
$databaseName = str($database)->before(':');
} else {
$databaseName = $database;
}
}
$this->backup_file = "/mongo-dump-$databaseName-" . Carbon::now()->timestamp . ".tar.gz";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $databaseName,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
$this->backup_standalone_mongodb($database);
} else if ($databaseType === 'standalone-mysql') {
$this->backup_file = "/mysql-dump-$database-" . Carbon::now()->timestamp . ".dmp";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
$this->backup_standalone_mysql($database);
} else if ($databaseType === 'standalone-mariadb') {
$this->backup_file = "/mariadb-dump-$database-" . Carbon::now()->timestamp . ".dmp";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
$this->backup_standalone_mariadb($database);
} else {
throw new \Exception('Unsupported database type');
}
$size = $this->calculate_size();
$this->remove_old_backups();
@@ -114,12 +291,14 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
'size' => $size,
]);
} catch (\Throwable $e) {
if ($this->backup_log) {
$this->backup_log->update([
'status' => 'failed',
'message' => $this->backup_output,
'size' => $size,
'filename' => null
]);
}
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e;
@@ -130,11 +309,43 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
throw $e;
}
}
private function backup_standalone_mongodb(string $databaseWithCollections): void
{
try {
$url = $this->database->getDbUrl(useInternal: true);
if ($databaseWithCollections === 'all') {
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
} else {
if (str($databaseWithCollections)->contains(':')) {
$databaseName = str($databaseWithCollections)->before(':');
$collectionsToExclude = str($databaseWithCollections)->after(':')->explode(',');
} else {
$databaseName = $databaseWithCollections;
$collectionsToExclude = collect();
}
$commands[] = "mkdir -p " . $this->backup_dir;
if ($collectionsToExclude->count() === 0) {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location";
}
}
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
throw $e;
}
}
private function backup_standalone_postgresql(string $database): void
{
try {
ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server);
@@ -149,7 +360,42 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
throw $e;
}
}
private function backup_standalone_mysql(string $database): void
{
try {
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
throw $e;
}
}
private function backup_standalone_mariadb(string $database): void
{
try {
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
throw $e;
}
}
private function add_to_backup_output($output): void
{
if ($this->backup_output) {
@@ -169,7 +415,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);
@@ -188,13 +434,13 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
// $region = $this->s3->region;
$bucket = $this->s3->bucket;
$endpoint = $this->s3->endpoint;
$this->s3->testConnection();
if (isDev()) {
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v coolify_coolify-data-dev:/data/coolify:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1";
$this->s3->testConnection(shouldSave: true);
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$network = $this->database->service->destination->network;
} else {
$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";
$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

@@ -7,6 +7,9 @@ use App\Actions\Database\StopDatabase;
use App\Actions\Service\StopService;
use App\Models\Application;
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 Illuminate\Bus\Queueable;
@@ -16,11 +19,11 @@ 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;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis $resource)
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource)
{
}
@@ -29,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()) {
@@ -41,15 +45,23 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted
case 'standalone-redis':
StopDatabase::run($this->resource);
break;
case 'standalone-mongodb':
StopDatabase::run($this->resource);
break;
case 'standalone-mysql':
StopDatabase::run($this->resource);
break;
case 'standalone-mariadb':
StopDatabase::run($this->resource);
break;
case 'service':
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,9 @@
namespace App\Jobs;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Notifications\Server\HighDiskUsage;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -11,68 +12,57 @@ 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 ?string $dockerRootFilesystem = null;
public $timeout = 300;
public ?int $usageBefore = null;
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
{
$queuedCount = 0;
$this->server->applications()->each(function ($application) use ($queuedCount) {
$count = data_get($application->deployments(), 'count', 0);
$queuedCount += $count;
});
if ($queuedCount > 0) {
ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange');
try {
$isInprogress = false;
$this->server->applications()->each(function ($application) use (&$isInprogress) {
if ($application->isDeploymentInprogress()) {
$isInprogress = true;
return;
}
try {
});
if ($isInprogress) {
throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
}
if (!$this->server->isFunctional()) {
return;
}
$this->dockerRootFilesystem = "/";
$this->usageBefore = $this->getFilesystemUsage();
$this->usageBefore = $this->server->getDiskUsage();
ray('Usage before: ' . $this->usageBefore);
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name)->color('orange');
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();
ray('Cleaning up ' . $this->server->name);
instant_remote_process(['docker image prune -af'], $this->server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server, false);
instant_remote_process(['docker builder prune -af'], $this->server, false);
$usageAfter = $this->server->getDiskUsage();
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;
}
}
private function getFilesystemUsage()
{
return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this->server, false);
}
}

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

@@ -0,0 +1,67 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use App\Notifications\Server\HighDiskUsage;
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 ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public ?int $disk_usage = null;
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function handle(): void
{
ray("checking server status for {$this->server->id}");
try {
$this->server->checkServerRediness();
$this->cleanup(notify: false);
} catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
handleError($e);
}
}
public function cleanup(bool $notify = false): void
{
$this->disk_usage = $this->server->getDiskUsage();
if ($this->disk_usage >= $this->server->settings->cleanup_after_percentage) {
if ($notify) {
if ($this->server->high_disk_usage_notification_sent) {
ray('high disk usage notification already sent');
return;
} else {
$this->server->high_disk_usage_notification_sent = true;
$this->server->save();
$this->server->team->notify(new HighDiskUsage($this->server, $this->disk_usage, $this->server->settings->cleanup_after_percentage));
}
} else {
DockerCleanupJob::dispatchSync($this->server);
$this->cleanup(notify: true);
}
} else {
$this->server->high_disk_usage_notification_sent = false;
$this->server->save();
}
}
}

View File

@@ -34,8 +34,11 @@ class Application extends BaseModel
static::deleting(function ($application) {
$application->settings()->delete();
$storages = $application->persistentStorages()->get();
$server = data_get($application, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false);
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$application->persistentStorages()->delete();
$application->environment_variables()->delete();
@@ -82,6 +85,18 @@ class Application extends BaseModel
);
}
public function gitWebhook(): Attribute
{
return Attribute::make(
get: function () {
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/settings/hooks";
}
return $this->git_repository;
}
);
}
public function gitCommits(): Attribute
{
return Attribute::make(
@@ -210,6 +225,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');
@@ -277,4 +300,31 @@ class Application extends BaseModel
}
return false;
}
public function isConfigurationChanged($save = false)
{
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
if ($this->pull_request_id === 0) {
$newConfigHash .= json_encode($this->environment_variables->all());
} else {
$newConfigHash .= json_encode($this->environment_variables_preview->all());
}
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
}

View File

@@ -7,14 +7,14 @@ use Illuminate\Database\Eloquent\Model;
class Environment extends Model
{
protected $fillable = [
'name',
'project_id',
];
public function can_delete_environment()
protected $guarded = [];
public function isEmpty()
{
return $this->applications()->count() == 0 && $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0;
return $this->applications()->count() == 0 &&
$this->redis()->count() == 0 &&
$this->postgresqls()->count() == 0 &&
$this->mongodbs()->count() == 0 &&
$this->services()->count() == 0;
}
public function applications()
@@ -30,12 +30,27 @@ class Environment extends Model
{
return $this->hasMany(StandaloneRedis::class);
}
public function mongodbs()
{
return $this->hasMany(StandaloneMongodb::class);
}
public function mysqls()
{
return $this->hasMany(StandaloneMysql::class);
}
public function mariadbs()
{
return $this->hasMany(StandaloneMariadb::class);
}
public function databases()
{
$postgresqls = $this->postgresqls;
$redis = $this->redis;
return $postgresqls->concat($redis);
$mongodbs = $this->mongodbs;
$mysqls = $this->mysqls;
$mariadbs = $this->mariadbs;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
}
public function project()

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
@@ -77,5 +82,4 @@ class EnvironmentVariable extends Model
set: fn (string $value) => Str::of($value)->trim(),
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
class PersonalAccessToken extends SanctumPersonalAccessToken
{
protected $fillable = [
'name',
'token',
'abilities',
'expires_at',
'team_id',
];
}

View File

@@ -18,7 +18,7 @@ class Project extends BaseModel
'project_id' => $project->id,
]);
Environment::create([
'name' => 'Production',
'name' => 'production',
'project_id' => $project->id,
]);
});
@@ -56,4 +56,16 @@ class Project extends BaseModel
{
return $this->hasManyThrough(StandaloneRedis::class, Environment::class);
}
public function mongodbs()
{
return $this->hasManyThrough(StandaloneMongodb::class, Environment::class);
}
public function mysqls()
{
return $this->hasMany(StandaloneMysql::class, Environment::class);
}
public function mariadbs()
{
return $this->hasMany(StandaloneMariadb::class, Environment::class);
}
}

View File

@@ -36,14 +36,13 @@ class S3Storage extends BaseModel
return "{$this->endpoint}/{$this->bucket}";
}
public function testConnection()
public function testConnection(bool $shouldSave = false)
{
try {
set_s3_target($this);
Storage::disk('custom-s3')->files();
$this->unusable_email_sent = false;
$this->is_usable = true;
return;
} catch (\Throwable $e) {
$this->is_usable = false;
if ($this->unusable_email_sent === false && is_transactional_emails_active()) {
@@ -65,7 +64,9 @@ class S3Storage extends BaseModel
throw $e;
} finally {
if ($shouldSave) {
$this->save();
}
}
}
}

View File

@@ -2,10 +2,15 @@
namespace App\Models;
use App\Actions\Server\InstallLogDrain;
use App\Actions\Server\InstallNewRelic;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Sleep;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
@@ -56,6 +61,8 @@ class Server extends BaseModel
public $casts = [
'proxy' => SchemalessAttributes::class,
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
];
protected $schemalessAttributes = [
'proxy',
@@ -109,11 +116,89 @@ class Server extends BaseModel
return $this->proxy->modelScope();
}
public function isEmpty()
public function isLocalhost()
{
$applications = $this->applications()->count() === 0;
$databases = $this->databases()->count() === 0;
if ($applications && $databases) {
return $this->ip === 'host.docker.internal' || $this->id === 0;
}
public function checkServerRediness()
{
$serverUptimeCheckNumber = $this->unreachable_count;
$serverUptimeCheckNumberMax = 5;
$currentTime = now()->timestamp;
$runtime5Minutes = 1 * 60;
// Run for 1 minutes max and check every 5 seconds
while ($currentTime + $runtime5Minutes > now()->timestamp) {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
if ($this->unreachable_notification_sent === false) {
ray('Server unreachable, sending notification...');
$this->team->notify(new Unreachable($this));
$this->update(['unreachable_notification_sent' => true]);
}
$this->settings()->update([
'is_reachable' => false,
]);
$this->update([
'unreachable_count' => 0,
]);
foreach ($this->applications() as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->databases() as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->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']);
}
}
throw new \Exception('Server is not reachable.');
}
$result = $this->validateConnection();
ray('validateConnection: ' . $result);
if (!$result) {
$serverUptimeCheckNumber++;
$this->update([
'unreachable_count' => $serverUptimeCheckNumber,
]);
Sleep::for(5)->seconds();
return;
}
$this->update([
'unreachable_count' => 0,
]);
if (data_get($this, 'unreachable_notification_sent') === true) {
ray('Server is reachable again, sending notification...');
$this->team->notify(new Revived($this));
$this->update(['unreachable_notification_sent' => false]);
}
if (
data_get($this, 'settings.is_reachable') === false ||
data_get($this, 'settings.is_usable') === false
) {
$this->settings()->update([
'is_reachable' => true,
'is_usable' => true
]);
}
break;
}
}
public function getDiskUsage()
{
return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
}
public function hasDefinedResources()
{
$applications = $this->applications()->count() > 0;
$databases = $this->databases()->count() > 0;
$services = $this->services()->count() > 0;
if ($applications || $databases || $services) {
return true;
}
return false;
@@ -122,9 +207,12 @@ class Server extends BaseModel
public function databases()
{
return $this->destinations()->map(function ($standaloneDocker) {
$postgresqls = $standaloneDocker->postgresqls;
$redis = $standaloneDocker->redis;
return $postgresqls->concat($redis);
$postgresqls = data_get($standaloneDocker, 'postgresqls', collect([]));
$redis = data_get($standaloneDocker, 'redis', collect([]));
$mongodbs = data_get($standaloneDocker, 'mongodbs', collect([]));
$mysqls = data_get($standaloneDocker, 'mysqls', collect([]));
$mariadbs = data_get($standaloneDocker, 'mariadbs', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
})->flatten();
}
public function applications()
@@ -144,7 +232,7 @@ class Server extends BaseModel
if (isDev()) {
return '127.0.0.1';
}
if ($this->ip === 'host.docker.internal') {
if ($this->isLocalhost()) {
return base_ip();
}
return $this->ip;
@@ -193,28 +281,36 @@ class Server extends BaseModel
}
public function isProxyShouldRun()
{
$shouldRun = false;
if ($this->proxyType() === ProxyTypes::NONE->value) {
return false;
}
foreach ($this->applications() as $application) {
if (data_get($application, 'fqdn')) {
$shouldRun = true;
break;
// foreach ($this->applications() as $application) {
// if (data_get($application, 'fqdn')) {
// $shouldRun = true;
// break;
// }
// }
// ray($this->services()->get());
// if ($this->id === 0) {
// $settings = InstanceSettings::get();
// if (data_get($settings, 'fqdn')) {
// $shouldRun = true;
// }
// }
return true;
}
}
if ($this->id === 0) {
$settings = InstanceSettings::get();
if (data_get($settings, 'fqdn')) {
$shouldRun = true;
}
}
return $shouldRun;
public function logDrain($type)
{
InstallLogDrain::run($this, $type);
}
public function isFunctional()
{
return $this->settings->is_reachable && $this->settings->is_usable;
}
public function isDrainLogActivated() {
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled;
}
public function validateConnection()
{
$uptime = instant_remote_process(['uptime'], $this, false);
@@ -256,7 +352,8 @@ class Server extends BaseModel
$this->settings->save();
return true;
}
public function validateCoolifyNetwork() {
public function validateCoolifyNetwork()
{
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, 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,34 +22,254 @@ 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();
$data = collect([]);
if ($postgres_user) {
$data = $data->merge([
'User' => [
'key' => data_get($postgres_user, 'key'),
'value' => data_get($postgres_user, 'value'),
'rules' => 'required',
],
]);
}
if ($postgres_password) {
$data = $data->merge([
'Password' => [
'key' => data_get($postgres_password, 'key'),
'value' => data_get($postgres_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($postgres_db_name) {
$data = $data->merge([
'Database Name' => [
'key' => data_get($postgres_db_name, 'key'),
'value' => data_get($postgres_db_name, 'value'),
'rules' => 'required',
],
]);
}
$fields->put('PostgreSQL', $data->toArray());
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();
$data = collect([]);
if ($mysql_user) {
$data = $data->merge([
'User' => [
'key' => data_get($mysql_user, 'key'),
'value' => data_get($mysql_user, 'value'),
'rules' => 'required',
],
]);
}
if ($mysql_password) {
$data = $data->merge([
'Password' => [
'key' => data_get($mysql_password, 'key'),
'value' => data_get($mysql_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($mysql_root_password) {
$data = $data->merge([
'Root Password' => [
'key' => data_get($mysql_root_password, 'key'),
'value' => data_get($mysql_root_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($mysql_db_name) {
$data = $data->merge([
'Database Name' => [
'key' => data_get($mysql_db_name, 'key'),
'value' => data_get($mysql_db_name, 'value'),
'rules' => 'required',
],
]);
}
$fields->put('MySQL', $data->toArray());
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();
$data = collect([]);
if ($mariadb_user) {
$data = $data->merge([
'User' => [
'key' => data_get($mariadb_user, 'key'),
'value' => data_get($mariadb_user, 'value'),
'rules' => 'required',
],
]);
}
if ($mariadb_password) {
$data = $data->merge([
'Password' => [
'key' => data_get($mariadb_password, 'key'),
'value' => data_get($mariadb_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($mariadb_root_password) {
$data = $data->merge([
'Root Password' => [
'key' => data_get($mariadb_root_password, 'key'),
'value' => data_get($mariadb_root_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($mariadb_db_name) {
$data = $data->merge([
'Database Name' => [
'key' => data_get($mariadb_db_name, 'key'),
'value' => data_get($mariadb_db_name, 'value'),
'rules' => 'required',
],
]);
}
$fields->put('MariaDB', $data->toArray());
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 = Cache::get('services', []);
$services = getServiceTemplates();
$service = data_get($services, Str::of($this->name)->beforeLast('-')->value, []);
return data_get($service, 'documentation', config('constants.docs.base_url'));
}
@@ -90,6 +309,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 +338,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 +477,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,8 +616,9 @@ class Service extends BaseModel
$key = Str::of($variableName);
$value = Str::of($variable);
}
// TODO: here is the problem
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew) {
if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
$fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
if (substr_count($key->value(), '_') === 3) {
@@ -438,15 +674,31 @@ class Service extends BaseModel
'service_id' => $this->id,
])->first();
if ($value->startsWith('SERVICE_')) {
// 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 +714,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,14 +785,28 @@ class Service extends BaseModel
}
// Add labels to the service
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) {
if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($fqdns, true));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
}
}
if ($this->server->isDrainLogActivated()) {
data_set($service, 'logging', [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
]);
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE);

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 ($this->service->server->isLocalhost() || 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

@@ -15,10 +15,23 @@ class StandaloneDocker extends BaseModel
{
return $this->morphMany(StandalonePostgresql::class, 'destination');
}
public function redis()
{
return $this->morphMany(StandaloneRedis::class, 'destination');
}
public function mongodbs()
{
return $this->morphMany(StandaloneMongodb::class, 'destination');
}
public function mysqls()
{
return $this->morphMany(StandaloneMysql::class, 'destination');
}
public function mariadbs()
{
return $this->morphMany(StandaloneMariadb::class, 'destination');
}
public function server()
{
@@ -30,8 +43,18 @@ class StandaloneDocker extends BaseModel
return $this->morphMany(Service::class, 'destination');
}
public function databases()
{
$postgresqls = $this->postgresqls;
$redis = $this->redis;
$mongodbs = $this->mongodbs;
$mysqls = $this->mysqls;
$mariadbs = $this->mariadbs;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
}
public function attachedTo()
{
return $this->applications?->count() > 0 || $this->databases?->count() > 0;
return $this->applications?->count() > 0 || $this->databases()->count() > 0;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class StandaloneMariadb extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'mariadb_password' => 'encrypted',
];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'mariadb-data-' . $database->uuid,
'mount_path' => '/var/lib/mysql',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
$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 type(): string
{
return 'standalone-mariadb';
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function getDbUrl(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
} else {
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}";
}
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

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