Compare commits

...

92 Commits

Author SHA1 Message Date
Andras Bacsai
ec376b2e47 Merge pull request #864 from coollabsio/next
v3.12.13
2023-01-18 19:00:03 +01:00
Andras Bacsai
a176562ad0 fix: secrets 2023-01-18 18:51:03 +01:00
Andras Bacsai
becf37b676 Merge pull request #858 from coollabsio/next
v3.12.12
2023-01-17 12:33:51 +01:00
Andras Bacsai
9b5efab8f8 fix: grpc 2023-01-17 11:51:53 +01:00
Andras Bacsai
e98a8ba599 traefik dashbord in dev 2023-01-17 11:12:52 +01:00
Andras Bacsai
7ddac50008 feat: http + h2c paralel 2023-01-17 11:12:42 +01:00
Andras Bacsai
9837ae359f feat: init h2c (http2/grpc) support 2023-01-17 10:35:04 +01:00
Andras Bacsai
710a829dcb version++ 2023-01-17 10:00:50 +01:00
Andras Bacsai
ccd84fa454 fix: build args docker compose 2023-01-17 10:00:27 +01:00
Andras Bacsai
335b36d3a9 Merge pull request #857 from zek/patch-2
Fix docker-compose build args
2023-01-17 09:26:12 +01:00
Talha Zekeriya Durmuş
2be30fae00 Handle string build parameter 2023-01-17 02:11:06 +01:00
Talha Zekeriya Durmuş
db5cd21884 Fix docker-compose build args 2023-01-17 02:04:01 +01:00
Andras Bacsai
bfd3020031 Merge pull request #853 from coollabsio/next
v3.12.11
2023-01-16 12:44:04 +01:00
Andras Bacsai
344c36997a fix: public gh repo reload compose 2023-01-16 12:36:59 +01:00
Andras Bacsai
dfd9272b70 version ++ 2023-01-16 12:17:48 +01:00
Andras Bacsai
359f4520f5 test template + tags during dev 2023-01-16 11:45:45 +01:00
Andras Bacsai
aecf014f4e Merge pull request #843 from zek/soketi-logo
Add Soketi Logo
2023-01-16 11:10:48 +01:00
Andras Bacsai
d2a89ddf84 Merge pull request #844 from zek/repman-logo
Add Repman logo
2023-01-16 11:10:35 +01:00
Andras Bacsai
c01fe153ae Merge pull request #847 from zek/mattermost
Add Matermost Logo
2023-01-16 11:10:14 +01:00
Andras Bacsai
4f4a838799 update templates+tags 2023-01-16 10:58:44 +01:00
Andras Bacsai
ac6f2567eb fix: build env variables with docker compose 2023-01-16 10:42:23 +01:00
Andras Bacsai
05a5816ac6 fix: do not cleanup compose applications as unconfigured 2023-01-16 10:22:14 +01:00
Andras Bacsai
9c8f6e9195 fix: delete apps with previews 2023-01-16 10:16:49 +01:00
Andras Bacsai
2fd001f6d2 fix: docker log sequence 2023-01-16 10:06:41 +01:00
Andras Bacsai
d641d32413 fix: compose file location 2023-01-16 09:48:15 +01:00
Andras Bacsai
18064ef6a2 fixes related to docker-compose 2023-01-16 09:44:08 +01:00
Andras Bacsai
5cb9216add wip: trpc 2023-01-13 15:50:20 +01:00
Andras Bacsai
91c36dc810 wip: trpc 2023-01-13 15:24:43 +01:00
Andras Bacsai
6efb02fa32 wip: trpc 2023-01-13 15:21:54 +01:00
Andras Bacsai
97313e4180 wip: trpc 2023-01-13 14:54:21 +01:00
Andras Bacsai
568ab24fd9 wip: trpc 2023-01-13 14:17:36 +01:00
Talha Zekeriya Durmuş
5a745efcd3 Add Matermost Logo 2023-01-13 02:38:33 +01:00
Andras Bacsai
c651570e62 wip: trpc 2023-01-12 16:50:17 +01:00
Andras Bacsai
8980598085 wip: trpc 2023-01-12 16:43:41 +01:00
Talha Zekeriya Durmuş
c07c742feb Add Repman logo 2023-01-12 01:45:13 +01:00
Talha Zekeriya Durmuş
1053abb9a9 Add Soketi Logo 2023-01-12 01:41:35 +01:00
Andras Bacsai
2c9e57cbb1 Merge pull request #841 from coollabsio/next
v3.12.10
2023-01-11 11:44:22 +01:00
Andras Bacsai
c6eaa2c8a6 update packages in api 2023-01-11 11:35:57 +01:00
Andras Bacsai
5ab5e913ee Merge pull request #840 from zek/patch-1
Fix: add missing variables
2023-01-11 11:33:17 +01:00
Talha Zekeriya Durmuş
cea53ca476 Fix: add missing variables 2023-01-11 11:12:44 +01:00
Andras Bacsai
58af09114b Merge pull request #834 from coollabsio/next
v3.12.9
2023-01-11 11:00:56 +01:00
Andras Bacsai
c4c0417e2d new pocketbase 2023-01-11 10:55:55 +01:00
Andras Bacsai
74f90e6947 Merge pull request #838 from zek/patch-1
Add Build Time Secrets for Laravel
2023-01-11 10:12:59 +01:00
Andras Bacsai
ad5c339780 fix 2023-01-11 10:11:32 +01:00
Andras Bacsai
305823db00 fix: secrets 2023-01-11 09:29:59 +01:00
Talha Zekeriya Durmuş
baf58b298f Add Build Time Secrets 2023-01-11 01:43:43 +01:00
Andras Bacsai
c37367d018 add directus 2023-01-10 15:30:10 +01:00
Andras Bacsai
1c98796e64 new templates + tags + dev mode updated 2023-01-10 13:24:04 +01:00
Andras Bacsai
e686d9a6ea add lock file 2023-01-10 13:01:37 +01:00
Andras Bacsai
a1936b9d59 update jsonwebtoken 2023-01-10 13:01:03 +01:00
Andras Bacsai
834f9c9337 template updates 2023-01-10 13:01:03 +01:00
Andras Bacsai
615f8cfd3b feat: handle invite_only plausible analytics 2023-01-10 13:01:03 +01:00
Andras Bacsai
8ed134105f remove console.log 2023-01-10 13:01:03 +01:00
Andras Bacsai
5d6169b270 Merge pull request #781 from kaname-png/libretranslate
feat(ui): add libretranslate service icon
2023-01-10 12:57:05 +01:00
Andras Bacsai
e83de8b938 fix: local images for reverting 2023-01-10 12:24:22 +01:00
Andras Bacsai
ee55e039b2 Merge pull request #798 from hyddeos/main
fix the console error on Documentation hover
2023-01-10 11:52:57 +01:00
Andras Bacsai
086dd89144 fix: temporary disable dns check with dns servers 2023-01-10 11:50:41 +01:00
Andras Bacsai
68e5d4dd2c fix: doc link 2023-01-10 11:35:10 +01:00
Andras Bacsai
55a35c6bec fix: remove prefetches 2023-01-10 11:31:44 +01:00
Andras Bacsai
d09b4885fe Merge pull request #784 from hyddeos/tool-tip
Change color for Tooltip on hover
2023-01-10 11:29:54 +01:00
Andras Bacsai
a46773e6d8 Merge branch 'next' into tool-tip 2023-01-10 11:29:36 +01:00
Andras Bacsai
a422d0220c fix: add documentation link again 2023-01-10 11:27:43 +01:00
Andras Bacsai
e5eba8430a Merge pull request #783 from hyddeos/doc-in-mob-menu
Add link to the Documentation in the mobile menu
2023-01-10 11:26:42 +01:00
Andras Bacsai
3d235dc316 Merge pull request #794 from TetrisIQ/main
feat: adding icon for whoogle
2023-01-10 11:19:42 +01:00
Andras Bacsai
80d3b4be8c Merge pull request #825 from MrSquaare/feature/openblocks-service
feat: add Openblocks icon
2023-01-10 11:19:13 +01:00
Andras Bacsai
fe8b7480df Merge pull request #836 from coollabsio/feat/git-source-custom-user
fix: custom gitlab git user
2023-01-10 11:17:42 +01:00
Andras Bacsai
cebfc3aaa0 Merge pull request #804 from titouanmathis/feat/git-source-custom-user
feat(git-source): Add support for custom SSH user for GitLab self-hosted
2023-01-10 11:17:01 +01:00
Andras Bacsai
f778b5a12d fix: custom gitlab git user 2023-01-10 11:15:21 +01:00
Andras Bacsai
2244050160 Merge pull request #816 from Yarmeli/main
[Bug] Fixed issue with docker-compose not loading for Gitlab instances
2023-01-10 10:56:03 +01:00
Andras Bacsai
9284e42b62 fix: $ sign in secrets 2023-01-10 10:52:40 +01:00
Andras Bacsai
ee40120496 fix: read-only iam 2023-01-10 10:26:11 +01:00
Andras Bacsai
30cd2149ea fix: read-only permission 2023-01-10 10:15:03 +01:00
Andras Bacsai
395df36d57 chore: version++ 2023-01-10 09:57:27 +01:00
Andras Bacsai
79597ea0e5 fix: parsing secrets 2023-01-10 09:57:01 +01:00
Guillaume Bonnet
283f39270a feat: add Openblocks icon 2023-01-05 12:26:50 +00:00
Andras Bacsai
7d892bb19d esbuild 2022-12-29 22:33:31 +01:00
Yarmeli
a025f124f3 Updated index.svelte with the same changes from +page.svelte 2022-12-29 19:00:11 +00:00
Yarmeli
84f7287bf8 Fixed issue unable to find the docker compose file 2022-12-29 18:54:54 +00:00
Andras Bacsai
a58544b502 Merge pull request #813 from coollabsio/next
v3.12.8
2022-12-27 21:10:41 +01:00
Andras Bacsai
4d26175ebe omg, what have I done.. 2022-12-27 21:02:04 +01:00
Andras Bacsai
78f0e6ff6b Update package.json 2022-12-27 14:22:49 +01:00
Andras Bacsai
3af97af634 Update common.ts 2022-12-27 14:22:29 +01:00
Andras Bacsai
2c2663c8a4 Update common.ts 2022-12-27 14:20:19 +01:00
Titouan Mathis
2391850218 Add support for custom SSH user for GitLab self-hosted 2022-12-21 15:10:51 +01:00
Eric
af548e6ef8 Change of link "rel" to "external"
To prevent hover console-error like on the documentations-icon in the desktop mode
2022-12-19 20:33:22 +01:00
hyddeos
ed24a9c990 fix the consol error on documentation hover 2022-12-19 20:02:24 +01:00
Alex
0d51b04d79 feat: adding icon for whoogle 2022-12-18 14:13:49 +01:00
hyddeos
379b1de64f change color for Tooltip on hover 2022-12-12 18:24:13 +01:00
hyddeos
f3ff324925 fixed size on icon 2022-12-12 17:50:35 +01:00
hyddeos
0f2160222f Add Documents link to Mobile-Menu 2022-12-12 17:45:04 +01:00
hyddeos
ce3750c51c Add link document to Mobile-menu 2022-12-12 17:38:45 +01:00
Kaname
72a7ea6e91 feat(ui): add libretranslate service icon 2022-12-12 15:48:23 +00:00
176 changed files with 22481 additions and 2658 deletions

View File

@@ -27,7 +27,7 @@ jobs:
context: others/pocketbase/ context: others/pocketbase/
platforms: linux/arm64 platforms: linux/arm64
push: true push: true
tags: coollabsio/pocketbase:0.10.2-arm64 tags: coollabsio/pocketbase:0.11.0-arm64
amd64: amd64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -48,7 +48,7 @@ jobs:
context: others/pocketbase/ context: others/pocketbase/
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: coollabsio/pocketbase:0.10.2-amd64 tags: coollabsio/pocketbase:0.11.0-amd64
aarch64: aarch64:
runs-on: [self-hosted, arm64] runs-on: [self-hosted, arm64]
steps: steps:
@@ -69,7 +69,7 @@ jobs:
context: others/pocketbase/ context: others/pocketbase/
platforms: linux/aarch64 platforms: linux/aarch64
push: true push: true
tags: coollabsio/pocketbase:0.10.2-aarch64 tags: coollabsio/pocketbase:0.11.0-aarch64
merge-manifest: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [amd64, arm64, aarch64] needs: [amd64, arm64, aarch64]
@@ -87,5 +87,5 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create & publish manifest - name: Create & publish manifest
run: | run: |
docker manifest create coollabsio/pocketbase:0.10.2 --amend coollabsio/pocketbase:0.10.2-amd64 --amend coollabsio/pocketbase:0.10.2-arm64 --amend coollabsio/pocketbase:0.10.2-aarch64 docker manifest create coollabsio/pocketbase:0.11.0 --amend coollabsio/pocketbase:0.11.0-amd64 --amend coollabsio/pocketbase:0.11.0-arm64 --amend coollabsio/pocketbase:0.11.0-aarch64
docker manifest push coollabsio/pocketbase:0.10.2 docker manifest push coollabsio/pocketbase:0.11.0

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ dist
apps/api/db/*.db apps/api/db/*.db
apps/api/db/migration.db-journal apps/api/db/migration.db-journal
apps/api/core* apps/api/core*
apps/server/build
apps/backup/backups/* apps/backup/backups/*
!apps/backup/backups/.gitkeep !apps/backup/backups/.gitkeep
/logs /logs

4
apps/api/.gitignore vendored
View File

@@ -8,4 +8,6 @@ package
!.env.example !.env.example
dist dist
dev.db dev.db
client client
testTemplate.yaml
testTags.json

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,229 @@
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: "0.10.2" defaultVersion: "9.22"
documentation: https://docs.directus.io/getting-started/introduction.html
type: directus-postgresql
name: Directus
subname: (PostgreSQL)
description: >-
Directus is a free and open-source headless CMS framework for managing custom SQL-based databases.
labels:
- CMS
- headless
services:
$$id:
name: Directus
depends_on:
- $$id-postgresql
- $$id-redis
image: directus/directus:$$core_version
volumes:
- $$id-uploads:/directus/uploads
- $$id-database:/directus/database
- $$id-extensions:/directus/extensions
environment:
- KEY=$$secret_key
- SECRET=$$secret_secret
- DB_CLIENT=pg
- DB_CONNECTION_STRING=$$secret_db_connection_string
- CACHE_ENABLED=true
- CACHE_STORE=redis
- CACHE_REDIS=$$secret_cache_redis
- ADMIN_EMAIL=$$config_admin_email
- ADMIN_PASSWORD=$$secret_admin_password
- CACHE_AUTO_PURGE=true
- PUBLIC_URL=$$config_public_url
ports:
- "8055"
$$id-postgresql:
name: Directus PostgreSQL
depends_on: []
image: postgres:14-alpine
volumes:
- $$id-postgresql-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=$$config_postgres_user
- POSTGRES_PASSWORD=$$secret_postgres_password
- POSTGRES_DB=$$config_postgres_db
ports: []
$$id-redis:
name: Directus Redis
depends_on: []
image: redis:7.0.4-alpine
command: "--maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5"
volumes:
- "$$id-redis:/data"
environment: []
variables:
- id: $$config_public_url
name: PUBLIC_URL
label: Public URL
defaultValue: $$generate_fqdn
description: ""
- id: $$secret_db_connection_string
name: DB_CONNECTION_STRING
label: Directus Database Url
defaultValue: postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db
description: ""
- id: $$config_postgres_db
main: $$id-postgresql
name: POSTGRES_DB
label: Database
defaultValue: directus
description: ""
- id: $$config_postgres_user
main: $$id-postgresql
name: POSTGRES_USER
label: User
defaultValue: $$generate_username
description: ""
- id: $$secret_postgres_password
main: $$id-postgresql
name: POSTGRES_PASSWORD
label: Password
defaultValue: $$generate_password
description: ""
showOnConfiguration: true
- id: $$secret_cache_redis
name: CACHE_REDIS
label: Redis Url
defaultValue: redis://$$id-redis:6379
description: ""
- id: $$config_admin_email
name: ADMIN_EMAIL
label: Initial Admin Email
defaultValue: "admin@example.com"
description: "The email address of the first user that is automatically created. You can change it later in Directus."
- id: $$secret_admin_password
name: ADMIN_PASSWORD
label: Initial Admin Password
defaultValue: $$generate_password
description: "The password of the first user that is automatically created."
showOnConfiguration: true
- id: $$secret_key
name: KEY
label: Key
defaultValue: $$generate_password
description: "Unique identifier for the project."
showOnConfiguration: true
- id: $$secret_secret
name: SECRET
label: Secret
defaultValue: $$generate_password
description: "Secret string for the project."
showOnConfiguration: true
- templateVersion: 1.0.0
defaultVersion: v1.3.8
documentation: https://github.com/LibreTranslate/LibreTranslate
description: Free and Open Source Machine Translation API. 100% self-hosted, offline capable and easy to setup.
type: libretranslate
name: Libretranslate
labels:
- translator
- argos
- python
- libretranslate
services:
$$id:
name: Libretranslate
image: libretranslate/libretranslate:$$core_version
environment:
- LT_HOST=0.0.0.0
- LT_SUGGESTIONS=true
- LT_CHAR_LIMIT=$$config_lt_char_limit
- LT_REQ_LIMIT=$$config_lt_req_limit
- LT_BATCH_LIMIT=$$config_lt_batch_limit
- LT_GA_ID=$$config_lt_ga_id
- LT_DISABLE_WEB_UI=$$config_lt_web_ui
volumes:
- $$id-libretranslate:/libretranslate
ports:
- "5000"
variables:
- id: $$config_lt_char_limit
name: LT_CHAR_LIMIT
label: Char limit
defaultValue: "5000"
description: "Set character limit."
- id: $$config_lt_req_limit
name: LT_REQ_LIMIT
label: Request limit
defaultValue: "5000"
description: "Set maximum number of requests per minute per client."
- id: $$config_lt_batch_limit
name: LT_BATCH_LIMIT
label: Batch Limit
defaultValue: "5000"
description: "Set maximum number of texts to translate in a batch request."
- id: $$config_lt_ga_id
name: LT_GA_ID
label: Google Analytics ID
defaultValue: ""
description: "Enable Google Analytics on the API client page by providing an ID"
- id: $$config_lt_web_ui
name: LT_DISABLE_WEB_UI
label: Web UI
defaultValue: "false"
description: "Disable or enable web ui. True or false."
- templateVersion: 1.0.0
defaultVersion: 0.8.0
documentation: https://github.com/benbusby/whoogle-search
type: whoogle
name: Whoogle Search
description: A self-hosted, ad-free, privacy-respecting metasearch engine
labels:
- search
- google
services:
$$id:
name: Whoogle Search
documentation: https://github.com/benbusby/whoogle-search
depends_on: []
image: benbusby/whoogle-search:$$core_version
cap_drop:
- ALL
environment:
- WHOOGLE_USER=$$config_whoogle_username
- WHOOGLE_PASS=$$secret_whoogle_password
- WHOOGLE_CONFIG_PREFERENCES_KEY=$$secret_whoogle_preferences_key
ulimits:
nofile:
soft: 262144
hard: 262144
ports:
- "5000"
variables:
- id: $$config_whoogle_username
name: WHOOGLE_USER
label: Whoogle User
defaultValue: $$generate_username
description: "Username to log into Whoogle"
- id: $$secret_whoogle_password
name: WHOOGLE_PASSWORD
label: Whoogle Password
defaultValue: $$generate_password
description: "Password to log into Whoogle"
showOnConfiguration: true
- id: $$secret_whoogle_preferences_key
name: WHOOGLE_CONFIG_PREFERENCES_KEY
label: Whoogle preferences key
defaultValue: $$generate_password
description: "password to encrypt preferences"
- templateVersion: 1.0.0
defaultVersion: 1.1.3
documentation: https://docs.openblocks.dev/
type: openblocks
name: Openblocks
description: The Open Source Retool Alternative
services:
$$id:
image: openblocksdev/openblocks-ce:$$core_version
volumes:
- $$id-stacks-data:/openblocks-stacks
ports:
- "3000"
- templateVersion: 1.0.0
defaultVersion: "0.11.0"
documentation: https://pocketbase.io/docs/ documentation: https://pocketbase.io/docs/
type: pocketbase type: pocketbase
name: Pocketbase name: Pocketbase
@@ -124,12 +348,12 @@
description: "" description: ""
- id: $$config_disable_auth - id: $$config_disable_auth
name: DISABLE_AUTH name: DISABLE_AUTH
label: Disable Authentication label: Authentication
defaultValue: "false" defaultValue: "false"
description: "" description: ""
- id: $$config_disable_registration - id: $$config_disable_registration
name: DISABLE_REGISTRATION name: DISABLE_REGISTRATION
label: Disable Registration label: Registration
defaultValue: "true" defaultValue: "true"
description: "" description: ""
- id: $$config_postgres_user - id: $$config_postgres_user
@@ -157,7 +381,7 @@
defaultValue: plausible.js defaultValue: plausible.js
description: This is the default script name. description: This is the default script name.
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: "1.17" defaultVersion: "1.18"
documentation: https://docs.gitea.io documentation: https://docs.gitea.io
type: gitea type: gitea
name: Gitea name: Gitea
@@ -332,12 +556,12 @@
volumes: volumes:
- $$id-lavalink:/lavalink - $$id-lavalink:/lavalink
ports: ports:
- "2333" - $$config_port
files: files:
- location: /opt/Lavalink/application.yml - location: /opt/Lavalink/application.yml
content: >- content: >-
server: server:
port: $$config_port port: 2333
address: 0.0.0.0 address: 0.0.0.0
lavalink: lavalink:
server: server:
@@ -364,18 +588,13 @@
max-file-size: 1GB max-file-size: 1GB
max-history: 30 max-history: 30
variables: variables:
- id: $$config_port
name: PORT
label: Port
defaultValue: "2333"
required: true
- id: $$secret_password - id: $$secret_password
name: PASSWORD name: PASSWORD
label: Password label: Password
defaultValue: $$generate_password defaultValue: $$generate_password
required: true required: true
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: v1.8.9 defaultVersion: v1.9.3
documentation: https://docs.appsmith.com/getting-started/setup/instance-configuration/ documentation: https://docs.appsmith.com/getting-started/setup/instance-configuration/
type: appsmith type: appsmith
name: Appsmith name: Appsmith
@@ -408,7 +627,7 @@
defaultValue: "true" defaultValue: "true"
description: "" description: ""
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: 0.57.4 defaultVersion: 0.58.4
documentation: https://hub.docker.com/r/zadam/trilium documentation: https://hub.docker.com/r/zadam/trilium
description: "A hierarchical note taking application with focus on building large personal knowledge bases." description: "A hierarchical note taking application with focus on building large personal knowledge bases."
labels: labels:
@@ -428,7 +647,7 @@
- "8080" - "8080"
variables: [] variables: []
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: 1.18.5 defaultVersion: 1.19.4
documentation: https://hub.docker.com/r/louislam/uptime-kuma documentation: https://hub.docker.com/r/louislam/uptime-kuma
description: A free & fancy self-hosted monitoring tool. description: A free & fancy self-hosted monitoring tool.
labels: labels:
@@ -445,7 +664,7 @@
- "3001" - "3001"
variables: [] variables: []
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: "5.8" defaultVersion: "6.0"
documentation: https://hub.docker.com/r/silviof/docker-languagetool documentation: https://hub.docker.com/r/silviof/docker-languagetool
description: "A multilingual grammar, style and spell checker." description: "A multilingual grammar, style and spell checker."
type: languagetool type: languagetool
@@ -460,7 +679,7 @@
- "8010" - "8010"
variables: [] variables: []
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: 1.26.0 defaultVersion: 1.27.0
documentation: https://hub.docker.com/r/vaultwarden/server documentation: https://hub.docker.com/r/vaultwarden/server
description: "Bitwarden compatible server written in Rust." description: "Bitwarden compatible server written in Rust."
type: vaultwarden type: vaultwarden
@@ -478,7 +697,7 @@
- "80" - "80"
variables: [] variables: []
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: 9.3.1 defaultVersion: 9.3.2
documentation: https://hub.docker.com/r/grafana/grafana documentation: https://hub.docker.com/r/grafana/grafana
type: grafana type: grafana
name: Grafana name: Grafana
@@ -499,7 +718,7 @@
- "3000" - "3000"
variables: [] variables: []
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: 1.1.2 defaultVersion: 1.2.0
documentation: https://appwrite.io/docs documentation: https://appwrite.io/docs
type: appwrite type: appwrite
name: Appwrite name: Appwrite
@@ -1669,7 +1888,7 @@
defaultValue: weblate defaultValue: weblate
description: "" description: ""
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: 2022.12.12-966e9c3c defaultVersion: 2023.01.15-52d41559
documentation: https://docs.searxng.org/ documentation: https://docs.searxng.org/
type: searxng type: searxng
name: SearXNG name: SearXNG
@@ -1742,7 +1961,7 @@
defaultValue: $$generate_password defaultValue: $$generate_password
description: "" description: ""
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: v3.0.0 defaultVersion: v3.0.2
documentation: https://glitchtip.com/documentation documentation: https://glitchtip.com/documentation
type: glitchtip type: glitchtip
name: GlitchTip name: GlitchTip
@@ -1964,7 +2183,7 @@
defaultValue: glitchtip defaultValue: glitchtip
description: "" description: ""
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: v2.16.0 defaultVersion: v2.16.1
documentation: https://hasura.io/docs/latest/index/ documentation: https://hasura.io/docs/latest/index/
type: hasura type: hasura
name: Hasura name: Hasura
@@ -2444,7 +2663,7 @@
description: "" description: ""
showOnConfiguration: true showOnConfiguration: true
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: v0.30.1 defaultVersion: v0.30.5
documentation: https://docs.meilisearch.com/learn/getting_started/quick_start.html documentation: https://docs.meilisearch.com/learn/getting_started/quick_start.html
type: meilisearch type: meilisearch
name: MeiliSearch name: MeiliSearch
@@ -2474,7 +2693,7 @@
showOnConfiguration: true showOnConfiguration: true
- templateVersion: 1.0.0 - templateVersion: 1.0.0
ignore: true ignore: true
defaultVersion: latest defaultVersion: 5.30.0
documentation: https://docs.ghost.org documentation: https://docs.ghost.org
arch: amd64 arch: amd64
type: ghost-mariadb type: ghost-mariadb
@@ -2592,7 +2811,7 @@
defaultValue: $$generate_password defaultValue: $$generate_password
description: "" description: ""
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: "5.25.3" defaultVersion: 5.30.0
documentation: https://docs.ghost.org documentation: https://docs.ghost.org
type: ghost-only type: ghost-only
name: Ghost name: Ghost
@@ -2656,7 +2875,7 @@
placeholder: "ghost_db" placeholder: "ghost_db"
required: true required: true
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: "5.25.3" defaultVersion: 5.30.0
documentation: https://docs.ghost.org documentation: https://docs.ghost.org
type: ghost-mysql type: ghost-mysql
name: Ghost name: Ghost
@@ -2733,7 +2952,7 @@
defaultValue: $$generate_password defaultValue: $$generate_password
description: "" description: ""
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: php8.1 defaultVersion: php8.2
documentation: https://wordpress.org/ documentation: https://wordpress.org/
type: wordpress type: wordpress
name: WordPress name: WordPress
@@ -2823,7 +3042,7 @@
description: "" description: ""
readOnly: true readOnly: true
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: php8.1 defaultVersion: php8.2
documentation: https://wordpress.org/ documentation: https://wordpress.org/
type: wordpress-only type: wordpress-only
name: WordPress name: WordPress
@@ -2897,7 +3116,7 @@
define('WP_DEBUG_DISPLAY', false); define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', 0); @ini_set('display_errors', 0);
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: 4.9.0 defaultVersion: 4.9.1
documentation: https://coder.com/docs/coder-oss/latest documentation: https://coder.com/docs/coder-oss/latest
type: vscodeserver type: vscodeserver
name: VSCode Server name: VSCode Server
@@ -2912,7 +3131,6 @@
depends_on: [] depends_on: []
image: "codercom/code-server:$$core_version" image: "codercom/code-server:$$core_version"
volumes: volumes:
- "$$id-config-data:/home/coder/.local/share/code-server"
- "$$id-vscodeserver-data:/home/coder" - "$$id-vscodeserver-data:/home/coder"
- "$$id-keys-directory:/root/.ssh" - "$$id-keys-directory:/root/.ssh"
- "$$id-theme-and-plugin-directory:/root/.local/share/code-server" - "$$id-theme-and-plugin-directory:/root/.local/share/code-server"
@@ -2928,7 +3146,7 @@
description: "" description: ""
showOnConfiguration: true showOnConfiguration: true
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: RELEASE.2022-12-12T19-27-27Z defaultVersion: RELEASE.2023-01-12T02-06-16Z
documentation: https://min.io/docs/minio documentation: https://min.io/docs/minio
type: minio type: minio
name: MinIO name: MinIO
@@ -2987,7 +3205,7 @@
description: "" description: ""
showOnConfiguration: true showOnConfiguration: true
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: 0.21.1 defaultVersion: stable
documentation: https://fider.io/docs documentation: https://fider.io/docs
type: fider type: fider
name: Fider name: Fider
@@ -3106,7 +3324,7 @@
defaultValue: $$generate_username defaultValue: $$generate_username
description: "" description: ""
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: 0.207.0 defaultVersion: 0.210.1
documentation: https://docs.n8n.io documentation: https://docs.n8n.io
type: n8n type: n8n
name: n8n.io name: n8n.io
@@ -3137,7 +3355,7 @@
defaultValue: $$generate_fqdn defaultValue: $$generate_fqdn
description: "" description: ""
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: stable defaultVersion: v1.5.1
documentation: https://plausible.io/doc/ documentation: https://plausible.io/doc/
arch: amd64 arch: amd64
type: plausibleanalytics type: plausibleanalytics
@@ -3250,12 +3468,12 @@
description: "" description: ""
- id: $$config_disable_auth - id: $$config_disable_auth
name: DISABLE_AUTH name: DISABLE_AUTH
label: Disable Authentication label: Authentication
defaultValue: "false" defaultValue: "false"
description: "" description: ""
- id: $$config_disable_registration - id: $$config_disable_registration
name: DISABLE_REGISTRATION name: DISABLE_REGISTRATION
label: Disable Registration label: Registration
defaultValue: "true" defaultValue: "true"
description: "" description: ""
- id: $$config_postgresql_username - id: $$config_postgresql_username
@@ -3283,7 +3501,7 @@
defaultValue: plausible.js defaultValue: plausible.js
description: This is the default script name. description: This is the default script name.
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: 0.99.1 defaultVersion: 0.101.2
documentation: https://docs.nocodb.com documentation: https://docs.nocodb.com
type: nocodb type: nocodb
name: NocoDB name: NocoDB

View File

@@ -16,31 +16,31 @@
}, },
"dependencies": { "dependencies": {
"@breejs/ts-worker": "2.0.0", "@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.5.0", "@fastify/autoload": "5.7.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0", "@fastify/cors": "8.2.0",
"@fastify/env": "4.1.0", "@fastify/env": "4.2.0",
"@fastify/jwt": "6.3.3", "@fastify/jwt": "6.5.0",
"@fastify/multipart": "7.3.0", "@fastify/multipart": "7.3.0",
"@fastify/static": "6.5.1", "@fastify/static": "6.6.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2", "@ladjs/graceful": "3.2.1",
"@prisma/client": "4.6.1", "@prisma/client": "4.8.1",
"@sentry/node": "7.21.1", "@sentry/node": "7.30.0",
"@sentry/tracing": "7.21.1", "@sentry/tracing": "7.30.0",
"axe": "11.0.0", "axe": "11.2.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bree": "9.1.2", "bree": "9.1.3",
"cabin": "11.0.1", "cabin": "11.1.1",
"compare-versions": "5.0.1", "compare-versions": "5.0.1",
"csv-parse": "5.3.2", "csv-parse": "5.3.3",
"csvtojson": "2.0.10", "csvtojson": "2.0.10",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.11.6", "dayjs": "1.11.7",
"dockerode": "3.3.4", "dockerode": "3.3.4",
"dotenv-extended": "2.9.0", "dotenv-extended": "2.9.0",
"execa": "6.1.0", "execa": "6.1.0",
"fastify": "4.10.2", "fastify": "4.11.0",
"fastify-plugin": "4.3.0", "fastify-plugin": "4.3.0",
"fastify-socket.io": "4.0.0", "fastify-socket.io": "4.0.0",
"generate-password": "1.7.0", "generate-password": "1.7.0",
@@ -48,36 +48,36 @@
"is-ip": "5.0.0", "is-ip": "5.0.0",
"is-port-reachable": "4.0.0", "is-port-reachable": "4.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "9.0.0",
"minimist": "^1.2.7", "minimist": "^1.2.7",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"node-os-utils": "1.3.7", "node-os-utils": "1.3.7",
"p-all": "4.0.0", "p-all": "4.0.0",
"p-throttle": "5.0.0", "p-throttle": "5.0.0",
"prisma": "4.6.1", "prisma": "4.8.1",
"public-ip": "6.0.1", "public-ip": "6.0.1",
"pump": "3.0.0", "pump": "3.0.0",
"shell-quote": "^1.7.4", "shell-quote": "^1.7.4",
"socket.io": "4.5.3", "socket.io": "4.5.4",
"ssh-config": "4.1.6", "ssh-config": "4.2.0",
"strip-ansi": "7.0.1", "strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.11.9", "@types/node": "18.11.18",
"@types/node-os-utils": "1.3.0", "@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.44.0", "@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.44.0", "@typescript-eslint/parser": "5.48.1",
"esbuild": "0.15.15", "esbuild": "0.16.16",
"eslint": "8.28.0", "eslint": "8.31.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.6.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.20", "nodemon": "2.0.20",
"prettier": "2.7.1", "prettier": "2.8.2",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"tsconfig-paths": "4.1.0", "tsconfig-paths": "4.1.2",
"types-fastify-socket.io": "0.0.1", "types-fastify-socket.io": "0.0.1",
"typescript": "4.9.3" "typescript": "4.9.4"
}, },
"prisma": { "prisma": {
"seed": "node prisma/seed.js" "seed": "node prisma/seed.js"

View File

@@ -0,0 +1,27 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_GitSource" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"forPublic" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT,
"apiUrl" TEXT,
"htmlUrl" TEXT,
"customPort" INTEGER NOT NULL DEFAULT 22,
"customUser" TEXT NOT NULL DEFAULT 'git',
"organization" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"githubAppId" TEXT,
"gitlabAppId" TEXT,
"isSystemWide" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "GitSource_gitlabAppId_fkey" FOREIGN KEY ("gitlabAppId") REFERENCES "GitlabApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "GitSource_githubAppId_fkey" FOREIGN KEY ("githubAppId") REFERENCES "GithubApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_GitSource" ("apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "isSystemWide", "name", "organization", "type", "updatedAt") SELECT "apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "isSystemWide", "name", "organization", "type", "updatedAt" FROM "GitSource";
DROP TABLE "GitSource";
ALTER TABLE "new_GitSource" RENAME TO "GitSource";
CREATE UNIQUE INDEX "GitSource_githubAppId_key" ON "GitSource"("githubAppId");
CREATE UNIQUE INDEX "GitSource_gitlabAppId_key" ON "GitSource"("gitlabAppId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,24 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
"isHttp2" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -186,6 +186,7 @@ model ApplicationSettings {
isPublicRepository Boolean @default(false) isPublicRepository Boolean @default(false)
isDBBranching Boolean @default(false) isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false) isCustomSSL Boolean @default(false)
isHttp2 Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
@@ -325,6 +326,7 @@ model GitSource {
apiUrl String? apiUrl String?
htmlUrl String? htmlUrl String?
customPort Int @default(22) customPort Int @default(22)
customUser String @default("git")
organization String? organization String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@@ -159,7 +159,7 @@ const host = '0.0.0.0';
if (!scheduler.workers.has('deployApplication')) { if (!scheduler.workers.has('deployApplication')) {
scheduler.run('deployApplication'); scheduler.run('deployApplication');
} }
}, 60000 * 15); }, 2000);
// autoUpdater // autoUpdater
setInterval(async () => { setInterval(async () => {
@@ -225,8 +225,22 @@ async function getTagsTemplates() {
const { default: got } = await import('got'); const { default: got } = await import('got');
try { try {
if (isDev) { if (isDev) {
const templates = await fs.readFile('./devTemplates.yaml', 'utf8'); let templates = await fs.readFile('./devTemplates.yaml', 'utf8');
const tags = await fs.readFile('./devTags.json', 'utf8'); let tags = await fs.readFile('./devTags.json', 'utf8');
try {
if (await fs.stat('./testTemplate.yaml')) {
templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8'));
}
} catch (error) {}
try {
if (await fs.stat('./testTags.json')) {
const testTags = await fs.readFile('./testTags.json', 'utf8');
if (testTags.length > 0) {
tags = JSON.stringify(JSON.parse(tags).concat(JSON.parse(testTags)));
}
}
} catch (error) {}
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates))); await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates)));
await fs.writeFile('./tags.json', tags); await fs.writeFile('./tags.json', tags);
console.log('[004] Tags and templates loaded in dev mode...'); console.log('[004] Tags and templates loaded in dev mode...');

View File

@@ -196,7 +196,7 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({ await executeCommand({
debug: true, debug: true,
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d` command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml up -d`
}); });
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
} catch (error) { } catch (error) {
@@ -419,6 +419,7 @@ import * as buildpacks from '../lib/buildPacks';
githubAppId: gitSource.githubApp?.id, githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id, gitlabAppId: gitSource.gitlabApp?.id,
customPort: gitSource.customPort, customPort: gitSource.customPort,
customUser: gitSource.customUser,
gitCommitHash, gitCommitHash,
configuration, configuration,
repository, repository,
@@ -600,6 +601,7 @@ import * as buildpacks from '../lib/buildPacks';
} }
if (buildPack === 'compose') { if (buildPack === 'compose') {
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`;
try { try {
const { stdout: containers } = await executeCommand({ const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId, dockerId: destinationDockerId,
@@ -629,7 +631,7 @@ import * as buildpacks from '../lib/buildPacks';
buildId, buildId,
applicationId, applicationId,
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d` command: `docker compose --project-directory ${workdir} -f ${fileYaml} up -d`
}); });
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
await prisma.build.update({ await prisma.build.update({
@@ -724,7 +726,7 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({ await executeCommand({
debug, debug,
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d` command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml up -d`
}); });
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
} catch (error) { } catch (error) {

View File

@@ -26,8 +26,10 @@ export default async function (data) {
throw 'No Services found in docker-compose file.'; throw 'No Services found in docker-compose file.';
} }
let envs = []; let envs = [];
let buildEnvs = [];
if (secrets.length > 0) { if (secrets.length > 0) {
envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)]; envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)];
buildEnvs = [...buildEnvs, ...generateSecrets(secrets, pullmergeRequestId, true, null, true)];
} }
const composeVolumes = []; const composeVolumes = [];
@@ -43,8 +45,32 @@ export default async function (data) {
let networks = {}; let networks = {};
for (let [key, value] of Object.entries(dockerComposeYaml.services)) { for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
value['container_name'] = `${applicationId}-${key}`; value['container_name'] = `${applicationId}-${key}`;
let environment = typeof value['environment'] === 'undefined' ? [] : value['environment']
let environment = typeof value['environment'] === 'undefined' ? [] : value['environment'];
if (Object.keys(environment).length > 0) {
environment = Object.entries(environment).map(([key, value]) => `${key}=${value}`);
}
value['environment'] = [...environment, ...envs]; value['environment'] = [...environment, ...envs];
let build = typeof value['build'] === 'undefined' ? [] : value['build'];
if (typeof build === 'string') {
build = { context: build };
}
const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args'];
let finalArgs = [...buildEnvs];
if (Object.keys(buildArgs).length > 0) {
for (const arg of buildArgs) {
const [key, _] = arg.split('=');
if (finalArgs.filter((env) => env.startsWith(key)).length === 0) {
finalArgs.push(arg);
}
}
}
value['build'] = {
...build,
args: finalArgs
};
value['labels'] = labels; value['labels'] = labels;
// TODO: If we support separated volume for each service, we need to add it here // TODO: If we support separated volume for each service, we need to add it here
if (value['volumes']?.length > 0) { if (value['volumes']?.length > 0) {
@@ -90,12 +116,13 @@ export default async function (data) {
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }); dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } });
await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml)); await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml));
console.log(yaml.dump(dockerComposeYaml));
await executeCommand({ await executeCommand({
debug, debug,
buildId, buildId,
applicationId, applicationId,
dockerId, dockerId,
command: `docker compose --project-directory ${workdir} pull` command: `docker compose --project-directory ${workdir} -f ${fileYaml} pull`
}); });
await saveBuildLog({ line: 'Pulling images from Compose file...', buildId, applicationId }); await saveBuildLog({ line: 'Pulling images from Compose file...', buildId, applicationId });
await executeCommand({ await executeCommand({
@@ -103,7 +130,7 @@ export default async function (data) {
buildId, buildId,
applicationId, applicationId,
dockerId, dockerId,
command: `docker compose --project-directory ${workdir} build --progress plain` command: `docker compose --project-directory ${workdir} -f ${fileYaml} build --progress plain`
}); });
await saveBuildLog({ line: 'Building images from Compose file...', buildId, applicationId }); await saveBuildLog({ line: 'Building images from Compose file...', buildId, applicationId });
} }

View File

@@ -1,12 +1,18 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildCacheImageForLaravel, buildImage } from './common'; import { buildCacheImageForLaravel, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, applicationId, tag, buildId, port } = data; const { workdir, applicationId, tag, buildId, port, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
Dockerfile.push(env);
});
}
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`ENV WEB_DOCUMENT_ROOT /app/public`); Dockerfile.push(`ENV WEB_DOCUMENT_ROOT /app/public`);
Dockerfile.push(`COPY --chown=application:application composer.* ./`); Dockerfile.push(`COPY --chown=application:application composer.* ./`);

View File

@@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common
import { scheduler } from './scheduler'; import { scheduler } from './scheduler';
import type { ExecaChildProcess } from 'execa'; import type { ExecaChildProcess } from 'execa';
export const version = '3.12.6'; export const version = '3.12.13';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
export const sentryDSN = export const sentryDSN =
'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216'; 'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
@@ -651,13 +651,11 @@ export async function executeCommand({
} else { } else {
if (shell) { if (shell) {
return await execaCommand(command, { return await execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }, env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
shell: true
}); });
} else { } else {
return await execa(dockerCommand, dockerArgs, { return await execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }, env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
shell: false
}); });
} }
} }
@@ -716,8 +714,10 @@ export async function startTraefikProxy(id: string): Promise<void> {
--network coolify-infra \ --network coolify-infra \
-p "80:80" \ -p "80:80" \
-p "443:443" \ -p "443:443" \
${isDev ? '-p "8080:8080"' : ''} \
--name coolify-proxy \ --name coolify-proxy \
-d ${defaultTraefikImage} \ -d ${defaultTraefikImage} \
${isDev ? '--api.insecure=true' : ''} \
--entrypoints.web.address=:80 \ --entrypoints.web.address=:80 \
--entrypoints.web.forwardedHeaders.insecure=true \ --entrypoints.web.forwardedHeaders.insecure=true \
--entrypoints.websecure.address=:443 \ --entrypoints.websecure.address=:443 \
@@ -1886,11 +1886,36 @@ export async function pushToRegistry(
}); });
} }
function parseSecret(secret, isBuild) {
if (secret.value.includes('$')) {
secret.value = secret.value.replaceAll('$', '$$$$');
}
if (secret.value.includes('\\n')) {
if (isBuild) {
return `ARG ${secret.name}=${secret.value}`;
} else {
return `${secret.name}=${secret.value}`;
}
} else if (secret.value.includes(' ')) {
if (isBuild) {
return `ARG ${secret.name}='${secret.value}'`;
} else {
return `${secret.name}='${secret.value}'`;
}
} else {
if (isBuild) {
return `ARG ${secret.name}=${secret.value}`;
} else {
return `${secret.name}=${secret.value}`;
}
}
}
export function generateSecrets( export function generateSecrets(
secrets: Array<any>, secrets: Array<any>,
pullmergeRequestId: string, pullmergeRequestId: string,
isBuild = false, isBuild = false,
port = null port = null,
compose = false
): Array<string> { ): Array<string> {
const envs = []; const envs = [];
const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret); const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret);
@@ -1901,15 +1926,7 @@ export function generateSecrets(
return; return;
} }
const build = isBuild && secret.isBuildSecret; const build = isBuild && secret.isBuildSecret;
if (build) { envs.push(parseSecret(secret, compose ? false : build));
if (secret.value.includes(' ') || secret.value.includes('\\n')) {
envs.push(`ARG ${secret.name}='${secret.value}'`);
} else {
envs.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
envs.push(`${secret.name}=${secret.value}`);
}
}); });
} }
if (!pullmergeRequestId && normalSecrets.length > 0) { if (!pullmergeRequestId && normalSecrets.length > 0) {
@@ -1918,15 +1935,7 @@ export function generateSecrets(
return; return;
} }
const build = isBuild && secret.isBuildSecret; const build = isBuild && secret.isBuildSecret;
if (build) { envs.push(parseSecret(secret, compose ? false : build));
if (secret.value.includes(' ') || secret.value.includes('\\n')) {
envs.push(`ARG ${secret.name}='${secret.value}'`);
} else {
envs.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
envs.push(`${secret.name}=${secret.value}`);
}
}); });
} }
const portFound = envs.filter((env) => env.startsWith('PORT')); const portFound = envs.filter((env) => env.startsWith('PORT'));

View File

@@ -12,7 +12,8 @@ export default async function ({
buildId, buildId,
privateSshKey, privateSshKey,
customPort, customPort,
forPublic forPublic,
customUser,
}: { }: {
applicationId: string; applicationId: string;
workdir: string; workdir: string;
@@ -25,6 +26,7 @@ export default async function ({
privateSshKey: string; privateSshKey: string;
customPort: number; customPort: number;
forPublic: boolean; forPublic: boolean;
customUser: string;
}): Promise<string> { }): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
if (!forPublic) { if (!forPublic) {
@@ -53,7 +55,7 @@ export default async function ({
} else { } else {
await executeCommand({ await executeCommand({
command: command:
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true `git clone -q -b ${branch} ${customUser}@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true
} }
); );
} }

View File

@@ -122,6 +122,9 @@ export async function cleanupUnconfiguredApplications(request: FastifyRequest<an
include: { settings: true, destinationDocker: true, teams: true } include: { settings: true, destinationDocker: true, teams: true }
}); });
for (const application of applications) { for (const application of applications) {
if (application?.buildPack === 'compose') {
continue;
}
if ( if (
!application.buildPack || !application.buildPack ||
!application.destinationDockerId || !application.destinationDockerId ||
@@ -500,14 +503,24 @@ export async function saveApplicationSettings(
projectId, projectId,
isBot, isBot,
isDBBranching, isDBBranching,
isCustomSSL isCustomSSL,
isHttp2
} = request.body; } = request.body;
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { data: {
fqdn: isBot ? null : undefined, fqdn: isBot ? null : undefined,
settings: { settings: {
update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } update: {
debug,
previews,
dualCerts,
autodeploy,
isBot,
isDBBranching,
isCustomSSL,
isHttp2
}
} }
}, },
include: { destinationDocker: true } include: { destinationDocker: true }
@@ -670,7 +683,7 @@ export async function restartApplication(
await executeCommand({ await executeCommand({
dockerId, dockerId,
command: `docker compose --project-directory ${workdir} up -d` command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml up -d`
}); });
return reply.code(201).send(); return reply.code(201).send();
} }
@@ -746,6 +759,7 @@ export async function deleteApplication(
await prisma.secret.deleteMany({ where: { applicationId: id } }); await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } }); await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: id } }); await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: id } });
await prisma.previewApplication.deleteMany({ where: { applicationId: id } });
if (teamId === '0') { if (teamId === '0') {
await prisma.application.deleteMany({ where: { id } }); await prisma.application.deleteMany({ where: { id } });
} else { } else {
@@ -764,7 +778,9 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
fqdn, fqdn,
settings: { dualCerts } settings: { dualCerts }
} = await prisma.application.findUnique({ where: { id }, include: { settings: true } }); } = await prisma.application.findUnique({ where: { id }, include: { settings: true } });
return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts }); // TODO: Disabled this because it is having problems with remote docker engines.
// return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts });
return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
} }
@@ -805,11 +821,12 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
remoteEngine, remoteEngine,
remoteIpAddress remoteIpAddress
}); });
if (isDNSCheckEnabled && !isDev && !forceSave) { // TODO: Disabled this because it is having problems with remote docker engines.
let hostname = request.hostname.split(':')[0]; // if (isDNSCheckEnabled && !isDev && !forceSave) {
if (remoteEngine) hostname = remoteIpAddress; // let hostname = request.hostname.split(':')[0];
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }); // if (remoteEngine) hostname = remoteIpAddress;
} // return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
// }
return {}; return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@@ -842,15 +859,16 @@ export async function getDockerImages(request) {
try { try {
const { stdout } = await executeCommand({ const { stdout } = await executeCommand({
dockerId: application.destinationDocker.id, dockerId: application.destinationDocker.id,
command: `docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}' | grep -i ${id} | grep -v cache`, command: `docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'`
shell: true
}); });
const { stdout: runningImage } = await executeCommand({ const { stdout: runningImage } = await executeCommand({
dockerId: application.destinationDocker.id, dockerId: application.destinationDocker.id,
command: `docker ps -a --filter 'label=com.docker.compose.service=${id}' --format {{.Image}}` command: `docker ps -a --filter 'label=com.docker.compose.service=${id}' --format {{.Image}}`
}); });
const images = stdout.trim().split('\n'); const images = stdout
.trim()
.split('\n')
.filter((image) => image.includes(id) && !image.includes('-cache'));
for (const image of images) { for (const image of images) {
const [repository, tag, createdAt] = image.split('#'); const [repository, tag, createdAt] = image.split('#');
if (tag.includes('-')) { if (tag.includes('-')) {
@@ -871,6 +889,7 @@ export async function getDockerImages(request) {
runningImage runningImage
}; };
} catch (error) { } catch (error) {
console.log(error);
return { return {
imagesAvailables imagesAvailables
}; };
@@ -1446,7 +1465,7 @@ export async function restartPreview(
await executeCommand({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` }); await executeCommand({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` });
await executeCommand({ await executeCommand({
dockerId, dockerId,
command: `docker compose --project-directory ${workdir} up -d` command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml up -d`
}); });
return reply.code(201).send(); return reply.code(201).send();
} }
@@ -1600,12 +1619,7 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
.split('\n') .split('\n')
.map((l) => ansi(l)) .map((l) => ansi(l))
.filter((a) => a); .filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout); return { logs: stripLogsStderr.concat(stripLogsStdout) };
const sortedLogs = logs.sort((a, b) =>
day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1
);
return { logs: sortedLogs };
// }
} catch (error) { } catch (error) {
const { statusCode, stderr } = error; const { statusCode, stderr } = error;
if (stderr.startsWith('Error: No such container')) { if (stderr.startsWith('Error: No such container')) {

View File

@@ -1,154 +1,170 @@
import type { OnlyId } from "../../../../types"; import type { OnlyId } from '../../../../types';
export interface SaveApplication extends OnlyId { export interface SaveApplication extends OnlyId {
Body: { Body: {
name: string, name: string;
buildPack: string, buildPack: string;
fqdn: string, fqdn: string;
port: number, port: number;
exposePort: number, exposePort: number;
installCommand: string, installCommand: string;
buildCommand: string, buildCommand: string;
startCommand: string, startCommand: string;
baseDirectory: string, baseDirectory: string;
publishDirectory: string, publishDirectory: string;
pythonWSGI: string, pythonWSGI: string;
pythonModule: string, pythonModule: string;
pythonVariable: string, pythonVariable: string;
dockerFileLocation: string, dockerFileLocation: string;
denoMainFile: string, denoMainFile: string;
denoOptions: string, denoOptions: string;
baseImage: string, baseImage: string;
gitCommitHash: string, gitCommitHash: string;
baseBuildImage: string, baseBuildImage: string;
deploymentType: string, deploymentType: string;
baseDatabaseBranch: string, baseDatabaseBranch: string;
dockerComposeFile: string, dockerComposeFile: string;
dockerComposeFileLocation: string, dockerComposeFileLocation: string;
dockerComposeConfiguration: string, dockerComposeConfiguration: string;
simpleDockerfile: string, simpleDockerfile: string;
dockerRegistryImageName: string dockerRegistryImageName: string;
} };
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string };
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean, isCustomSSL: boolean }; Body: {
debug: boolean;
previews: boolean;
dualCerts: boolean;
autodeploy: boolean;
branch: string;
projectId: number;
isBot: boolean;
isDBBranching: boolean;
isCustomSSL: boolean;
isHttp2: boolean;
};
} }
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string };
Body: { force: boolean } Body: { force: boolean };
} }
export interface CheckDomain extends OnlyId { export interface CheckDomain extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string };
} }
export interface CheckDNS extends OnlyId { export interface CheckDNS extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string };
Body: { Body: {
exposePort: number, exposePort: number;
fqdn: string, fqdn: string;
forceSave: boolean, forceSave: boolean;
dualCerts: boolean dualCerts: boolean;
} };
} }
export interface DeployApplication { export interface DeployApplication {
Querystring: { domain: string } Querystring: { domain: string };
Body: { pullmergeRequestId: string | null, branch: string, forceRebuild?: boolean } Body: { pullmergeRequestId: string | null; branch: string; forceRebuild?: boolean };
} }
export interface GetImages { export interface GetImages {
Body: { buildPack: string, deploymentType: string } Body: { buildPack: string; deploymentType: string };
} }
export interface SaveApplicationSource extends OnlyId { export interface SaveApplicationSource extends OnlyId {
Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string, simpleDockerfile?: string } Body: {
gitSourceId?: string | null;
forPublic?: boolean;
type?: string;
simpleDockerfile?: string;
};
} }
export interface CheckRepository extends OnlyId { export interface CheckRepository extends OnlyId {
Querystring: { repository: string, branch: string } Querystring: { repository: string; branch: string };
} }
export interface SaveDestination extends OnlyId { export interface SaveDestination extends OnlyId {
Body: { destinationId: string } Body: { destinationId: string };
} }
export interface SaveSecret extends OnlyId { export interface SaveSecret extends OnlyId {
Body: { Body: {
name: string, name: string;
value: string, value: string;
isBuildSecret: boolean, isBuildSecret: boolean;
previewSecret: boolean, previewSecret: boolean;
isNew: boolean isNew: boolean;
} };
} }
export interface DeleteSecret extends OnlyId { export interface DeleteSecret extends OnlyId {
Body: { name: string } Body: { name: string };
} }
export interface SaveStorage extends OnlyId { export interface SaveStorage extends OnlyId {
Body: { Body: {
path: string, path: string;
newStorage: boolean, newStorage: boolean;
storageId: string storageId: string;
} };
} }
export interface DeleteStorage extends OnlyId { export interface DeleteStorage extends OnlyId {
Body: { Body: {
path: string, path: string;
} };
} }
export interface GetApplicationLogs { export interface GetApplicationLogs {
Params: { Params: {
id: string, id: string;
containerId: string containerId: string;
} };
Querystring: { Querystring: {
since: number, since: number;
} };
} }
export interface GetBuilds extends OnlyId { export interface GetBuilds extends OnlyId {
Querystring: { Querystring: {
buildId: string buildId: string;
skip: number, skip: number;
} };
} }
export interface GetBuildIdLogs { export interface GetBuildIdLogs {
Params: { Params: {
id: string, id: string;
buildId: string buildId: string;
}, };
Querystring: { Querystring: {
sequence: number sequence: number;
} };
} }
export interface SaveDeployKey extends OnlyId { export interface SaveDeployKey extends OnlyId {
Body: { Body: {
deployKeyId: number deployKeyId: number;
} };
} }
export interface CancelDeployment { export interface CancelDeployment {
Body: { Body: {
buildId: string, buildId: string;
applicationId: string applicationId: string;
} };
} }
export interface DeployApplication extends OnlyId { export interface DeployApplication extends OnlyId {
Body: { Body: {
pullmergeRequestId: string | null, pullmergeRequestId: string | null;
branch: string, branch: string;
forceRebuild?: boolean forceRebuild?: boolean;
} };
} }
export interface StopPreviewApplication extends OnlyId { export interface StopPreviewApplication extends OnlyId {
Body: { Body: {
pullmergeRequestId: string | null, pullmergeRequestId: string | null;
} };
} }
export interface RestartPreviewApplication { export interface RestartPreviewApplication {
Params: { Params: {
id: string, id: string;
pullmergeRequestId: string | null, pullmergeRequestId: string | null;
} };
} }
export interface RestartApplication { export interface RestartApplication {
Params: { Params: {
id: string, id: string;
}, };
Body: { Body: {
imageId: string | null, imageId: string | null;
} };
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { compareVersions } from "compare-versions"; import { compareVersions } from 'compare-versions';
import cuid from "cuid"; import cuid from 'cuid';
import bcrypt from "bcryptjs"; import bcrypt from 'bcryptjs';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { import {
@@ -13,12 +13,12 @@ import {
uniqueName, uniqueName,
version, version,
sentryDSN, sentryDSN,
executeCommand, executeCommand
} from "../../../lib/common"; } from '../../../lib/common';
import { scheduler } from "../../../lib/scheduler"; import { scheduler } from '../../../lib/scheduler';
import type { FastifyReply, FastifyRequest } from "fastify"; import type { FastifyReply, FastifyRequest } from 'fastify';
import type { Login, Update } from "."; import type { Login, Update } from '.';
import type { GetCurrentUser } from "./types"; import type { GetCurrentUser } from './types';
export async function hashPassword(password: string): Promise<string> { export async function hashPassword(password: string): Promise<string> {
const saltRounds = 15; const saltRounds = 15;
@@ -29,9 +29,9 @@ export async function backup(request: FastifyRequest) {
try { try {
const { backupData } = request.params; const { backupData } = request.params;
let std = null; let std = null;
const [id, backupType, type, zipped, storage] = backupData.split(':') const [id, backupType, type, zipped, storage] = backupData.split(':');
console.log(id, backupType, type, zipped, storage) console.log(id, backupType, type, zipped, storage);
const database = await prisma.database.findUnique({ where: { id } }) const database = await prisma.database.findUnique({ where: { id } });
if (database) { if (database) {
// await executeDockerCmd({ // await executeDockerCmd({
// dockerId: database.destinationDockerId, // dockerId: database.destinationDockerId,
@@ -40,8 +40,7 @@ export async function backup(request: FastifyRequest) {
std = await executeCommand({ std = await executeCommand({
dockerId: database.destinationDockerId, dockerId: database.destinationDockerId,
command: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v coolify-local-backup:/app/backups -e CONTAINERS_TO_BACKUP="${backupData}" coollabsio/backup` command: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v coolify-local-backup:/app/backups -e CONTAINERS_TO_BACKUP="${backupData}" coollabsio/backup`
}) });
} }
if (std.stdout) { if (std.stdout) {
return std.stdout; return std.stdout;
@@ -58,7 +57,7 @@ export async function cleanupManually(request: FastifyRequest) {
try { try {
const { serverId } = request.body; const { serverId } = request.body;
const destination = await prisma.destinationDocker.findUnique({ const destination = await prisma.destinationDocker.findUnique({
where: { id: serverId }, where: { id: serverId }
}); });
await cleanupDockerStorage(destination.id, true, true); await cleanupDockerStorage(destination.id, true, true);
return {}; return {};
@@ -68,17 +67,25 @@ export async function cleanupManually(request: FastifyRequest) {
} }
export async function refreshTags() { export async function refreshTags() {
try { try {
const { default: got } = await import('got') const { default: got } = await import('got');
try { try {
if (isDev) { if (isDev) {
const tags = await fs.readFile('./devTags.json', 'utf8') let tags = await fs.readFile('./devTags.json', 'utf8');
await fs.writeFile('./tags.json', tags) try {
if (await fs.stat('./testTags.json')) {
const testTags = await fs.readFile('./testTags.json', 'utf8');
if (testTags.length > 0) {
tags = JSON.parse(tags).concat(JSON.parse(testTags));
}
}
} catch (error) {}
await fs.writeFile('./tags.json', tags);
} else { } else {
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text() const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text();
await fs.writeFile('/app/tags.json', tags) await fs.writeFile('/app/tags.json', tags);
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
return {}; return {};
@@ -88,17 +95,25 @@ export async function refreshTags() {
} }
export async function refreshTemplates() { export async function refreshTemplates() {
try { try {
const { default: got } = await import('got') const { default: got } = await import('got');
try { try {
if (isDev) { if (isDev) {
const response = await fs.readFile('./devTemplates.yaml', 'utf8') let templates = await fs.readFile('./devTemplates.yaml', 'utf8');
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response))) try {
if (await fs.stat('./testTemplate.yaml')) {
templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8'));
}
} catch (error) {}
const response = await fs.readFile('./devTemplates.yaml', 'utf8');
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response)));
} else { } else {
const response = await got.get('https://get.coollabs.io/coolify/service-templates.yaml').text() const response = await got
await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response))) .get('https://get.coollabs.io/coolify/service-templates.yaml')
.text();
await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response)));
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
return {}; return {};
} catch ({ status, message }) { } catch ({ status, message }) {
@@ -107,28 +122,29 @@ export async function refreshTemplates() {
} }
export async function checkUpdate(request: FastifyRequest) { export async function checkUpdate(request: FastifyRequest) {
try { try {
const { default: got } = await import('got') const { default: got } = await import('got');
const isStaging = const isStaging =
request.hostname === "staging.coolify.io" || request.hostname === 'staging.coolify.io' || request.hostname === 'arm.coolify.io';
request.hostname === "arm.coolify.io";
const currentVersion = version; const currentVersion = version;
const { coolify } = await got.get('https://get.coollabs.io/versions.json', { const { coolify } = await got
searchParams: { .get('https://get.coollabs.io/versions.json', {
appId: process.env['COOLIFY_APP_ID'] || undefined, searchParams: {
version: currentVersion appId: process.env['COOLIFY_APP_ID'] || undefined,
} version: currentVersion
}).json() }
})
.json();
const latestVersion = coolify.main.version; const latestVersion = coolify.main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion); const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isStaging) { if (isStaging) {
return { return {
isUpdateAvailable: true, isUpdateAvailable: true,
latestVersion: "next", latestVersion: 'next'
}; };
} }
return { return {
isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1, isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1,
latestVersion, latestVersion
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@@ -142,8 +158,13 @@ export async function update(request: FastifyRequest<Update>) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` }); await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` });
await executeCommand({ shell: true, command: `env | grep COOLIFY > .env` }); await executeCommand({ shell: true, command: `env | grep COOLIFY > .env` });
await executeCommand({ command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` }); await executeCommand({
await executeCommand({ shell: true, command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` }); command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
});
await executeCommand({
shell: true,
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
});
return {}; return {};
} else { } else {
await asyncSleep(2000); await asyncSleep(2000);
@@ -156,12 +177,12 @@ export async function update(request: FastifyRequest<Update>) {
export async function resetQueue(request: FastifyRequest<any>) { export async function resetQueue(request: FastifyRequest<any>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
if (teamId === "0") { if (teamId === '0') {
await prisma.build.updateMany({ await prisma.build.updateMany({
where: { status: { in: ["queued", "running"] } }, where: { status: { in: ['queued', 'running'] } },
data: { status: "canceled" }, data: { status: 'canceled' }
}); });
scheduler.workers.get("deployApplication").postMessage("cancel"); scheduler.workers.get('deployApplication').postMessage('cancel');
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@@ -170,7 +191,7 @@ export async function resetQueue(request: FastifyRequest<any>) {
export async function restartCoolify(request: FastifyRequest<any>) { export async function restartCoolify(request: FastifyRequest<any>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
if (teamId === "0") { if (teamId === '0') {
if (!isDev) { if (!isDev) {
await executeCommand({ command: `docker restart coolify` }); await executeCommand({ command: `docker restart coolify` });
return {}; return {};
@@ -180,7 +201,7 @@ export async function restartCoolify(request: FastifyRequest<any>) {
} }
throw { throw {
status: 500, status: 500,
message: "You are not authorized to restart Coolify.", message: 'You are not authorized to restart Coolify.'
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@@ -192,43 +213,52 @@ export async function showDashboard(request: FastifyRequest) {
const userId = request.user.userId; const userId = request.user.userId;
const teamId = request.user.teamId; const teamId = request.user.teamId;
let applications = await prisma.application.findMany({ let applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true }, include: { settings: true, destinationDocker: true, teams: true }
}); });
const databases = await prisma.database.findMany({ const databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true }, include: { settings: true, destinationDocker: true, teams: true }
}); });
const services = await prisma.service.findMany({ const services = await prisma.service.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, teams: true }, include: { destinationDocker: true, teams: true }
}); });
const gitSources = await prisma.gitSource.findMany({ const gitSources = await prisma.gitSource.findMany({
where: { OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] }, where: {
include: { teams: true }, OR: [
{ teams: { some: { id: teamId === '0' ? undefined : teamId } } },
{ isSystemWide: true }
]
},
include: { teams: true }
}); });
const destinations = await prisma.destinationDocker.findMany({ const destinations = await prisma.destinationDocker.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { teams: true }, include: { teams: true }
}); });
const settings = await listSettings(); const settings = await listSettings();
let foundUnconfiguredApplication = false; let foundUnconfiguredApplication = false;
for (const application of applications) { for (const application of applications) {
if (((!application.buildPack || !application.branch) && !application.simpleDockerfile) || !application.destinationDockerId || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") { if (
foundUnconfiguredApplication = true ((!application.buildPack || !application.branch) && !application.simpleDockerfile) ||
!application.destinationDockerId ||
(!application.settings?.isBot && !application?.fqdn && application.buildPack !== 'compose')
) {
foundUnconfiguredApplication = true;
} }
} }
let foundUnconfiguredService = false; let foundUnconfiguredService = false;
for (const service of services) { for (const service of services) {
if (!service.fqdn) { if (!service.fqdn) {
foundUnconfiguredService = true foundUnconfiguredService = true;
} }
} }
let foundUnconfiguredDatabase = false; let foundUnconfiguredDatabase = false;
for (const database of databases) { for (const database of databases) {
if (!database.version) { if (!database.version) {
foundUnconfiguredDatabase = true foundUnconfiguredDatabase = true;
} }
} }
return { return {
@@ -240,101 +270,94 @@ export async function showDashboard(request: FastifyRequest) {
services, services,
gitSources, gitSources,
destinations, destinations,
settings, settings
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
} }
} }
export async function login( export async function login(request: FastifyRequest<Login>, reply: FastifyReply) {
request: FastifyRequest<Login>,
reply: FastifyReply
) {
if (request.user) { if (request.user) {
return reply.redirect("/dashboard"); return reply.redirect('/dashboard');
} else { } else {
const { email, password, isLogin } = request.body || {}; const { email, password, isLogin } = request.body || {};
if (!email || !password) { if (!email || !password) {
throw { status: 500, message: "Email and password are required." }; throw { status: 500, message: 'Email and password are required.' };
} }
const users = await prisma.user.count(); const users = await prisma.user.count();
const userFound = await prisma.user.findUnique({ const userFound = await prisma.user.findUnique({
where: { email }, where: { email },
include: { teams: true, permission: true }, include: { teams: true, permission: true },
rejectOnNotFound: false, rejectOnNotFound: false
}); });
if (!userFound && isLogin) { if (!userFound && isLogin) {
throw { status: 500, message: "User not found." }; throw { status: 500, message: 'User not found.' };
} }
const { isRegistrationEnabled, id } = await prisma.setting.findFirst(); const { isRegistrationEnabled, id } = await prisma.setting.findFirst();
let uid = cuid(); let uid = cuid();
let permission = "read"; let permission = 'read';
let isAdmin = false; let isAdmin = false;
if (users === 0) { if (users === 0) {
await prisma.setting.update({ await prisma.setting.update({
where: { id }, where: { id },
data: { isRegistrationEnabled: false }, data: { isRegistrationEnabled: false }
}); });
uid = "0"; uid = '0';
} }
if (userFound) { if (userFound) {
if (userFound.type === "email") { if (userFound.type === 'email') {
if (userFound.password === "RESETME") { if (userFound.password === 'RESETME') {
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) { if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
if (userFound.id === "0") { if (userFound.id === '0') {
await prisma.user.update({ await prisma.user.update({
where: { email: userFound.email }, where: { email: userFound.email },
data: { password: "RESETME" }, data: { password: 'RESETME' }
}); });
} else { } else {
await prisma.user.update({ await prisma.user.update({
where: { email: userFound.email }, where: { email: userFound.email },
data: { password: "RESETTIMEOUT" }, data: { password: 'RESETTIMEOUT' }
}); });
} }
throw { throw {
status: 500, status: 500,
message: message: 'Password reset link has expired. Please request a new one.'
"Password reset link has expired. Please request a new one.",
}; };
} else { } else {
await prisma.user.update({ await prisma.user.update({
where: { email: userFound.email }, where: { email: userFound.email },
data: { password: hashedPassword }, data: { password: hashedPassword }
}); });
return { return {
userId: userFound.id, userId: userFound.id,
teamId: userFound.id, teamId: userFound.id,
permission: userFound.permission, permission: userFound.permission,
isAdmin: true, isAdmin: true
}; };
} }
} }
const passwordMatch = await bcrypt.compare( const passwordMatch = await bcrypt.compare(password, userFound.password);
password,
userFound.password
);
if (!passwordMatch) { if (!passwordMatch) {
throw { throw {
status: 500, status: 500,
message: "Wrong password or email address.", message: 'Wrong password or email address.'
}; };
} }
uid = userFound.id; uid = userFound.id;
isAdmin = true; isAdmin = true;
} }
} else { } else {
permission = "owner"; permission = 'owner';
isAdmin = true; isAdmin = true;
if (!isRegistrationEnabled) { if (!isRegistrationEnabled) {
throw { throw {
status: 404, status: 404,
message: "Registration disabled by administrator.", message: 'Registration disabled by administrator.'
}; };
} }
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
@@ -344,17 +367,17 @@ export async function login(
id: uid, id: uid,
email, email,
password: hashedPassword, password: hashedPassword,
type: "email", type: 'email',
teams: { teams: {
create: { create: {
id: uid, id: uid,
name: uniqueName(), name: uniqueName(),
destinationDocker: { connect: { network: "coolify" } }, destinationDocker: { connect: { network: 'coolify' } }
}, }
}, },
permission: { create: { teamId: uid, permission: "owner" } }, permission: { create: { teamId: uid, permission: 'owner' } }
}, },
include: { teams: true }, include: { teams: true }
}); });
} else { } else {
await prisma.user.create({ await prisma.user.create({
@@ -362,16 +385,16 @@ export async function login(
id: uid, id: uid,
email, email,
password: hashedPassword, password: hashedPassword,
type: "email", type: 'email',
teams: { teams: {
create: { create: {
id: uid, id: uid,
name: uniqueName(), name: uniqueName()
}, }
}, },
permission: { create: { teamId: uid, permission: "owner" } }, permission: { create: { teamId: uid, permission: 'owner' } }
}, },
include: { teams: true }, include: { teams: true }
}); });
} }
} }
@@ -379,23 +402,20 @@ export async function login(
userId: uid, userId: uid,
teamId: uid, teamId: uid,
permission, permission,
isAdmin, isAdmin
}; };
} }
} }
export async function getCurrentUser( export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fastify) {
request: FastifyRequest<GetCurrentUser>,
fastify
) {
let token = null; let token = null;
const { teamId } = request.query; const { teamId } = request.query;
try { try {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: request.user.userId }, where: { id: request.user.userId }
}); });
if (!user) { if (!user) {
throw "User not found"; throw 'User not found';
} }
} catch (error) { } catch (error) {
throw { status: 401, message: error }; throw { status: 401, message: error };
@@ -404,17 +424,15 @@ export async function getCurrentUser(
try { try {
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: { id: request.user.userId, teams: { some: { id: teamId } } }, where: { id: request.user.userId, teams: { some: { id: teamId } } },
include: { teams: true, permission: true }, include: { teams: true, permission: true }
}); });
if (user) { if (user) {
const permission = user.permission.find( const permission = user.permission.find((p) => p.teamId === teamId).permission;
(p) => p.teamId === teamId
).permission;
const payload = { const payload = {
...request.user, ...request.user,
teamId, teamId,
permission: permission || null, permission: permission || null,
isAdmin: permission === "owner" || permission === "admin", isAdmin: permission === 'owner' || permission === 'admin'
}; };
token = fastify.jwt.sign(payload); token = fastify.jwt.sign(payload);
} }
@@ -422,12 +440,14 @@ export async function getCurrentUser(
// No new token -> not switching teams // No new token -> not switching teams
} }
} }
const pendingInvitations = await prisma.teamInvitation.findMany({ where: { uid: request.user.userId } }) const pendingInvitations = await prisma.teamInvitation.findMany({
where: { uid: request.user.userId }
});
return { return {
settings: await prisma.setting.findUnique({ where: { id: "0" } }), settings: await prisma.setting.findUnique({ where: { id: '0' } }),
sentryDSN, sentryDSN,
pendingInvitations, pendingInvitations,
token, token,
...request.user, ...request.user
}; };
} }

File diff suppressed because it is too large Load Diff

View File

@@ -22,11 +22,11 @@ export async function listSources(request: FastifyRequest) {
export async function saveSource(request, reply) { export async function saveSource(request, reply) {
try { try {
const { id } = request.params const { id } = request.params
let { name, htmlUrl, apiUrl, customPort, isSystemWide } = request.body let { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide } = request.body
if (customPort) customPort = Number(customPort) if (customPort) customPort = Number(customPort)
await prisma.gitSource.update({ await prisma.gitSource.update({
where: { id }, where: { id },
data: { name, htmlUrl, apiUrl, customPort, isSystemWide } data: { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide }
}); });
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
@@ -48,6 +48,7 @@ export async function getSource(request: FastifyRequest<OnlyId>) {
apiUrl: null, apiUrl: null,
organization: null, organization: null,
customPort: 22, customPort: 22,
customUser: 'git',
}, },
settings settings
} }
@@ -133,7 +134,7 @@ export async function saveGitLabSource(request: FastifyRequest<SaveGitLabSource>
try { try {
const { id } = request.params const { id } = request.params
const { teamId } = request.user const { teamId } = request.user
let { type, name, htmlUrl, apiUrl, oauthId, appId, appSecret, groupName, customPort } = let { type, name, htmlUrl, apiUrl, oauthId, appId, appSecret, groupName, customPort, customUser } =
request.body request.body
if (oauthId) oauthId = Number(oauthId); if (oauthId) oauthId = Number(oauthId);
@@ -142,7 +143,7 @@ export async function saveGitLabSource(request: FastifyRequest<SaveGitLabSource>
if (id === 'new') { if (id === 'new') {
const newId = cuid() const newId = cuid()
await prisma.gitSource.create({ data: { id: newId, type, apiUrl, htmlUrl, name, customPort, teams: { connect: { id: teamId } } } }); await prisma.gitSource.create({ data: { id: newId, type, apiUrl, htmlUrl, name, customPort, customUser, teams: { connect: { id: teamId } } } });
await prisma.gitlabApp.create({ await prisma.gitlabApp.create({
data: { data: {
teams: { connect: { id: teamId } }, teams: { connect: { id: teamId } },
@@ -158,7 +159,7 @@ export async function saveGitLabSource(request: FastifyRequest<SaveGitLabSource>
id: newId id: newId
} }
} else { } else {
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name, customPort } }); await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name, customPort, customUser } });
await prisma.gitlabApp.update({ await prisma.gitlabApp.update({
where: { id }, where: { id },
data: { data: {

View File

@@ -21,6 +21,7 @@ export interface SaveGitLabSource extends OnlyId {
appSecret: string, appSecret: string,
groupName: string, groupName: string,
customPort: number, customPort: number,
customUser: string,
} }
} }
export interface CheckGitLabOAuthId extends OnlyId { export interface CheckGitLabOAuthId extends OnlyId {

View File

@@ -1,9 +1,31 @@
import { FastifyRequest } from "fastify"; import { FastifyRequest } from 'fastify';
import { errorHandler, getDomain, isDev, prisma, executeCommand } from "../../../lib/common"; import { errorHandler, getDomain, isDev, prisma, executeCommand } from '../../../lib/common';
import { getTemplates } from "../../../lib/services"; import { getTemplates } from '../../../lib/services';
import { OnlyId } from "../../../types"; import { OnlyId } from '../../../types';
function generateServices(serviceId, containerId, port) { function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps = false) {
if (isHttp2) {
return {
[serviceId]: {
loadbalancer: {
servers: [
{
url: `${isHttps ? 'https' : 'http'}://${containerId}:${port}`
}
]
}
},
[`${serviceId}-http2`]: {
loadbalancer: {
servers: [
{
url: `h2c://${containerId}:${port}`
}
]
}
}
};
}
return { return {
[serviceId]: { [serviceId]: {
loadbalancer: { loadbalancer: {
@@ -14,43 +36,54 @@ function generateServices(serviceId, containerId, port) {
] ]
} }
} }
} };
} }
function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, isDualCerts, isCustomSSL) { function generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
isDualCerts,
isCustomSSL,
isHttp2 = false
) {
let rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`;
let http: any = { let http: any = {
entrypoints: ['web'], entrypoints: ['web'],
rule: `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`, rule,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
middlewares: [] middlewares: []
} };
let https: any = { let https: any = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`, rule,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
tls: { tls: {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
} };
let httpWWW: any = { let httpWWW: any = {
entrypoints: ['web'], entrypoints: ['web'],
rule: `Host(\`www.${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`, rule,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
middlewares: [] middlewares: []
} };
let httpsWWW: any = { let httpsWWW: any = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`, rule,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
tls: { tls: {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
} };
// 2. http + non-www only // 2. http + non-www only
if (!isHttps && !isWWW) { if (!isHttps && !isWWW) {
https.middlewares.push('redirect-to-http'); https.middlewares.push('redirect-to-http');
@@ -58,19 +91,19 @@ function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, is
httpWWW.middlewares.push('redirect-to-non-www'); httpWWW.middlewares.push('redirect-to-non-www');
httpsWWW.middlewares.push('redirect-to-non-www'); httpsWWW.middlewares.push('redirect-to-non-www');
delete https.tls delete https.tls;
delete httpsWWW.tls delete httpsWWW.tls;
} }
// 3. http + www only // 3. http + www only
if (!isHttps && isWWW) { if (!isHttps && isWWW) {
https.middlewares.push('redirect-to-http'); https.middlewares.push('redirect-to-http');
httpsWWW.middlewares.push('redirect-to-http'); httpsWWW.middlewares.push('redirect-to-http');
http.middlewares.push('redirect-to-www'); http.middlewares.push('redirect-to-www');
https.middlewares.push('redirect-to-www'); https.middlewares.push('redirect-to-www');
delete https.tls delete https.tls;
delete httpsWWW.tls delete httpsWWW.tls;
} }
// 5. https + non-www only // 5. https + non-www only
if (isHttps && !isWWW) { if (isHttps && !isWWW) {
@@ -86,17 +119,17 @@ function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, is
httpsWWW.tls = true; httpsWWW.tls = true;
} else { } else {
https.tls = true; https.tls = true;
delete httpsWWW.tls.certresolver delete httpsWWW.tls.certresolver;
httpsWWW.tls.domains = { httpsWWW.tls.domains = {
main: domain main: domain
} };
} }
} else { } else {
if (!isDualCerts) { if (!isDualCerts) {
delete httpsWWW.tls.certresolver delete httpsWWW.tls.certresolver;
httpsWWW.tls.domains = { httpsWWW.tls.domains = {
main: domain main: domain
} };
} }
} }
} }
@@ -114,26 +147,59 @@ function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, is
httpsWWW.tls = true; httpsWWW.tls = true;
} else { } else {
httpsWWW.tls = true; httpsWWW.tls = true;
delete https.tls.certresolver delete https.tls.certresolver;
https.tls.domains = { https.tls.domains = {
main: domain main: domain
} };
} }
} else { } else {
if (!isDualCerts) { if (!isDualCerts) {
delete https.tls.certresolver delete https.tls.certresolver;
https.tls.domains = { https.tls.domains = {
main: domain main: domain
} };
} }
} }
} }
if (isHttp2) {
let http2 = {
...http,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let http2WWW = {
...httpWWW,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let https2 = {
...https,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let https2WWW = {
...httpsWWW,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
return {
[`${serviceId}-${pathPrefix}`]: { ...http },
[`${serviceId}-${pathPrefix}-http2`]: { ...http2 },
[`${serviceId}-${pathPrefix}-secure`]: { ...https },
[`${serviceId}-${pathPrefix}-secure-http2`]: { ...https2 },
[`${serviceId}-${pathPrefix}-www`]: { ...httpWWW },
[`${serviceId}-${pathPrefix}-www-http2`]: { ...http2WWW },
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW },
[`${serviceId}-${pathPrefix}-secure-www-http2`]: { ...https2WWW }
};
}
return { return {
[`${serviceId}-${pathPrefix}`]: { ...http }, [`${serviceId}-${pathPrefix}`]: { ...http },
[`${serviceId}-${pathPrefix}-secure`]: { ...https }, [`${serviceId}-${pathPrefix}-secure`]: { ...https },
[`${serviceId}-${pathPrefix}-www`]: { ...httpWWW }, [`${serviceId}-${pathPrefix}-www`]: { ...httpWWW },
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW }, [`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW }
} };
} }
export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote: boolean = false) { export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote: boolean = false) {
const traefik = { const traefik = {
@@ -174,26 +240,26 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const coolifySettings = await prisma.setting.findFirst(); const coolifySettings = await prisma.setting.findFirst();
if (coolifySettings.isTraefikUsed && coolifySettings.proxyDefaultRedirect) { if (coolifySettings.isTraefikUsed && coolifySettings.proxyDefaultRedirect) {
traefik.http.routers['catchall-http'] = { traefik.http.routers['catchall-http'] = {
entrypoints: ["web"], entrypoints: ['web'],
rule: "HostRegexp(`{catchall:.*}`)", rule: 'HostRegexp(`{catchall:.*}`)',
service: "noop", service: 'noop',
priority: 1, priority: 1,
middlewares: ["redirect-regexp"] middlewares: ['redirect-regexp']
} };
traefik.http.routers['catchall-https'] = { traefik.http.routers['catchall-https'] = {
entrypoints: ["websecure"], entrypoints: ['websecure'],
rule: "HostRegexp(`{catchall:.*}`)", rule: 'HostRegexp(`{catchall:.*}`)',
service: "noop", service: 'noop',
priority: 1, priority: 1,
middlewares: ["redirect-regexp"] middlewares: ['redirect-regexp']
} };
traefik.http.middlewares['redirect-regexp'] = { traefik.http.middlewares['redirect-regexp'] = {
redirectregex: { redirectregex: {
regex: '(.*)', regex: '(.*)',
replacement: coolifySettings.proxyDefaultRedirect, replacement: coolifySettings.proxyDefaultRedirect,
permanent: false permanent: false
} }
} };
traefik.http.services['noop'] = { traefik.http.services['noop'] = {
loadBalancer: { loadBalancer: {
servers: [ servers: [
@@ -202,25 +268,41 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
} }
] ]
} }
} };
} }
const sslpath = '/etc/traefik/acme/custom'; const sslpath = '/etc/traefik/acme/custom';
let certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } } } } }) let certificates = await prisma.certificate.findMany({
where: {
team: {
applications: { some: { settings: { isCustomSSL: true } } },
destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } }
}
}
});
if (remote) { if (remote) {
certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } }) certificates = await prisma.certificate.findMany({
where: {
team: {
applications: { some: { settings: { isCustomSSL: true } } },
destinationDocker: {
some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true }
}
}
}
});
} }
let parsedCertificates = [] let parsedCertificates = [];
for (const certificate of certificates) { for (const certificate of certificates) {
parsedCertificates.push({ parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`, certFile: `${sslpath}/${certificate.id}-cert.pem`,
keyFile: `${sslpath}/${certificate.id}-key.pem` keyFile: `${sslpath}/${certificate.id}-key.pem`
}) });
} }
if (parsedCertificates.length > 0) { if (parsedCertificates.length > 0) {
traefik.tls.certificates = parsedCertificates traefik.tls.certificates = parsedCertificates;
} }
let applications = []; let applications = [];
@@ -236,7 +318,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
destinationDocker: true, destinationDocker: true,
persistentStorage: true, persistentStorage: true,
serviceSecret: true, serviceSecret: true,
serviceSetting: true, serviceSetting: true
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'desc' }
}); });
@@ -251,23 +333,25 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
destinationDocker: true, destinationDocker: true,
persistentStorage: true, persistentStorage: true,
serviceSecret: true, serviceSecret: true,
serviceSetting: true, serviceSetting: true
}, },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' }
}); });
} }
if (applications.length > 0) { if (applications.length > 0) {
const dockerIds = new Set() const dockerIds = new Set();
const runningContainers = {} const runningContainers = {};
applications.forEach((app) => dockerIds.add(app.destinationDocker.id)); applications.forEach((app) => dockerIds.add(app.destinationDocker.id));
for (const dockerId of dockerIds) { for (const dockerId of dockerIds) {
const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) const { stdout: container } = await executeCommand({
dockerId,
command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'`
});
if (container) { if (container) {
const containersArray = container.trim().split('\n'); const containersArray = container.trim().split('\n');
if (containersArray.length > 0) { if (containersArray.length > 0) {
runningContainers[dockerId] = containersArray runningContainers[dockerId] = containersArray;
} }
} }
} }
@@ -289,38 +373,54 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
if ( if (
!runningContainers[destinationDockerId] || !runningContainers[destinationDockerId] ||
runningContainers[destinationDockerId].length === 0 || runningContainers[destinationDockerId].length === 0 ||
runningContainers[destinationDockerId].filter((container) => container.startsWith(id)).length === 0 runningContainers[destinationDockerId].filter((container) => container.startsWith(id))
.length === 0
) { ) {
continue continue;
} }
if (buildPack === 'compose') { if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration)) const services = Object.entries(JSON.parse(dockerComposeConfiguration));
if (services.length > 0) { if (services.length > 0) {
for (const service of services) { for (const service of services) {
const [key, value] = service const [key, value] = service;
if (key && value) { if (key && value) {
if (!value.fqdn || !value.port) { if (!value.fqdn || !value.port) {
continue; continue;
} }
const { fqdn, port } = value const { fqdn, port } = value;
const containerId = `${id}-${key}` const containerId = `${id}-${key}`;
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const pathPrefix = '/' const pathPrefix = '/';
const isCustomSSL = false; const isCustomSSL = false;
const dualCerts = false; const dualCerts = false;
const serviceId = `${id}-${port || 'default'}` const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, containerId, port) } ...traefik.http.routers,
...generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, containerId, port)
};
} }
} }
} }
continue; continue;
} }
const { previews, dualCerts, isCustomSSL } = settings; const { previews, dualCerts, isCustomSSL, isHttp2 } = settings;
const { network, id: dockerId } = destinationDocker; const { network, id: dockerId } = destinationDocker;
if (!fqdn) { if (!fqdn) {
continue; continue;
@@ -329,12 +429,31 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const pathPrefix = '/' const pathPrefix = '/';
const serviceId = `${id}-${port || 'default'}` const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) } ...traefik.http.routers,
...generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL,
isHttp2
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, id, port, isHttp2, isHttps)
};
if (previews) { if (previews) {
const { stdout } = await executeCommand({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` }) const { stdout } = await executeCommand({
dockerId,
command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
});
if (stdout) { if (stdout) {
const containers = stdout const containers = stdout
.trim() .trim()
@@ -343,44 +462,57 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
.map((c) => c.replace(/"/g, '')); .map((c) => c.replace(/"/g, ''));
if (containers.length > 0) { if (containers.length > 0) {
for (const container of containers) { for (const container of containers) {
const previewDomain = `${container.split('-')[1]}${coolifySettings.previewSeparator}${domain}`; const previewDomain = `${container.split('-')[1]}${
coolifySettings.previewSeparator
}${domain}`;
const nakedDomain = previewDomain.replace(/^www\./, ''); const nakedDomain = previewDomain.replace(/^www\./, '');
const pathPrefix = '/' const pathPrefix = '/';
const serviceId = `${container}-${port || 'default'}` const serviceId = `${container}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, previewDomain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) } ...traefik.http.routers,
...generateRouters(
serviceId,
previewDomain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, container, port, isHttp2)
};
} }
} }
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} }
} }
if (services.length > 0) { if (services.length > 0) {
const dockerIds = new Set() const dockerIds = new Set();
const runningContainers = {} const runningContainers = {};
services.forEach((app) => dockerIds.add(app.destinationDocker.id)); services.forEach((app) => dockerIds.add(app.destinationDocker.id));
for (const dockerId of dockerIds) { for (const dockerId of dockerIds) {
const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) const { stdout: container } = await executeCommand({
dockerId,
command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'`
});
if (container) { if (container) {
const containersArray = container.trim().split('\n'); const containersArray = container.trim().split('\n');
if (containersArray.length > 0) { if (containersArray.length > 0) {
runningContainers[dockerId] = containersArray runningContainers[dockerId] = containersArray;
} }
} }
} }
for (const service of services) { for (const service of services) {
try { try {
let { let { fqdn, id, type, destinationDockerId, dualCerts, serviceSetting } = service;
fqdn,
id,
type,
destinationDockerId,
dualCerts,
serviceSetting
} = service;
if (!fqdn) { if (!fqdn) {
continue; continue;
} }
@@ -392,7 +524,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
runningContainers[destinationDockerId].length === 0 || runningContainers[destinationDockerId].length === 0 ||
!runningContainers[destinationDockerId].includes(id) !runningContainers[destinationDockerId].includes(id)
) { ) {
continue continue;
} }
const templates = await getTemplates(); const templates = await getTemplates();
let found = templates.find((a) => a.type === type); let found = templates.find((a) => a.type === type);
@@ -401,88 +533,143 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
} }
found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id)); found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id));
for (const oneService of Object.keys(found.services)) { for (const oneService of Object.keys(found.services)) {
const isDomainConfiguration = found?.services[oneService]?.proxy?.filter(p => p.domain) ?? []; const isDomainConfiguration =
found?.services[oneService]?.proxy?.filter((p) => p.domain) ?? [];
if (isDomainConfiguration.length > 0) { if (isDomainConfiguration.length > 0) {
const { proxy } = found.services[oneService]; const { proxy } = found.services[oneService];
for (let configuration of proxy) { for (let configuration of proxy) {
if (configuration.domain) { if (configuration.domain) {
const setting = serviceSetting.find((a) => a.variableName === configuration.domain); const setting = serviceSetting.find(
(a) => a.variableName === configuration.domain
);
if (setting) { if (setting) {
configuration.domain = configuration.domain.replace(configuration.domain, setting.value); configuration.domain = configuration.domain.replace(
configuration.domain,
setting.value
);
} }
} }
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port') const foundPortVariable = serviceSetting.find(
(a) => a.name.toLowerCase() === 'port'
);
if (foundPortVariable) { if (foundPortVariable) {
configuration.port = foundPortVariable.value configuration.port = foundPortVariable.value;
} }
let port, pathPrefix, customDomain; let port, pathPrefix, customDomain;
if (configuration) { if (configuration) {
port = configuration?.port; port = configuration?.port;
pathPrefix = configuration?.pathPrefix || '/'; pathPrefix = configuration?.pathPrefix || '/';
customDomain = configuration?.domain customDomain = configuration?.domain;
} }
if (customDomain) { if (customDomain) {
fqdn = customDomain fqdn = customDomain;
} else { } else {
fqdn = service.fqdn fqdn = service.fqdn;
} }
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const isCustomSSL = false; const isCustomSSL = false;
const serviceId = `${oneService}-${port || 'default'}` const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, oneService, port) } ...traefik.http.routers,
...generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, oneService, port)
};
} }
} else { } else {
if (found.services[oneService].ports && found.services[oneService].ports.length > 0) { if (found.services[oneService].ports && found.services[oneService].ports.length > 0) {
for (let [index, port] of found.services[oneService].ports.entries()) { for (let [index, port] of found.services[oneService].ports.entries()) {
if (port == 22) continue; if (port == 22) continue;
if (index === 0) { if (index === 0) {
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port') const foundPortVariable = serviceSetting.find(
(a) => a.name.toLowerCase() === 'port'
);
if (foundPortVariable) { if (foundPortVariable) {
port = foundPortVariable.value port = foundPortVariable.value;
} }
} }
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const pathPrefix = '/' const pathPrefix = '/';
const isCustomSSL = false const isCustomSSL = false;
const serviceId = `${oneService}-${port || 'default'}` const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) } ...traefik.http.routers,
...generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, id, port)
};
} }
} }
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} }
} }
if (!remote) { if (!remote) {
const { fqdn, dualCerts } = await prisma.setting.findFirst(); const { fqdn, dualCerts } = await prisma.setting.findFirst();
if (!fqdn) { if (!fqdn) {
return return;
} }
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const id = isDev ? 'host.docker.internal' : 'coolify' const id = isDev ? 'host.docker.internal' : 'coolify';
const container = isDev ? 'host.docker.internal' : 'coolify' const container = isDev ? 'host.docker.internal' : 'coolify';
const port = 3000 const port = 3000;
const pathPrefix = '/' const pathPrefix = '/';
const isCustomSSL = false const isCustomSSL = false;
const serviceId = `${id}-${port || 'default'}` const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) } ...traefik.http.routers,
...generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, container, port)
};
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} finally { } finally {
if (Object.keys(traefik.http.routers).length === 0) { if (Object.keys(traefik.http.routers).length === 0) {
traefik.http.routers = null; traefik.http.routers = null;
@@ -496,9 +683,9 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
export async function otherProxyConfiguration(request: FastifyRequest<TraefikOtherConfiguration>) { export async function otherProxyConfiguration(request: FastifyRequest<TraefikOtherConfiguration>) {
try { try {
const { id } = request.query const { id } = request.query;
if (id) { if (id) {
const { privatePort, publicPort, type, address = id } = request.query const { privatePort, publicPort, type, address = id } = request.query;
let traefik = {}; let traefik = {};
if (publicPort && type && privatePort) { if (publicPort && type && privatePort) {
if (type === 'tcp') { if (type === 'tcp') {
@@ -559,18 +746,18 @@ export async function otherProxyConfiguration(request: FastifyRequest<TraefikOth
} }
} }
} else { } else {
throw { status: 500 } throw { status: 500 };
} }
} }
} else { } else {
throw { status: 500 } throw { status: 500 };
} }
return { return {
...traefik ...traefik
}; };
} }
throw { status: 500 } throw { status: 500 };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,15 @@ import { addToast } from './store';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
export const asyncSleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)); export const asyncSleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay));
export function dashify(str: string, options?: any): string {
if (typeof str !== 'string') return str;
return str
.trim()
.replace(/\W/g, (m) => (/[À-ž]/.test(m) ? m : '-'))
.replace(/^-+|-+$/g, '')
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase();
}
export function errorNotification(error: any | { message: string }): void { export function errorNotification(error: any | { message: string }): void {
if (error instanceof Error) { if (error instanceof Error) {
console.error(error.message) console.error(error.message)

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import ExternalLink from './ExternalLink.svelte';
import Tooltip from './Tooltip.svelte';
export let url = 'https://docs.coollabs.io';
export let text: any = '';
export let isExternal = false;
let id =
'cool-' +
url
.split('')
.map((c) => c.charCodeAt(0).toString(16).padStart(2, '0'))
.join('')
.slice(-16);
</script>
<a
{id}
href={url}
target="_blank noreferrer"
class="flex no-underline inline-block cursor-pointer"
class:icons={!text}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
/>
</svg>
{text}
{#if isExternal}
<ExternalLink />
{/if}
</a>
{#if !text}
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>
{/if}

View File

@@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
class="w-3 h-3 text-white"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25" />
</svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -0,0 +1,6 @@
<script lang="ts">
export let text: string;
export let customClass = 'max-w-[24rem]';
</script>
<div class="p-2 text-xs text-stone-400 {customClass}">{@html text}</div>

View File

@@ -5,6 +5,9 @@
const handleError = (ev: { target: { src: string } }) => (ev.target.src = fallback); const handleError = (ev: { target: { src: string } }) => (ev.target.src = fallback);
let extension = 'png'; let extension = 'png';
let svgs = [ let svgs = [
'mattermost',
'repman',
'directus',
'pocketbase', 'pocketbase',
'gitea', 'gitea',
'languagetool', 'languagetool',

View File

@@ -42,6 +42,7 @@ interface AppSession {
gitlab: string | null; gitlab: string | null;
}; };
pendingInvitations: Array<any>; pendingInvitations: Array<any>;
isARM: boolean
} }
export const appSession: Writable<AppSession> = writable({ export const appSession: Writable<AppSession> = writable({
@@ -61,7 +62,8 @@ export const appSession: Writable<AppSession> = writable({
github: null, github: null,
gitlab: null gitlab: null
}, },
pendingInvitations: [] pendingInvitations: [],
isARM: false
}); });
interface AddToast { interface AddToast {
@@ -171,3 +173,11 @@ export const setLocation = (resource: any, settings?: any) => {
} }
}; };
export const selectedBuildId: any = writable(null) export const selectedBuildId: any = writable(null)
export function checkIfDeploymentEnabledServices( service: any) {
return (
service.fqdn &&
service.destinationDocker &&
service.version &&
service.type
);
}

View File

@@ -61,6 +61,7 @@
let isBot = application.settings?.isBot; let isBot = application.settings?.isBot;
let isDBBranching = application.settings?.isDBBranching; let isDBBranching = application.settings?.isDBBranching;
let htmlUrl = application.gitSource?.htmlUrl; let htmlUrl = application.gitSource?.htmlUrl;
let isHttp2 = application.settings?.isHttp2;
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null; let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
let dockerComposeServices: any[] = []; let dockerComposeServices: any[] = [];
@@ -177,6 +178,9 @@
if (name === 'isDBBranching') { if (name === 'isDBBranching') {
isDBBranching = !isDBBranching; isDBBranching = !isDBBranching;
} }
if (name === 'isHttp2') {
isHttp2 = !isHttp2;
}
try { try {
await trpc.applications.saveSettings.mutate({ await trpc.applications.saveSettings.mutate({
id: application.id, id: application.id,
@@ -351,12 +355,17 @@
} }
async function reloadCompose() { async function reloadCompose() {
if (loading.reloadCompose) return; if (loading.reloadCompose) return;
if (!$appSession.tokens.github && !isPublicRepository) {
const { token } = await get(`/applications/${id}/configuration/githubToken`);
$appSession.tokens.github = token;
}
loading.reloadCompose = true; loading.reloadCompose = true;
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation
: `/${application.dockerComposeFileLocation}`;
try { try {
if (application.gitSource.type === 'github') { if (application.gitSource.type === 'github') {
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation
: `/${application.dockerComposeFileLocation}`;
const headers = isPublicRepository const headers = isPublicRepository
? {} ? {}
: { : {
@@ -380,9 +389,20 @@
} }
} }
if (application.gitSource.type === 'gitlab') { if (application.gitSource.type === 'gitlab') {
if (!$appSession.tokens.gitlab) { if (!$appSession.tokens.gitlab && !isPublicRepository) {
await getGitlabToken(); await getGitlabToken();
} }
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation.substring(1) // Remove the '/' from the start
: application.dockerComposeFileLocation;
// If the file is in a subdirectory, lastIndex will be > 0
// Otherwise it will be -1 and path will be an empty string
const lastIndex = composeLocation.lastIndexOf('/') + 1;
const path = composeLocation.substring(0, lastIndex);
const fileName = composeLocation.substring(lastIndex);
const headers = isPublicRepository const headers = isPublicRepository
? {} ? {}
: { : {
@@ -390,13 +410,12 @@
}; };
const url = isPublicRepository const url = isPublicRepository
? `` ? ``
: `/v4/projects/${application.projectId}/repository/tree`; : `/v4/projects/${application.projectId}/repository/tree?path=${path}`; // Use path param to find file in a subdirectory
const files = await get(`${apiUrl}${url}`, { const files = await get(`${apiUrl}${url}`, {
...headers ...headers
}); });
const dockerComposeFileYml = files.find( const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) => (file: { name: string; type: string }) => file.name === fileName && file.type === 'blob'
file.name === composeLocation && file.type === 'blob'
); );
const id = dockerComposeFileYml.id; const id = dockerComposeFileYml.id;
@@ -698,7 +717,7 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center pb-4"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="dualCerts" id="dualCerts"
dataTooltip="Must be stopped to modify." dataTooltip="Must be stopped to modify."
@@ -722,6 +741,16 @@
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
id="isHttp2"
isCenter={false}
bind:setting={isHttp2}
title="Enable HTTP/2 protocol?"
description="Enable HTTP/2 protocol. <br><br>HTTP/2 is a major revision of the HTTP network protocol used by the World Wide Web that allows faster web page loading by reducing the number of requests needed to load a web page.<br><br>Useful for gRPC and other HTTP/2 based services."
on:click={() => changeSettings('isHttp2')}
/>
</div>
{/if} {/if}
</div> </div>
{#if isSimpleDockerfile} {#if isSimpleDockerfile}

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let application: any = data.application.data;
import { page } from '$app/stores';
import { appSession, status, trpc } from '$lib/store';
import { errorNotification } from '$lib/common';
import { goto } from '$app/navigation';
const { id } = $page.params;
let forceDelete = false;
async function deleteApplication(name: string, force: boolean) {
const sure = confirm('Are you sure you want to delete this application?');
if (sure) {
$status.application.initialLoading = true;
try {
await trpc.applications.deleteApplication.mutate({ id, force });
return await goto('/');
} catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
forceDelete = true;
}
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
}
}
}
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Danger Zone</div>
</div>
{#if forceDelete}
<button
id="forcedelete"
on:click={() => deleteApplication(application.name, true)}
type="submit"
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-lg btn-error hover:bg-red-700 text-sm w-64"
>
Force Delete Application
</button>
{:else}
<button
id="delete"
on:click={() => deleteApplication(application.name, false)}
type="submit"
disabled={!$appSession.isAdmin}
class="btn btn-lg btn-error hover:bg-red-700 text-sm w-64"
>
Delete Application
</button>
{/if}
</div>

View File

@@ -21,7 +21,7 @@
onMount(async () => { onMount(async () => {
const { data } = await trpc.applications.getApplicationById.query({ id }); const { data } = await trpc.applications.getApplicationById.query({ id });
application = data; application = data;
if (data.dockerComposeFile) { if (application.dockerComposeFile && application.buildPack === 'compose') {
services = normalizeDockerServices(JSON.parse(data.dockerComposeFile).services); services = normalizeDockerServices(JSON.parse(data.dockerComposeFile).services);
} else { } else {
services = [ services = [

View File

@@ -0,0 +1,323 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let application: any = data.application.data;
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { asyncSleep, errorNotification, getRndInteger } from '$lib/common';
import { onDestroy, onMount } from 'svelte';
import { addToast, appSession, trpc } from '$lib/store';
import Tooltip from '$lib/components/Tooltip.svelte';
import * as Icons from '$lib/components/icons';
const { id } = $page.params;
let loadBuildingStatusInterval: any = null;
let loading = {
init: true,
restart: false,
removing: false
};
let numberOfGetStatus = 0;
let status: any = {};
async function removeApplication(preview: any) {
try {
loading.removing = true;
await trpc.applications.stopPreview.mutate({
id,
pullmergeRequestId: preview.pullmergeRequestId
});
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
async function redeploy(preview: any) {
try {
const { buildId } = await trpc.applications.deploy.mutate({
id,
pullmergeRequestId: preview.pullmergeRequestId,
branch: preview.sourceBranch
});
addToast({
message: 'Deployment queued.',
type: 'success'
});
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
replaceState: true
});
}
} catch (error) {
return errorNotification(error);
}
}
async function loadPreviewsFromDocker() {
try {
const { data } = await trpc.applications.loadPreviews.mutate({ id });
application.previewApplication = data.previews;
addToast({
message: 'Previews loaded.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function getStatus(resources: any) {
const { applicationId, pullmergeRequestId, id } = resources;
if (status[id]) return status[id];
while (numberOfGetStatus > 1) {
await asyncSleep(getRndInteger(100, 200));
}
try {
numberOfGetStatus++;
let isRunning = false;
let isBuilding = false;
const { data } = await trpc.applications.getPreviewStatus.query({
id: applicationId,
pullmergeRequestId
});
isRunning = data.isRunning;
isBuilding = data.isBuilding;
if (isBuilding) {
status[id] = 'building';
return 'building';
} else if (isRunning) {
status[id] = 'running';
return 'running';
} else {
status[id] = 'stopped';
return 'stopped';
}
} catch (error) {
status[id] = 'error';
return 'error';
} finally {
numberOfGetStatus--;
status = status;
}
}
async function restartPreview(preview: any) {
try {
loading.restart = true;
const { pullmergeRequestId } = preview;
await trpc.applications.restartPreview.mutate({ id, pullmergeRequestId });
addToast({
type: 'success',
message: 'Restart successful.'
});
} catch (error) {
return errorNotification(error);
} finally {
await getStatus(preview);
loading.restart = false;
}
}
onDestroy(() => {
clearInterval(loadBuildingStatusInterval);
});
onMount(async () => {
loadBuildingStatusInterval = setInterval(() => {
application.previewApplication.forEach(async (preview: any) => {
const { applicationId, pullmergeRequestId } = preview;
if (status[preview.id] === 'building') {
const { data } = await trpc.applications.getPreviewStatus.query({
id: applicationId,
pullmergeRequestId
});
if (data.isBuilding) {
status[preview.id] = 'building';
} else if (data.isRunning) {
status[preview.id] = 'running';
return 'running';
} else {
status[preview.id] = 'stopped';
return 'stopped';
}
}
});
}, 2000);
try {
loading.init = true;
loading.restart = true;
} catch (error) {
return errorNotification(error);
} finally {
loading.init = false;
loading.restart = false;
}
});
</script>
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Preview Deployments</div>
<div class="text-center">
<button class="btn btn-sm bg-coollabs" on:click={loadPreviewsFromDocker}
>Load Previews</button
>
</div>
</div>
</div>
</div>
{#if loading.init}
<div class="px-6 pt-4">
<div class="flex justify-center py-4 text-center text-xl font-bold">Loading...</div>
</div>
{:else if application.previewApplication.length > 0}
<div class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-2 px-6">
{#each application.previewApplication as preview}
<div class="no-underline mb-5 w-full">
<div class="w-full rounded p-5 bg-coolgray-200 indicator">
{#await getStatus(preview)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
{:then}
{#if status[preview.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/await}
<div class="w-full flex flex-row">
<div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">
PR #{preview.pullmergeRequestId}
{#if status[preview.id] === 'building'}
<span
class="badge badge-sm text-xs uppercase rounded bg-coolgray-300 text-green-500 border-none font-bold"
>
BUILDING
</span>
{/if}
</h1>
<div class="h-10 text-xs">
<h2>{preview.customDomain.replace('https://', '').replace('http://', '')}</h2>
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if preview.customDomain}
<a
id="openpreview"
href={preview.customDomain}
target="_blank noreferrer"
class="icons"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg>
</a>
{/if}
<Tooltip triggeredBy="#openpreview">Open Preview</Tooltip>
{#if loading.restart}
<button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else}
<button
id="restart"
disabled={!$appSession.isAdmin}
on:click={() => restartPreview(preview)}
type="submit"
class="icons bg-transparent text-sm flex items-center space-x-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg>
</button>
{/if}
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<button
id="forceredeploypreview"
class="icons"
disabled={!$appSession.isAdmin}
on:click={() => redeploy(preview)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
</svg></button
>
<Tooltip triggeredBy="#forceredeploypreview">Force redeploy (without cache)</Tooltip
>
<button
id="deletepreview"
class="icons"
class:hover:text-error={!loading.removing}
disabled={loading.removing || !$appSession.isAdmin}
on:click={() => removeApplication(preview)}
><Icons.Delete />
</button>
<Tooltip triggeredBy="#deletepreview">Delete Preview</Tooltip>
</div>
</div>
</div>
</div>
</div>
{/each}
</div>
{:else}
No previews found.
{/if}

View File

@@ -0,0 +1,151 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let application: any = data.application.data;
let imagesAvailables: any = data.imagesAvailables;
let runningImage: any = data.runningImage;
import { page } from '$app/stores';
import { status, addToast, trpc } from '$lib/store';
import { errorNotification } from '$lib/common';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
let remoteImage: any = null;
async function revertToLocal(image: any) {
const sure = confirm(`Are you sure you want to revert to ${image.tag} ?`);
if (sure) {
try {
$status.application.initialLoading = true;
$status.application.loading = true;
const imageId = `${image.repository}:${image.tag}`;
await trpc.applications.restart.mutate({ id, imageId });
// await post(`/applications/${id}/restart`, { imageId });
addToast({
type: 'success',
message: 'Revert successful.'
});
} catch (error) {
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
$status.application.loading = false;
}
}
}
async function revertToRemote() {
const sure = confirm(`Are you sure you want to revert to ${remoteImage} ?`);
if (sure) {
try {
$status.application.initialLoading = true;
$status.application.loading = true;
$status.application.restarting = true;
await trpc.applications.restart.mutate({ id, imageId: remoteImage });
addToast({
type: 'success',
message: 'Revert successful.'
});
} catch (error) {
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
$status.application.loading = false;
$status.application.restarting = false;
}
}
}
</script>
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">
Revert <Explainer
position="dropdown-bottom"
explanation="You can revert application to a previously built image. Currently only locally stored images
supported."
/>
</div>
</div>
<div class="pb-4 text-xs">
If you do not want the next commit to overwrite the reverted application, temporary disable <span
class="text-yellow-400 font-bold">Automatic Deployment</span
>
feature <a href={`/applications/${id}/features`}>here</a>.
</div>
{#if imagesAvailables.length > 0}
<div class="text-xl font-bold pb-3">Local Images</div>
<div
class="px-4 lg:pb-10 pb-6 flex flex-wrap items-center justify-center lg:justify-start gap-8"
>
{#each imagesAvailables as image}
<div class="gap-2 py-4 m-2">
<div class="flex flex-col justify-center items-center">
<div class="text-xl font-bold">
{image.tag}
</div>
<div>
<a
class="flex no-underline text-xs my-4"
href="{application.gitSource.htmlUrl}/{application.repository}/commit/{image.tag}"
target="_blank noreferrer"
>
<button class="btn btn-sm">
Check Commit
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
class="w-3 h-3 text-white ml-2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25"
/>
</svg>
</button></a
>
{#if image.repository + ':' + image.tag !== runningImage}
<button
class="btn btn-sm btn-primary w-full"
on:click={() => revertToLocal(image)}>Revert Now</button
>
{:else}
<button
class="btn btn-sm btn-primary w-full btn-disabled bg-transparent underline"
>Currently Used</button
>
{/if}
</div>
</div>
</div>
{/each}
</div>
{:else}
<div class="flex flex-col pb-10">
<div class="text-xl font-bold">No Local images available</div>
</div>
{/if}
<div class="text-xl font-bold pb-3">
Remote Images (Docker Registry) <Explainer
position="dropdown-bottom"
explanation="If the image is not available or you are unauthorized to access it, you will not be able to revert to it."
/>
</div>
<form on:submit|preventDefault={revertToRemote}>
<input
id="dockerImage"
name="dockerImage"
required
placeholder="coollabsio/coolify:0.0.1"
bind:value={remoteImage}
/>
<button class="btn btn-sm btn-primary" type="submit">Revert Now</button>
</form>
</div>
</div>

View File

@@ -0,0 +1,16 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { PageLoad } from './$types';
export const ssr = false;
export const load: PageLoad = async ({ params }) => {
try {
const { id } = params;
const { data } = await trpc.applications.getLocalImages.query({ id });
return data;
} catch (err) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,116 @@
<script lang="ts">
import type { PageParentData } from './$types';
export let data: PageParentData;
let application: any = data.application.data;
import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import { trpc } from '$lib/store';
const { id } = $page.params;
let services: any = [];
let selectedService: any = null;
let usageLoading = false;
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval: any;
async function getUsage() {
if (usageLoading) return;
usageLoading = true;
const { data } = await trpc.applications.getUsage.query({ id, containerId: selectedService });
usage = data.usage;
usageLoading = false;
}
function normalizeDockerServices(services: any[]) {
const tempdockerComposeServices = [];
for (const [name, data] of Object.entries(services)) {
tempdockerComposeServices.push({
name,
data
});
}
return tempdockerComposeServices;
}
async function selectService(service: any, init: boolean = false) {
if (usageInterval) clearInterval(usageInterval);
usageLoading = false;
usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
selectedService = `${application.id}${service.name ? `-${service.name}` : ''}`;
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
}
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => {
if (application.dockerComposeFile && application.buildPack === 'compose') {
services = normalizeDockerServices(JSON.parse(application.dockerComposeFile).services);
} else {
services = [
{
name: ''
}
];
await selectService('');
}
});
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Monitoring</div>
</div>
</div>
<div class="flex gap-2 lg:gap-8 pb-4">
{#each services as service}
<button
on:click={() => selectService(service, true)}
class:bg-primary={selectedService ===
`${application.id}${service.name ? `-${service.name}` : ''}`}
class:bg-coolgray-200={selectedService !==
`${application.id}${service.name ? `-${service.name}` : ''}`}
class="w-full rounded p-5 hover:bg-primary font-bold"
>
{application.id}{service.name ? `-${service.name}` : ''}</button
>
{/each}
</div>
{#if selectedService}
<div class="mx-auto max-w-4xl px-6 py-4 bg-coolgray-100 border border-coolgray-200 relative">
{#if usageLoading}
<button
id="streaming"
class="btn btn-sm bg-transparent border-none loading absolute top-0 left-0 text-xs"
/>
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
<div class="text-center">
<div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div>
<div class="stat-value text-xl">{usage?.MemUsage}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Used CPU</div>
<div class="stat-value text-xl">{usage?.CPUPerc}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Network IO</div>
<div class="stat-value text-xl">{usage?.NetIO}</div>
</div>
</div>
</div>
{/if}

View File

@@ -0,0 +1,305 @@
<script lang="ts">
import type { LayoutData } from './$types';
export let data: LayoutData;
let database = data.database.data.database;
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import { appSession, status, isDeploymentEnabled, trpc } from '$lib/store';
import * as Icons from '$lib/components/icons';
import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import DatabaseLinks from './components/DatabaseLinks.svelte';
const { id } = $page.params;
$status.database.isPublic = database.settings.isPublic || false;
let statusInterval: any = false;
let forceDelete = false;
$isDeploymentEnabled = !$appSession.isAdmin;
async function deleteDatabase(force: boolean) {
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
if (sure) {
$status.database.initialLoading = true;
try {
await trpc.databases.delete.mutate({ id, force });
return await window.location.assign('/');
} catch (error) {
return errorNotification(error);
} finally {
$status.database.initialLoading = false;
}
}
}
async function stopDatabase() {
const sure = confirm(
"Are you sure you want to stop this database? You won't be able to access it until you start it again."
);
if (sure) {
$status.database.initialLoading = true;
$status.database.loading = true;
try {
await trpc.databases.stop.mutate({ id });
$status.database.isPublic = false;
} catch (error) {
return errorNotification(error);
} finally {
$status.database.initialLoading = false;
$status.database.loading = false;
await getStatus();
}
}
}
async function startDatabase() {
$status.database.initialLoading = true;
$status.database.loading = true;
try {
await trpc.databases.start.mutate({ id });
} catch (error) {
return errorNotification(error);
} finally {
$status.database.initialLoading = false;
$status.database.loading = false;
await getStatus();
}
}
async function getStatus() {
if ($status.database.loading) return;
$status.database.loading = true;
const { data } = await trpc.databases.status.query({ id });
$status.database.isRunning = data.isRunning;
$status.database.initialLoading = false;
$status.database.loading = false;
}
onDestroy(() => {
$status.database.initialLoading = true;
$status.database.isRunning = false;
$status.database.isExited = false;
$status.database.loading = false;
clearInterval(statusInterval);
});
onMount(async () => {
$status.database.isRunning = false;
$status.database.loading = false;
if (
database.type &&
database.destinationDockerId &&
database.version &&
database.defaultDatabase
) {
await getStatus();
statusInterval = setInterval(async () => {
await getStatus();
}, 2000);
} else {
$status.database.initialLoading = false;
}
});
</script>
{#if id !== 'new'}
<nav class="header lg:flex-row flex-col-reverse">
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
<div class="flex flex-col items-center justify-center">
<div class="title">
{#if $page.url.pathname === `/databases/${id}`}
Configurations
{:else if $page.url.pathname === `/databases/${id}/logs`}
Database Logs
{:else if $page.url.pathname === `/databases/${id}/configuration/type`}
Select a Database Type
{:else if $page.url.pathname === `/databases/${id}/configuration/version`}
Select a Database Version
{:else if $page.url.pathname === `/databases/${id}/configuration/destination`}
Select a Destination
{/if}
</div>
</div>
<DatabaseLinks {database} />
</div>
<div class="lg:block hidden flex-1" />
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0">
{#if database.type && database.destinationDockerId && database.version}
{#if $status.database.isExited}
<a
id="exited"
href={!$status.database.isRunning ? `/databases/${id}/logs` : null}
class="icons bg-transparent text-red-500 tooltip-error"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
<Tooltip triggeredBy="#exited">{'Service exited with an error!'}</Tooltip>
{/if}
{#if $status.database.initialLoading}
<button class="icons flex animate-spin duration-500 ease-in-out">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else if $status.database.isRunning}
<button
id="stop"
on:click={stopDatabase}
type="submit"
disabled={!$appSession.isAdmin}
class="icons bg-transparent text-red-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<Tooltip triggeredBy="#stop">{'Stop'}</Tooltip>
{:else}
<button
id="start"
on:click={startDatabase}
type="submit"
disabled={!$appSession.isAdmin}
class="icons bg-transparent text-sm flex items-center space-x-2 text-green-500"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
<Tooltip triggeredBy="#start">{'Start'}</Tooltip>
{/if}
{/if}
<div class="border border-stone-700 h-8" />
<a
id="configuration"
href="/databases/{id}"
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/databases/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`}
>
<button class="icons bg-transparent m text-sm disabled:text-red-500">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<Tooltip triggeredBy="#configuration">{'Configuration'}</Tooltip>
<div class="border border-stone-700 h-8" />
<a
id="databaselogs"
href={$status.database.isRunning ? `/databases/${id}/logs` : null}
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`}
>
<button disabled={!$status.database.isRunning} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg></button
></a
>
<Tooltip triggeredBy="#databaselogs">{'Logs'}</Tooltip>
{#if forceDelete}
<button
on:click={() => deleteDatabase(true)}
type="submit"
disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"
>
Force Delete</button
>{:else}
<button
id="delete"
on:click={() => deleteDatabase(false)}
type="submit"
disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"><Icons.Delete /></button
>
{/if}
<Tooltip triggeredBy="#delete" placement="left">Delete</Tooltip>
</div>
</nav>
{/if}
<slot />

View File

@@ -0,0 +1,48 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { LayoutLoad } from './$types';
import { redirect } from '@sveltejs/kit';
function checkConfiguration(database: any): string | null {
let configurationPhase = null;
if (!database.type) {
configurationPhase = 'type';
} else if (!database.version) {
configurationPhase = 'version';
} else if (!database.destinationDockerId) {
configurationPhase = 'destination';
}
return configurationPhase;
}
export const load: LayoutLoad = async ({ params, url }) => {
const { pathname } = new URL(url);
const { id } = params;
try {
const database = await trpc.databases.getDatabaseById.query({ id });
if (!database) {
throw redirect(307, '/databases');
}
const configurationPhase = checkConfiguration(database);
console.log({ configurationPhase });
// if (
// configurationPhase &&
// pathname !== `/applications/${params.id}/configuration/${configurationPhase}`
// ) {
// throw redirect(302, `/applications/${params.id}/configuration/${configurationPhase}`);
// }
return {
database
};
} catch (err) {
if (err instanceof Error) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.' + '<br><br>' + err.message
});
}
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import type { LayoutData } from './$types';
export let data: LayoutData;
let database = data.database.data.database;
let privatePort = data.database.data.privatePort;
import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte';
import Databases from './components/Databases/Databases.svelte';
import { status, trpc } from '$lib/store';
const { id } = $page.params;
let loading = {
usage: false
};
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval: any;
async function getUsage() {
if (loading.usage) return;
if (!$status.database.isRunning) return;
loading.usage = true;
const { data } = await trpc.databases.usage.query({ id });
usage = data.usage;
loading.usage = false;
}
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => {
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1500);
});
</script>
<div class="mx-auto max-w-6xl p-5">
<div class="text-center">
<div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div>
<div class="stat-value text-xl">{usage?.MemUsage}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Used CPU</div>
<div class="stat-value text-xl">{usage?.CPUPerc}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Network IO</div>
<div class="stat-value text-xl">{usage?.NetIO}</div>
</div>
</div>
</div>
<Databases bind:database {privatePort} />

View File

@@ -0,0 +1,32 @@
<script lang="ts">
export let database: any;
import Clickhouse from '$lib/components/icons/databases/Clickhouse.svelte';
import CouchDb from '$lib/components/icons/databases/CouchDB.svelte';
import EdgeDb from '$lib/components/icons/databases/EdgeDB.svelte';
import MariaDb from '$lib/components/icons/databases/MariaDB.svelte';
import MongoDb from '$lib/components/icons/databases/MongoDB.svelte';
import MySql from '$lib/components/icons/databases/MySQL.svelte';
import PostgreSql from '$lib/components/icons/databases/PostgreSQL.svelte';
import Redis from '$lib/components/icons/databases/Redis.svelte';
</script>
<span class="relative">
{#if database.type === 'clickhouse'}
<Clickhouse />
{:else if database.type === 'couchdb'}
<CouchDb />
{:else if database.type === 'mongodb'}
<MongoDb />
{:else if database.type === 'mysql'}
<MySql />
{:else if database.type === 'mariadb'}
<MariaDb />
{:else if database.type === 'postgresql'}
<PostgreSql />
{:else if database.type === 'redis'}
<Redis />
{:else if database.type === 'edgedb'}
<EdgeDb />
{/if}
</span>

View File

@@ -0,0 +1,68 @@
<script lang="ts">
export let database: any;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
<h1 class="title">CouchDB</h1>
</div>
<div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="Example: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root Password</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
</div>

View File

@@ -0,0 +1,281 @@
<script lang="ts">
export let database: any;
export let privatePort: any;
import { page } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
import MySql from './MySQL.svelte';
import MongoDb from './MongoDB.svelte';
import MariaDb from './MariaDB.svelte';
import PostgreSql from './PostgreSQL.svelte';
import Redis from './Redis.svelte';
import CouchDb from './CouchDb.svelte';
import EdgeDB from './EdgeDB.svelte';
import { errorNotification } from '$lib/common';
import { addToast, appSession, status, trpc } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
let loading = {
main: false,
public: false
};
let publicUrl = '';
let appendOnly = database.settings.appendOnly;
let databaseDefault: any;
let databaseDbUser: any;
let databaseDbUserPassword: any;
generateDbDetails();
function generateDbDetails() {
databaseDefault = database.defaultDatabase;
databaseDbUser = database.dbUser;
databaseDbUserPassword = database.dbUserPassword;
if (database.type === 'mongodb' || database.type === 'edgedb') {
if (database.type === 'mongodb') {
databaseDefault = '?readPreference=primary&ssl=false';
}
databaseDbUser = database.rootUser;
databaseDbUserPassword = database.rootUserPassword;
} else if (database.type === 'redis') {
databaseDefault = '';
databaseDbUser = '';
}
}
function generateUrl() {
const ipAddress = () => {
if ($status.database.isPublic) {
if (database.destinationDocker.remoteEngine) {
return database.destinationDocker.remoteIpAddress;
}
if ($appSession.ipv6) {
return $appSession.ipv6;
}
if ($appSession.ipv4) {
return $appSession.ipv4;
}
return '<Cannot determine public IP address>';
} else {
return database.id;
}
};
const user = () => {
if (databaseDbUser) {
return databaseDbUser + ':';
}
return '';
};
const port = () => {
if ($status.database.isPublic) {
return database.publicPort;
} else {
return privatePort;
}
};
publicUrl = `${
database.type
}://${user()}${databaseDbUserPassword}@${ipAddress()}:${port()}/${databaseDefault}`;
}
async function changeSettings(name: any) {
if (name !== 'appendOnly') {
if (loading.public || !$status.database.isRunning) return;
}
loading.public = true;
let data = {
isPublic: $status.database.isPublic,
appendOnly
};
if (name === 'isPublic') {
data.isPublic = !$status.database.isPublic;
}
if (name === 'appendOnly') {
data.appendOnly = !appendOnly;
}
try {
const { publicPort } = await trpc.databases.saveSettings.mutate({
id,
isPublic: data.isPublic,
appendOnly: data.appendOnly
});
$status.database.isPublic = data.isPublic;
appendOnly = data.appendOnly;
if ($status.database.isPublic) {
database.publicPort = publicPort;
}
} catch (error) {
return errorNotification(error);
} finally {
loading.public = false;
}
}
async function handleSubmit() {
try {
loading.main = true;
await trpc.databases.save.mutate({
id,
...database,
isRunning: $status.database.isRunning
});
generateDbDetails();
addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.main = false;
}
}
</script>
<div class="mx-auto max-w-6xl p-4">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 items-center">
<h1 class="title">General</h1>
{#if $appSession.isAdmin}
<button
type="submit"
class="btn btn-sm"
class:loading={loading.main}
class:bg-databases={!loading.main}
disabled={loading.main}>Save</button
>
{/if}
</div>
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<label for="name">Name</label>
<input
class="w-full"
readonly={!$appSession.isAdmin}
name="name"
id="name"
bind:value={database.name}
required
/>
<label for="destination">Destination</label>
{#if database.destinationDockerId}
<div class="no-underline">
<input
value={database.destinationDocker.name}
id="destination"
disabled
readonly
class="bg-transparent w-full"
/>
</div>
{/if}
<label for="version">Version / Tag</label>
<a
href={$appSession.isAdmin && !$status.database.isRunning
? `/databases/${id}/configuration/version?from=/databases/${id}`
: ''}
class="no-underline"
>
<input
class="w-full"
value={database.version}
readonly
disabled={$status.database.isRunning || $status.database.initialLoading}
class:cursor-pointer={!$status.database.isRunning}
/></a
>
<label for="host">Host</label>
<CopyPasswordField
placeholder="Generated automatically after start"
isPasswordField={false}
readonly
disabled
id="host"
name="host"
value={database.id}
/>
<label for="publicPort">Port</label>
<CopyPasswordField
placeholder="Generated automatically after set to public"
id="publicPort"
readonly
disabled
name="publicPort"
value={loading.public
? 'Loading...'
: $status.database.isPublic
? database.publicPort
: privatePort}
/>
</div>
{#if database.type === 'mysql'}
<MySql bind:database />
{:else if database.type === 'postgresql'}
<PostgreSql bind:database />
{:else if database.type === 'mongodb'}
<MongoDb bind:database />
{:else if database.type === 'mariadb'}
<MariaDb bind:database />
{:else if database.type === 'redis'}
<Redis bind:database />
{:else if database.type === 'couchdb'}
<CouchDb {database} />
{:else if database.type === 'edgedb'}
<EdgeDB {database} />
{/if}
<div class="flex flex-col space-y-2 mt-5">
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<label for="url"
>Connection String
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
<Explainer
explanation="You can only access the database with this URL if your application is deployed to the same Destination."
/>
{/if}</label
>
<button class="btn btn-sm" on:click|preventDefault={generateUrl}
>Show Connection String</button
>
</div>
<div class="lg:px-10 px-2">
{#if publicUrl}
<CopyPasswordField
placeholder="Click on the button to generate URL"
id="url"
name="url"
readonly
disabled
value={loading.public ? 'Loading...' : publicUrl}
/>
{/if}
</div>
</div>
</form>
<div class="flex space-x-1 pb-5 font-bold">
<h1 class="title">Features</h1>
</div>
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<Setting
id="isPublic"
loading={loading.public}
bind:setting={$status.database.isPublic}
on:click={() => changeSettings('isPublic')}
title="Set it Public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
disabled={!$status.database.isRunning}
/>
{#if database.type === 'redis'}
<Setting
id="appendOnly"
loading={loading.public}
bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')}
title="Change append only mode"
description="Useful if you would like to restore redis data from a backup.<br><span class=' text-white'>Database restart is required.</span>"
/>
{/if}
</div>
</div>

View File

@@ -0,0 +1,50 @@
<script lang="ts">
export let database: any;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">EdgeDB</div>
</div>
<div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="Example: edgedb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser"
>Root Password <Explainer
explanation="Could be changed while the database is running."
/></label
>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
</div>

View File

@@ -0,0 +1,78 @@
<script lang="ts">
export let database: any;
import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
<h1 class="title">MariaDB</h1>
</div>
<div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase"
>Default Database</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="Example: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" >User</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword"
>Password
<Explainer explanation="Could be changed while the database is running." /></label
>
<CopyPasswordField
disabled={!$status.database.isRunning}
readonly={!$status.database.isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
bind:value={database.dbUserPassword}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" >Root User</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword"
>Root Password
<Explainer explanation="Could be changed while the database is running." /></label
>
<CopyPasswordField
disabled={!$status.database.isRunning}
readonly={!$status.database.isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
bind:value={database.rootUserPassword}
/>
</div>
</div>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
export let database: any;
import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
<h1 class="title">MongoDB</h1>
</div>
<div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label>
<CopyPasswordField
placeholder="Generated automatically after start"
id="rootUser"
readonly
disabled
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword"
>Root Password
<Explainer explanation="Could be changed while the database is running." /></label
>
<CopyPasswordField
disabled={!$status.database.isRunning}
readonly={!$status.database.isRunning}
placeholder="Generated automatically after start"
isPasswordField={true}
id="rootUserPassword"
name="rootUserPassword"
bind:value={database.rootUserPassword}
/>
</div>
</div>

View File

@@ -0,0 +1,76 @@
<script lang="ts">
export let database: any;
import { status, appSession } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
<h1 class="title">MySQL</h1>
</div>
<div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="Example: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword"
>Password
<Explainer explanation="Could be changed while the database is running." /></label
>
<CopyPasswordField
disabled={!$status.database.isRunning}
readonly={!$status.database.isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
bind:value={database.dbUserPassword}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={$appSession.isARM ? 'root' : database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword"
>Password
<Explainer explanation="Could be changed while the database is running." /></label
>
<CopyPasswordField
disabled={!$status.database.isRunning}
readonly={!$status.database.isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
bind:value={database.rootUserPassword}
/>
</div>
</div>

View File

@@ -0,0 +1,68 @@
<script lang="ts">
export let database: any;
import { status, appSession } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
<h1 class="title">PostgreSQL</h1>
</div>
<div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="Example: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
{#if !$appSession.isARM}
<div class="grid grid-cols-2 items-center">
<label for="rootUser"
>Postgres User Password <Explainer
explanation="Could be changed while the database is running."
/></label
>
<CopyPasswordField
disabled={!$status.database.isRunning}
readonly={!$status.database.isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
bind:value={database.rootUserPassword}
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword"
>Password
<Explainer explanation="Could be changed while the database is running." /></label
>
<CopyPasswordField
disabled={!$status.database.isRunning}
readonly={!$status.database.isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
bind:value={database.dbUserPassword}
/>
</div>
</div>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
export let database: any;
import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
<h1 class="title">Redis</h1>
</div>
<div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword"
>Password
<Explainer explanation="Could be changed while the database is running." /></label
>
<CopyPasswordField
disabled={!$status.database.isRunning}
readonly={!$status.database.isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
bind:value={database.dbUserPassword}
/>
</div>
</div>

View File

@@ -0,0 +1,37 @@
import { errorNotification } from '$lib/common';
import { trpc } from '$lib/store';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
databaseId: string;
};
export async function saveSecret({
isNew,
name,
value,
isNewSecret,
databaseId
}: Props): Promise<void> {
if (!name) return errorNotification('Name is required');
if (!value) return errorNotification('Value is required');
try {
await trpc.databases.saveSecret.mutate({
name,
value,
isNew: isNew || false
});
if (isNewSecret) {
name = '';
value = '';
}
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import type { LayoutData } from './$types';
export let data: LayoutData;
let destination = data.destination.destination;
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import { appSession, trpc } from '$lib/store';
import * as Icons from '$lib/components/icons';
import Tooltip from '$lib/components/Tooltip.svelte';
const isDestinationDeletable =
(destination?.application.length === 0 &&
destination?.database.length === 0 &&
destination?.service.length === 0) ||
true;
async function deleteDestination(destination: any) {
if (!isDestinationDeletable) return;
const sure = confirm("Are you sure you want to delete this destination? This can't be undone.");
if (sure) {
try {
await trpc.destinations.delete.mutate({ id: destination.id });
return await goto('/', { replaceState: true });
} catch (error) {
return errorNotification(error);
}
}
}
function deletable() {
if (!isDestinationDeletable) {
return 'Please delete all resources before deleting this.';
}
if ($appSession.isAdmin) {
return "Delete this destination. This can't be undone.";
} else {
return "You don't have permission to delete this destination.";
}
}
</script>
{#if $page.params.id !== 'new'}
<nav class="header lg:flex-row flex-col-reverse">
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
<div class="flex flex-col items-center justify-center title">
{#if $page.url.pathname === `/destinations/${$page.params.id}`}
Configurations
{:else if $page.url.pathname.startsWith(`/destinations/${$page.params.id}/configuration/sshkey`)}
Select a SSH Key
{/if}
</div>
</div>
<div class="lg:block hidden flex-1" />
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0">
<button
id="delete"
on:click={() => deleteDestination(destination)}
type="submit"
disabled={!$appSession.isAdmin && isDestinationDeletable}
class:hover:text-red-500={$appSession.isAdmin && isDestinationDeletable}
class="icons bg-transparent text-sm"
class:text-stone-600={!isDestinationDeletable}><Icons.Delete /></button
>
<Tooltip triggeredBy="#delete">{deletable()}</Tooltip>
</div>
</nav>
{/if}
<slot />

View File

@@ -0,0 +1,45 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { LayoutLoad } from './$types';
import { redirect } from '@sveltejs/kit';
function checkConfiguration(destination: any): string | null {
let configurationPhase = null;
if (!destination?.remoteEngine) return configurationPhase;
if (!destination?.sshKey) {
configurationPhase = 'sshkey';
}
return configurationPhase;
}
export const load: LayoutLoad = async ({ params, url }) => {
const { pathname } = new URL(url);
const { id } = params;
try {
const destination = await trpc.destinations.getDestinationById.query({ id });
if (!destination) {
throw redirect(307, '/destinations');
}
const configurationPhase = checkConfiguration(destination);
console.log({ configurationPhase });
// if (
// configurationPhase &&
// pathname !== `/applications/${params.id}/configuration/${configurationPhase}`
// ) {
// throw redirect(302, `/applications/${params.id}/configuration/${configurationPhase}`);
// }
return {
destination
};
} catch (err) {
if (err instanceof Error) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.' + '<br><br>' + err.message
});
}
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import type { LayoutData } from './$types';
export let data: LayoutData;
let destination = data.destination.destination;
let settings = data.destination.settings;
import { page } from '$app/stores';
import New from './components/New.svelte';
import Destination from './components/Destination.svelte';
const { id } = $page.params;
</script>
{#if id === 'new'}
<New />
{:else}
<Destination bind:destination bind:settings />
{/if}

View File

@@ -0,0 +1,14 @@
<script lang="ts">
export let destination: any;
export let settings: any;
import LocalDocker from './LocalDocker.svelte';
import RemoteDocker from './RemoteDocker.svelte';
</script>
<div class="mx-auto max-w-6xl px-6">
{#if destination.remoteEngine}
<RemoteDocker bind:destination {settings} />
{:else}
<LocalDocker bind:destination {settings} />
{/if}
</div>

View File

@@ -0,0 +1,212 @@
<script lang="ts">
export let destination: any;
export let settings: any;
import { page } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { errorNotification } from '$lib/common';
import { addToast, appSession, trpc } from '$lib/store';
import Setting from '$lib/components/Setting.svelte';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
let loading = {
restart: false,
proxy: false,
save: false
};
async function handleSubmit() {
loading.save = true;
try {
await trpc.destinations.save.mutate({ ...destination });
addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.save = false;
}
}
onMount(async () => {
loading.proxy = true;
const { isRunning } = await trpc.destinations.status.query({ id });
let proxyUsed = !destination.isCoolifyProxyUsed;
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
try {
await trpc.destinations.saveSettings.mutate({
id,
isCoolifyProxyUsed: proxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error) {
return errorNotification(error);
}
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
try {
await trpc.destinations.saveSettings.mutate({
id,
isCoolifyProxyUsed: proxyUsed,
engine: destination.engine
});
await startProxy();
destination.isCoolifyProxyUsed = proxyUsed;
} catch (error) {
return errorNotification(error);
} finally {
loading.proxy = false;
}
}
loading.proxy = false;
});
async function changeProxySetting() {
if (!cannotDisable) {
const isProxyActivated = destination.isCoolifyProxyUsed;
if (isProxyActivated) {
const sure = confirm(
`Are you sure you want to ${
destination.isCoolifyProxyUsed ? 'disable' : 'enable'
} Coolify proxy? It will remove the proxy for all configured networks and all deployments on '${
destination.engine
}'! Nothing will be reachable if you do it!`
);
if (!sure) return;
}
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
loading.proxy = true;
await trpc.destinations.saveSettings.mutate({
id,
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
if (isProxyActivated) {
await stopProxy();
} else {
await startProxy();
}
} catch (error) {
return errorNotification(error);
} finally {
loading.proxy = false;
}
}
}
async function stopProxy() {
try {
await trpc.destinations.stopProxy.mutate({ id });
return addToast({
message: 'Coolify proxy stopped.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function startProxy() {
try {
await trpc.destinations.startProxy.mutate({ id });
return addToast({
message: ' Coolify proxy started.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function forceRestartProxy() {
const sure = confirm(
"Are you sure you want to restart the proxy? It will remove the proxy for all configured networks and all deployments on '" +
destination.engine +
"'! Nothing will be reachable if you do it!"
);
if (sure) {
try {
loading.restart = true;
addToast({
message: 'Restarting proxy...',
type: 'success'
});
await trpc.destinations.restartProxy.mutate({
id
});
} catch (error) {
setTimeout(() => {
window.location.reload();
}, 5000);
} finally {
loading.restart = false;
}
}
}
</script>
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-2">
<button
type="submit"
class="btn btn-sm"
class:bg-destinations={!loading.save}
class:loading={loading.save}
disabled={loading.save}
>Save
</button>
<button
class="btn btn-sm"
class:loading={loading.restart}
class:bg-error={!loading.restart}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}>Force restart proxy</button
>
</div>
<div class="grid gap-2 grid-cols-2 auto-rows-max mt-10 items-center">
<label for="name">Name</label>
<input
class="w-full"
name="name"
placeholder="Name"
disabled={!$appSession.isAdmin}
readonly={!$appSession.isAdmin}
bind:value={destination.name}
/>
<label for="engine">Engine</label>
<CopyPasswordField
id="engine"
readonly
disabled
name="engine"
placeholder="Example: /var/run/docker.sock"
value={destination.engine}
/>
<label for="network">Netwokr</label>
<CopyPasswordField
id="network"
readonly
disabled
name="network"
placeholder="Default: coolify"
value={destination.network}
/>
{#if $appSession.teamId === '0'}
<Setting
id="changeProxySetting"
loading={loading.proxy}
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: ''
}`}
/>
{/if}
</div>
</form>

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import cuid from 'cuid';
import NewLocalDocker from './NewLocalDocker.svelte';
import NewRemoteDocker from './NewRemoteDocker.svelte';
let payload = {};
let selected = 'localDocker';
function setPredefined(type: any) {
selected = type;
switch (type) {
case 'localDocker':
payload = {
name: 'Local Docker',
engine: '/var/run/docker.sock',
remoteEngine: false,
network: cuid(),
isCoolifyProxyUsed: true
};
break;
case 'remoteDocker':
payload = {
name: 'Remote Docker',
remoteEngine: true,
remoteIpAddress: null,
remoteUser: 'root',
remotePort: 22,
network: cuid(),
isCoolifyProxyUsed: true
};
break;
default:
break;
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Add New Destination</div>
</div>
<div class="flex-col space-y-2 pb-10 text-center">
<div class="text-xl font-bold text-white">Predefined destinations</div>
<div class="flex justify-center space-x-2">
<button class="btn btn-sm" on:click={() => setPredefined('localDocker')}>Local Docker</button>
<button class="btn btn-sm" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button>
<!-- <button class="w-32" on:click={() => setPredefined('kubernetes')}>Kubernetes</button> -->
</div>
</div>
{#if selected === 'localDocker'}
<NewLocalDocker {payload} />
{:else if selected === 'remoteDocker'}
<NewRemoteDocker {payload} />
{:else}
<div class="text-center font-bold text-4xl py-10">Not implemented yet</div>
{/if}

View File

@@ -0,0 +1,72 @@
<script lang="ts">
export let payload: any;
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import Setting from '$lib/components/Setting.svelte';
import { appSession, trpc } from '$lib/store';
const from = $page.url.searchParams.get('from');
let loading = false;
async function handleSubmit() {
if (loading) return;
try {
loading = true;
await trpc.destinations.check.query({ network: payload.network });
const { id } = await trpc.destinations.save.mutate({ id: 'new', ...payload });
return await goto(from || `/destinations/${id}`);
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
<div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div
class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0"
>
<div class="title font-bold">Configuration</div>
<button
type="submit"
class="btn btn-sm bg-destinations w-full lg:w-fit"
class:loading
disabled={loading}
>{loading ? (payload.isCoolifyProxyUsed ? 'Saving...' : 'Saving...') : 'Save'}</button
>
</div>
<div class="mt-2 grid grid-cols-2 items-center lg:pl-10">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input required name="name" placeholder="Name" bind:value={payload.name} />
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="engine" class="text-base font-bold text-stone-100">Engine</label>
<input
required
name="engine"
placeholder="Example: /var/run/docker.sock"
bind:value={payload.engine}
/>
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<input required name="network" placeholder="Default: coolify" bind:value={payload.network} />
</div>
{#if $appSession.teamId === '0'}
<div class="grid grid-cols-2 items-center lg:pl-10">
<Setting
id="changeProxySetting"
bind:setting={payload.isCoolifyProxyUsed}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
title="Use Coolify Proxy?"
description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'}
/>
</div>
{/if}
</form>
</div>

View File

@@ -0,0 +1,104 @@
<script lang="ts">
export let payload: any;
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { trpc } from '$lib/store';
const from = $page.url.searchParams.get('from');
let loading = false;
async function handleSubmit() {
if (loading) return;
try {
loading = true;
await trpc.destinations.check.query({ network: payload.network });
const { id } = await trpc.destinations.save.mutate({ id: 'new', ...payload });
return await goto(from || `/destinations/${id}`);
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
<div class="text-center flex justify-center">
<SimpleExplainer
customClass="max-w-[32rem]"
text="Remote Docker Engines are using <span class='text-white font-bold'>SSH</span> to communicate with the remote docker engine.
You need to setup an <span class='text-white font-bold'>SSH key</span> in advance on the server and install Docker.
<br>See <a class='text-white' href='https://docs.coollabs.io/coolify/destinations#remote-docker-engine' target='blank'>docs</a> for more details."
/>
</div>
<div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0">
<div class="title font-bold">Configuration</div>
<button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading}
>{loading
? payload.isCoolifyProxyUsed
? 'Saving...'
: 'Saving...'
: "Save"}</button
>
</div>
<div class="mt-2 grid grid-cols-2 items-center lg:pl-10">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input required name="name" placeholder="Name" bind:value={payload.name} />
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remoteIpAddress" class="text-base font-bold text-stone-100"
>IP Address</label
>
<input
required
name="remoteIpAddress"
placeholder="Example: 192.168..."
bind:value={payload.remoteIpAddress}
/>
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remoteUser" class="text-base font-bold text-stone-100">User</label>
<input
required
name="remoteUser"
placeholder="Example: root"
bind:value={payload.remoteUser}
/>
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remotePort" class="text-base font-bold text-stone-100">Port</label>
<input
required
name="remotePort"
placeholder="Example: 22"
bind:value={payload.remotePort}
/>
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<input
required
name="network"
placeholder="Default: coolify"
bind:value={payload.network}
/>
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<Setting
id="isCoolifyProxyUsed"
bind:setting={payload.isCoolifyProxyUsed}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
title="Use Coolify Proxy?"
description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'}
/>
</div>
</form>
</div>

View File

@@ -0,0 +1,279 @@
<script lang="ts">
export let destination: any;
export let settings: any;
import { page } from '$app/stores';
import Setting from '$lib/components/Setting.svelte';
import { get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { addToast, appSession } from '$lib/store';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
let loading = {
restart: false,
proxy: true,
save: false,
verify: false
};
$: isDisabled = !$appSession.isAdmin;
async function handleSubmit() {
loading.save = true;
try {
await post(`/destinations/${id}`, { ...destination });
addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.save = false;
}
}
onMount(async () => {
if (destination.remoteEngine && destination.remoteVerified) {
loading.proxy = true;
const { isRunning } = await get(`/destinations/${id}/status`);
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error) {
return errorNotification(error);
}
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await startProxy();
} catch (error) {
return errorNotification(error);
}
}
}
loading.proxy = false;
});
async function changeProxySetting() {
if (!destination.remoteVerified) return;
loading.proxy = true;
if (!cannotDisable) {
const isProxyActivated = destination.isCoolifyProxyUsed;
if (isProxyActivated) {
const sure = confirm(
`Are you sure you want to ${
destination.isCoolifyProxyUsed ? 'disable' : 'enable'
} Coolify proxy? It will remove the proxy for all configured networks and all deployments! Nothing will be reachable if you do it!`
);
if (!sure) {
loading.proxy = false;
return;
}
}
let proxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: proxyUsed,
engine: destination.engine
});
if (isProxyActivated) {
await stopProxy();
} else {
await startProxy();
}
destination.isCoolifyProxyUsed = proxyUsed;
} catch (error) {
return errorNotification(error);
} finally {
loading.proxy = false;
}
}
}
async function stopProxy() {
try {
await post(`/destinations/${id}/stop`, { engine: destination.engine });
return addToast({
message: $t('destination.coolify_proxy_stopped'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function startProxy() {
try {
await post(`/destinations/${id}/start`, { engine: destination.engine });
return addToast({
message: $t('destination.coolify_proxy_started'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function forceRestartProxy() {
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
loading.restart = true;
addToast({
message: $t('destination.coolify_proxy_restarting'),
type: 'success'
});
await post(`/destinations/${id}/restart`, {
engine: destination.engine,
fqdn: settings.fqdn
});
} catch (error) {
setTimeout(() => {
window.location.reload();
}, 5000);
} finally {
loading.restart = false;
}
}
}
async function verifyRemoteDocker() {
try {
loading.verify = true;
await post(`/destinations/${id}/verify`, {});
destination.remoteVerified = true;
return addToast({
message: 'Remote Docker Engine verified!',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verify = false;
}
}
</script>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-5">
{#if $appSession.isAdmin}
<button
type="submit"
class="btn btn-sm"
class:loading={loading.save}
class:bg-destinations={!loading.save}
disabled={loading.save}
>{$t('forms.save')}
</button>
<button
disabled={loading.verify}
class="btn btn-sm"
class:loading={loading.verify}
on:click|preventDefault|stopPropagation={verifyRemoteDocker}
>{!destination.remoteVerified
? 'Verify Remote Docker Engine'
: 'Check Remote Docker Engine'}</button
>
{#if destination.remoteVerified}
<button
class="btn btn-sm"
class:loading={loading.restart}
class:bg-error={!loading.restart}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}
>{$t('destination.force_restart_proxy')}</button
>
{/if}
{/if}
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name">{$t('forms.name')}</label>
<input
name="name"
class="w-full"
placeholder={$t('forms.name')}
disabled={!$appSession.isAdmin}
readonly={!$appSession.isAdmin}
bind:value={destination.name}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="network">{$t('forms.network')}</label>
<CopyPasswordField
id="network"
readonly
disabled
name="network"
placeholder="{$t('forms.default')}: coolify"
value={destination.network}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remoteIpAddress">IP Address</label>
<CopyPasswordField
id="remoteIpAddress"
readonly
disabled
name="remoteIpAddress"
value={destination.remoteIpAddress}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remoteUser">User</label>
<CopyPasswordField
id="remoteUser"
readonly
disabled
name="remoteUser"
value={destination.remoteUser}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remotePort">Port</label>
<CopyPasswordField
id="remotePort"
readonly
disabled
name="remotePort"
value={destination.remotePort}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="sshKey">SSH Key</label>
<a
href={!isDisabled ? `/destinations/${id}/configuration/sshkey?from=/destinations/${id}` : ''}
class="no-underline"
><input
value={destination.sshKey.name}
readonly
id="sshKey"
class="cursor-pointer w-full"
/></a
>
</div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
id="changeProxySetting"
disabled={cannotDisable || !destination.remoteVerified}
loading={loading.proxy}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title={$t('destination.use_coolify_proxy')}
description={`Install & configure a proxy (based on Traefik) on the destination to allow you to access your applications and services without any manual configuration.${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: ''
}`}
/>
</div>
</form>

View File

@@ -0,0 +1,366 @@
<script lang="ts">
import { page } from '$app/stores';
import { status, trpc } from '$lib/store';
import { onDestroy, onMount } from 'svelte';
import type { LayoutData } from './$types';
export let data: LayoutData;
const id = $page.params.id;
let service = data.service;
let template = data.template;
import { errorNotification } from '$lib/common';
import {
appSession,
isDeploymentEnabled,
location,
setLocation,
checkIfDeploymentEnabledServices,
addToast
} from '$lib/store';
import { goto } from '$app/navigation';
import { saveForm } from './utils';
import Menu from './components/Menu.svelte';
$isDeploymentEnabled = checkIfDeploymentEnabledServices(service);
let statusInterval: any;
async function deleteService() {
const sure = confirm('Are you sure you want to delete this service?');
if (sure) {
$status.service.initialLoading = true;
try {
if (service.type && $status.service.isRunning) await trpc.services.stop.mutate({ id });
await trpc.services.delete.mutate({ id });
return await goto('/');
} catch (error) {
return errorNotification(error);
} finally {
$status.service.initialLoading = false;
}
}
}
async function restartService() {
const sure = confirm('Are you sure you want to restart this service?');
if (sure) {
await stopService(true);
await startService();
}
}
async function stopService(skip = false) {
if (skip) {
$status.service.initialLoading = true;
$status.service.loading = true;
try {
await trpc.services.stop.mutate({ id });
if (service.type.startsWith('wordpress')) {
await trpc.services.wordpress.mutate({ id, ftpEnabled: false });
service.wordpress?.ftpEnabled && window.location.reload();
}
} catch (error) {
return errorNotification(error);
} finally {
$status.service.initialLoading = false;
$status.service.loading = false;
await getStatus();
}
return;
}
const sure = confirm(
"Are you sure you want to stop this service? You won't be able to access it anymore."
);
if (sure) {
$status.service.initialLoading = true;
$status.service.loading = true;
try {
await trpc.services.stop.mutate({ id });
if (service.type.startsWith('wordpress')) {
await trpc.services.wordpress.mutate({ id, ftpEnabled: false });
service.wordpress?.ftpEnabled && window.location.reload();
}
} catch (error) {
return errorNotification(error);
} finally {
$status.service.initialLoading = false;
$status.service.loading = false;
await getStatus();
}
}
}
async function startService() {
$status.service.initialLoading = true;
$status.service.loading = true;
try {
const form: any = document.getElementById('saveForm');
if (form) {
const formData = new FormData(form);
service = await saveForm(formData, service);
}
await trpc.services.start.mutate({ id });
} catch (error) {
return errorNotification(error);
} finally {
$status.service.initialLoading = false;
$status.service.loading = false;
await getStatus();
}
}
async function getStatus() {
if ($status.service.loading) return;
$status.service.loading = true;
const data = await trpc.services.status.query({ id });
$status.service.statuses = data;
let numberOfServices = Object.keys(data).length;
if (Object.keys($status.service.statuses).length === 0) {
$status.service.overallStatus = 'stopped';
} else {
if (Object.keys($status.service.statuses).length !== numberOfServices) {
$status.service.overallStatus = 'degraded';
} else {
for (const oneService in $status.service.statuses) {
const { isExited, isRestarting, isRunning } = $status.service.statuses[oneService].status;
if (isExited || isRestarting) {
$status.service.overallStatus = 'degraded';
break;
}
if (isRunning) {
$status.service.overallStatus = 'healthy';
}
if (!isExited && !isRestarting && !isRunning) {
$status.service.overallStatus = 'stopped';
}
}
}
}
$status.service.loading = false;
$status.service.initialLoading = false;
}
onDestroy(() => {
$status.service.initialLoading = true;
$status.service.loading = false;
$status.service.statuses = [];
$status.service.overallStatus = 'stopped';
$location = null;
$isDeploymentEnabled = false;
clearInterval(statusInterval);
});
onMount(async () => {
setLocation(service);
$status.service.loading = false;
if ($isDeploymentEnabled) {
await getStatus();
statusInterval = setInterval(async () => {
await getStatus();
}, 2000);
} else {
$status.service.initialLoading = false;
}
});
</script>
<div class="mx-auto max-w-screen-2xl px-6 grid grid-cols-1 lg:grid-cols-2">
<nav class="header flex flex-col lg:flex-row order-2 lg:order-1 px-0 lg:px-4 items-start">
<div class="title lg:pb-10">
<div class="flex justify-center items-center space-x-2">
<div>
{#if $page.url.pathname === `/services/${id}/configuration/type`}
Select a Service Type
{:else if $page.url.pathname === `/services/${id}/configuration/version`}
Select a Service Version
{:else if $page.url.pathname === `/services/${id}/configuration/destination`}
Select a Destination
{:else}
<div class="flex justify-center items-center space-x-2">
<div>Configurations</div>
<div
class="badge badge-lg rounded uppercase"
class:text-green-500={$status.service.overallStatus === 'healthy'}
class:text-yellow-400={$status.service.overallStatus === 'degraded'}
class:text-red-500={$status.service.overallStatus === 'stopped'}
>
{$status.service.overallStatus === 'healthy'
? 'Healthy'
: $status.service.overallStatus === 'degraded'
? 'Degraded'
: 'Stopped'}
</div>
</div>
{/if}
</div>
</div>
</div>
<div class="flex flex-row space-x-2 lg:px-2">
{#if $page.url.pathname.startsWith(`/services/${id}/configuration/`)}
<button
on:click={() => deleteService()}
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
>
Delete Service
</button>
{/if}
</div>
</nav>
<div
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
>
{#if $status.service.initialLoading}
<button class="btn btn-ghost btn-sm gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 animate-spin duration-500 ease-in-out"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
{$status.service.startup[id] || 'Loading...'}
</button>
{:else if $status.service.overallStatus === 'healthy'}
<button
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2"
on:click={() => restartService()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
</svg>
Force Redeploy
</button>
<button
on:click={() => stopService(false)}
type="submit"
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-error "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
Stop
</button>
{:else if $status.service.overallStatus === 'degraded'}
<button
on:click={() => stopService()}
type="submit"
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-error"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg> Stop
</button>
{:else if $status.service.overallStatus === 'stopped'}
{#if $status.service.overallStatus === 'degraded'}
<button
class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
on:click={() => restartService()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
</svg>
{$status.application.statuses.length === 1 ? 'Force Redeploy' : 'Redeploy Stack'}
</button>
{:else if $status.service.overallStatus === 'stopped'}
<button
class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
on:click={() => startService()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-pink-500"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
{/if}
{/if}
</div>
</div>
<div
class="mx-auto max-w-screen-2xl px-0 lg:px-10 grid grid-cols-1"
class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/services/${id}/configuration/`)}
>
{#if !$page.url.pathname.startsWith(`/services/${id}/configuration/`)}
<nav class="header flex flex-col lg:pt-0 ">
<Menu {service} {template} />
</nav>
{/if}
<div class="pt-0 col-span-0 lg:col-span-3 pb-24 px-4 lg:px-0">
<slot />
</div>
</div>

View File

@@ -0,0 +1,46 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { LayoutLoad } from './$types';
import { redirect } from '@sveltejs/kit';
function checkConfiguration(service: any): string | null {
let configurationPhase = null;
if (!service.type) {
configurationPhase = 'type';
} else if (!service.destinationDockerId) {
configurationPhase = 'destination';
}
return configurationPhase;
}
export const load: LayoutLoad = async ({ params, url }) => {
const { pathname } = new URL(url);
const { id } = params;
try {
const service = await trpc.services.getServices.query({ id });
if (!service) {
throw redirect(307, '/services');
}
const configurationPhase = checkConfiguration(service);
console.log({ configurationPhase });
// if (
// configurationPhase &&
// pathname !== `/applications/${params.id}/configuration/${configurationPhase}`
// ) {
// throw redirect(302, `/applications/${params.id}/configuration/${configurationPhase}`);
// }
return {
...service.data
};
} catch (err) {
if (err instanceof Error) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.' + '<br><br>' + err.message
});
}
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,562 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let service = data.service;
let template = data.template;
let tags = data.tags;
import cuid from 'cuid';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { errorNotification, getDomain } from '$lib/common';
import {
appSession,
status,
setLocation,
addToast,
checkIfDeploymentEnabledServices,
isDeploymentEnabled
} from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
import DocLink from '$lib/components/DocLink.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import ServiceStatus from './components/ServiceStatus.svelte';
import { saveForm } from './utils';
import Select from 'svelte-select';
import Wordpress from './components/Wordpress.svelte';
import { browser } from '$app/environment';
const { id } = $page.params;
let hostPorts = Object.keys(template).filter((key) => {
if (template[key]?.hostPorts?.length > 0) {
return true;
}
});
$: isDisabled =
!$appSession.isAdmin ||
$status.service.overallStatus === 'degraded' ||
$status.service.overallStatus === 'healthy' ||
$status.service.initialLoading;
let forceSave = false;
let loading = {
save: false,
verification: false,
cleanup: false
};
let dualCerts = service.dualCerts;
let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
function containerClass() {
return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200';
}
async function isDNSValid(domain: any, isWWW: any) {
try {
// await get(`/services/${id}/check?domain=${domain}`);
addToast({
message: 'DNS configuration is valid.',
type: 'success'
});
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
async function handleSubmit(e: any) {
if (loading.save) return;
loading.save = true;
try {
const formData = new FormData(e.target);
// await post(`/services/${id}/check`, {
// fqdn: service.fqdn,
// forceSave,
// dualCerts,
// exposePort: service.exposePort
// });
for (const setting of service.serviceSetting) {
if (setting.variableName?.startsWith('$$config_coolify_fqdn') && setting.value) {
for (let field of formData) {
const [key, value] = field;
if (setting.name === key) {
if (setting.value !== value) {
// await post(`/services/${id}/check`, {
// fqdn: value,
// otherFqdn: true
// });
}
}
}
}
}
if (formData) service = await saveForm(formData, service);
setLocation(service);
forceSave = false;
$isDeploymentEnabled = checkIfDeploymentEnabledServices(service);
return addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
//@ts-ignore
if (error?.message.startsWith('DNS not set')) {
forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(service.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
return errorNotification(error);
} finally {
loading.save = false;
}
}
async function setEmailsToVerified() {
loading.verification = true;
try {
// await post(`/services/${id}/${service.type}/activate`, { id: service.id });
return addToast({
message: 'Emails have been verified.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verification = false;
}
}
async function migrateAppwriteDB() {
loading.verification = true;
try {
// await post(`/services/${id}/${service.type}/migrate`, { id: service.id });
return addToast({
message: "Appwrite's database has been migrated.",
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verification = false;
}
}
async function changeSettings(name: any) {
if (!$appSession.isAdmin) return;
try {
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
// await post(`/services/${id}/settings`, { dualCerts });
return addToast({
message: 'Settings updated.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function cleanupLogs() {
loading.cleanup = true;
try {
// await post(`/services/${id}/${service.type}/cleanup`, { id: service.id });
return addToast({
message: 'Cleared unnecessary database logs.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.cleanup = false;
}
}
async function selectTag(event: any) {
service.version = event.detail.value;
}
onMount(async () => {
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
service.fqdn = `http://${cuid()}.demo.coolify.io`;
// if (service.type === 'wordpress') {
// service.wordpress.mysqlDatabase = 'db';
// }
// if (service.type === 'plausibleanalytics') {
// service.plausibleAnalytics.email = 'noreply@demo.com';
// service.plausibleAnalytics.username = 'admin';
// }
// if (service.type === 'minio') {
// service.minio.apiFqdn = `http://${cuid()}.demo.coolify.io`;
// }
// if (service.type === 'ghost') {
// service.ghost.mariadbDatabase = 'db';
// }
// if (service.type === 'fider') {
// service.fider.emailNoreply = 'noreply@demo.com';
// }
// await handleSubmit();
}
});
</script>
<div class="w-full">
<form id="saveForm" on:submit|preventDefault={handleSubmit}>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 ">General</div>
{#if $appSession.isAdmin}
<button
type="submit"
class="btn btn-sm"
class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave}
class:loading={loading.save}
class:btn-primary={!loading.save}
disabled={loading.save}
>{loading.save ? 'Saving...' : forceSave ? 'Continue' : 'Force Save'}</button
>
{/if}
{#if service.type === 'plausibleanalytics' && $status.service.overallStatus === 'healthy'}
<button
class="btn btn-sm"
on:click|preventDefault={setEmailsToVerified}
disabled={loading.verification}
class:loading={loading.verification}
>{loading.verification ? 'Verifying...' : 'Verify without SMTP'}</button
>
<button
class="btn btn-sm"
on:click|preventDefault={cleanupLogs}
disabled={loading.cleanup}
class:loading={loading.cleanup}>Cleanup Unnecessary Database Logs</button
>
{/if}
{#if service.type === 'appwrite' && $status.service.overallStatus === 'healthy'}
<button
class="btn btn-sm"
on:click|preventDefault={migrateAppwriteDB}
disabled={loading.verification}
class:loading={loading.verification}
>{loading.verification
? 'Migrating... it may take a while...'
: "Migrate Appwrite's Database"}</button
>
<div>
<DocLink url="https://appwrite.io/docs/upgrade#run-the-migration" />
</div>
{/if}
</div>
</div>
<div class="grid grid-flow-row gap-2 px-4">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name">Name</label>
<input
name="name"
id="name"
class="w-full"
disabled={!$appSession.isAdmin}
bind:value={service.name}
required
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="version">Version / Tag</label>
{#if tags.tags?.length > 0}
<div class="custom-select-wrapper w-full">
<Select
form="saveForm"
containerClasses={isDisabled && containerClass()}
{isDisabled}
id="version"
showIndicator={!isDisabled}
items={[...tags.tags]}
on:select={selectTag}
value={service.version}
isClearable={false}
/>
</div>
{:else}
<input class="w-full border-red-500" disabled placeholder="Error getting tags..." />
{/if}
</div>
<div class="grid grid-cols-2 items-center">
<label for="destination">Destination</label>
<div>
{#if service.destinationDockerId}
<div class="no-underline">
<input
value={service.destinationDocker.name}
id="destination"
disabled
class="bg-transparent w-full"
/>
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-center">
<label for="fqdn"
>FQDN
<Explainer
explanation={"If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white '>You must set your DNS to point to the server IP in advance.</span>"}
/>
</label>
<CopyPasswordField
placeholder="eg: https://coollabs.io"
readonly={isDisabled}
disabled={isDisabled}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
</div>
{#each Object.keys(template) as oneService}
{#each template[oneService].fqdns as fqdn}
<div class="grid grid-cols-2 items-center py-1">
<label for={fqdn.name}>{fqdn.label || fqdn.name}</label>
<CopyPasswordField
placeholder="eg: https://coolify.io"
readonly={isDisabled}
disabled={isDisabled}
required={fqdn.required}
name={fqdn.name}
id={fqdn.name}
bind:value={fqdn.value}
/>
</div>
{/each}
{/each}
</div>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="btn btn-sm bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="btn btn-sm bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="btn btn-sm bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="btn btn-sm bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
<div class="grid grid-flow-row gap-2 px-4">
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
disabled={$status.service.isRunning || !$appSession.isAdmin}
dataTooltip="You must stop the application to change this setting."
bind:setting={dualCerts}
title="Generate SSL for www and non-www?"
description={"It will generate certificates for both www and non-www. <br>You need to have <span class='text-settings'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."}
on:click={() => !$status.service.isRunning && changeSettings('dualCerts')}
/>
</div>
{#if hostPorts.length === 0}
<div class="grid grid-cols-2 items-center">
<label for="exposePort"
>Exposed Port <Explainer
explanation={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
/></label
>
<input
class="w-full"
readonly={isDisabled}
disabled={isDisabled}
name="exposePort"
id="exposePort"
bind:value={service.exposePort}
placeholder="12345"
/>
</div>
{/if}
</div>
<div class="pt-6">
{#each Object.keys(template) as oneService}
<div
class="flex flex-row my-2 space-x-2 mb-6"
class:my-6={template[oneService].environment.length > 0 &&
template[oneService].environment.find((env) => env.main === oneService)}
class:border-b={template[oneService].environment.length > 0 &&
template[oneService].environment.find((env) => env.main === oneService)}
class:border-coolgray-500={template[oneService].environment.length > 0 &&
template[oneService].environment.find((env) => env.main === oneService)}
>
<div class="title font-bold pb-3 capitalize">
{template[oneService].name ||
oneService.replace(`${id}-`, '').replace(id, service.type)}
</div>
<ServiceStatus id={oneService} />
</div>
<div class="grid grid-flow-row gap-2 px-4">
{#if template[oneService].environment.length > 0}
{#each template[oneService].environment as variable}
{#if variable.main === oneService}
<div class="grid grid-cols-2 items-center gap-2">
<label class="h-10" for={variable.name}
>{variable.label || variable.name}
{#if variable.description}
<Explainer explanation={variable.description} />
{/if}</label
>
{#if variable.defaultValue === '$$generate_fqdn'}
<CopyPasswordField
disabled
readonly
name={variable.name}
id={variable.name}
value={service.fqdn}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === '$$generate_fqdn_slash'}
<CopyPasswordField
disabled
readonly
name={variable.name}
id={variable.name}
value={service.fqdn + '/' || ''}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === '$$generate_domain'}
<CopyPasswordField
disabled
readonly
name={variable.name}
id={variable.name}
value={getDomain(service.fqdn) || ''}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === '$$generate_network'}
<CopyPasswordField
disabled
readonly
name={variable.name}
id={variable.name}
value={service.destinationDocker.network}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === 'true' || variable.defaultValue === 'false'}
{#if variable.value === 'true' || variable.value === 'false' || variable.value === 'invite_only'}
<select
class="w-full font-normal"
readonly={isDisabled}
disabled={isDisabled}
id={variable.name}
name={variable.name}
bind:value={variable.value}
form="saveForm"
placeholder={variable.placeholder}
required={variable?.required}
>
<option value="true">enabled</option>
<option value="false">disabled</option>
{#if service.type.startsWith('plausibleanalytics') && variable.id == 'config_disable_registration'}
<option value="invite_only">invite_only</option>
{/if}
</select>
{:else}
<select
class="w-full font-normal"
readonly={isDisabled}
disabled={isDisabled}
id={variable.name}
name={variable.name}
bind:value={variable.defaultValue}
form="saveForm"
placeholder={variable.placeholder}
required={variable?.required}
>
<option value="true">true</option>
<option value="false">false</option>
</select>
{/if}
{:else if variable.defaultValue === '$$generate_password'}
<CopyPasswordField
isPasswordField
readonly
disabled
name={variable.name}
id={variable.name}
value={variable.value}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.type === 'textarea'}
<textarea
class="w-full"
value={variable.value}
readonly={isDisabled}
disabled={isDisabled}
class:resize-none={$status.service.overallStatus === 'healthy'}
rows="5"
name={variable.name}
id={variable.name}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else}
<CopyPasswordField
isPasswordField={variable.id.startsWith('secret')}
required={variable?.required}
readonly={variable.readOnly || isDisabled}
disabled={variable.readOnly || isDisabled}
name={variable.name}
id={variable.name}
value={variable.value}
placeholder={variable.placeholder}
/>
{/if}
</div>
{/if}
{/each}
{#if template[oneService].name.toLowerCase() === 'wordpress' && service.type.startsWith('wordpress')}
<Wordpress {service} />
{/if}
{/if}
</div>
{/each}
</div>
</form>
</div>

View File

@@ -0,0 +1,138 @@
<script lang="ts">
export let service: any;
export let template: any;
import { page } from '$app/stores';
import { appSession } from '$lib/store';
import ServiceLinks from './ServiceLinks.svelte';
</script>
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
<li class="menu-title">
<span>General</span>
</li>
<li class="rounded">
<ServiceLinks {template} {service} linkToDocs={true} />
</li>
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}`}>
<a href={`/services/${$page.params.id}`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
/>
</svg>Configurations</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/secrets`}
>
<a href={`/services/${$page.params.id}/secrets`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg>Secrets</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/storages`}
>
<a href={`/services/${$page.params.id}/storages`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>Persistent Volumes</a
>
</li>
<li class="menu-title">
<span>Logs</span>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/logs`}
>
<a
href={`/services/${$page.params.id}/logs`}
class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>Service</a
>
</li>
{#if $appSession.isAdmin}
<li class="menu-title">
<span>Advanced</span>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/danger`}
>
<a href={`/services/${$page.params.id}/danger`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 9v2m0 4v.01" />
<path
d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"
/>
</svg>Danger Zone</a
>
</li>
{/if}
</ul>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import DocLink from '$lib/components/DocLink.svelte';
import ServiceIcons from '$lib/components/icons/services/ServiceIcons.svelte';
export let service: any;
export let template: any;
export let linkToDocs: boolean = false;
const name: any = service.type && service.type[0].toUpperCase() + service.type.substring(1);
</script>
{#if linkToDocs}
<DocLink url={template[service?.id]?.documentation || 'https://docs.coollabs.io'} text={`Documentation`} isExternal={true} />
{:else}
<ServiceIcons type={service.type} />
{/if}

View File

@@ -0,0 +1,37 @@
<script lang="ts">
export let id: any;
import { status } from '$lib/store';
let serviceStatus = {
isExcluded: false,
isExited: false,
isRunning: false,
isRestarting: false,
isStopped: false
};
$: if (Object.keys($status.service.statuses).length > 0 && $status.service.statuses[id]?.status) {
let { isExited, isRunning, isRestarting, isExcluded } = $status.service.statuses[id].status;
serviceStatus.isExited = isExited;
serviceStatus.isRunning = isRunning;
serviceStatus.isExcluded = isExcluded;
serviceStatus.isRestarting = isRestarting;
serviceStatus.isStopped = !isExited && !isRunning && !isRestarting;
} else {
serviceStatus.isExited = false;
serviceStatus.isRunning = false;
serviceStatus.isExcluded = false;
serviceStatus.isRestarting = false;
serviceStatus.isStopped = true;
}
</script>
{#if serviceStatus.isExcluded}
<span class="badge font-bold uppercase rounded text-orange-500 mt-2">Excluded</span>
{:else if serviceStatus.isRunning}
<span class="badge font-bold uppercase rounded text-green-500 mt-2">Running</span>
{:else if serviceStatus.isStopped || serviceStatus.isExited}
<span class="badge font-bold uppercase rounded text-red-500 mt-2">Stopped</span>
{:else if serviceStatus.isRestarting}
<span class="badge font-bold uppercase rounded text-yellow-500 mt-2">Restarting</span>
{/if}

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { page } from '$app/stores';
import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
import { errorNotification, getDomain } from '$lib/common';
import { browser } from '$app/environment';
export let service: any;
const { id } = $page.params;
const settings = service.settings;
const { ipv4, ipv6 } = settings;
let ftpUrl = generateUrl(service.wordpress?.ftpPublicPort) || '';
let ftpUser = service.wordpress?.ftpUser;
let ftpPassword = service.wordpress?.ftpPassword;
let ftpLoading = false;
let ftpEnabled = service.wordpress?.ftpEnabled || false;
function generateUrl(publicPort: any) {
return browser
? `sftp://${settings?.fqdn ? getDomain(settings.fqdn) : ipv4 || ipv6}:${publicPort}`
: 'Loading...';
}
async function changeSettings(name: any) {
if (ftpLoading) return;
if ($status.service.overallStatus === 'healthy') {
ftpLoading = true;
if (name === 'ftpEnabled') {
ftpEnabled = !ftpEnabled;
}
try {
// const {
// publicPort,
// ftpUser: user,
// ftpPassword: password
// } = await post(`/services/${id}/wordpress/ftp`, {
// ftpEnabled
// });
// ftpUrl = generateUrl(publicPort);
// ftpUser = user;
// ftpPassword = password;
// service.wordpress.ftpEnabled = ftpEnabled;
} catch (error) {
return errorNotification(error);
} finally {
ftpLoading = false;
}
}
}
</script>
<div class="grid grid-cols-2 items-center">
<Setting
id="ftpEnabled"
bind:setting={ftpEnabled}
loading={ftpLoading}
disabled={$status.service.overallStatus !== 'healthy'}
on:click={() => changeSettings('ftpEnabled')}
title="Enable sFTP connection to WordPress data"
description="Enables an on-demand sFTP connection to the WordPress data directory. This is useful if you want to use sFTP to upload files."
/>
</div>
{#if service.wordpress?.ftpEnabled}
<div class="grid grid-cols-2 items-center">
<label for="ftpUrl">sFTP Connection URI</label>
<CopyPasswordField id="ftpUrl" readonly disabled name="ftpUrl" value={ftpUrl} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="ftpUser">User</label>
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="ftpPassword">Password</label>
<CopyPasswordField
id="ftpPassword"
isPasswordField
readonly
disabled
name="ftpPassword"
value={ftpPassword}
/>
</div>
{/if}

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let service: any = data.service.data;
import { page } from '$app/stores';
import { appSession, status, trpc } from '$lib/store';
import { errorNotification } from '$lib/common';
import { goto } from '$app/navigation';
const { id } = $page.params;
async function deleteService() {
const sure = confirm('Are you sure you want to delete this service?');
if (sure) {
$status.service.initialLoading = true;
try {
if (service.type && $status.service.overallStatus !== 'stopped') {
await trpc.services.stop.mutate({ id });
}
await trpc.services.delete.mutate({ id });
return await goto('/');
} catch (error) {
return errorNotification(error);
} finally {
$status.service.initialLoading = false;
}
}
}
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Danger Zone</div>
</div>
<button
id="forcedelete"
on:click={() => deleteService()}
type="submit"
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-lg btn-error text-sm"
>
Delete Service
</button>
</div>

View File

@@ -0,0 +1,173 @@
<script lang="ts">
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import { trpc } from '$lib/store';
import { onDestroy, onMount } from 'svelte';
let service: any = {};
let template: any = null;
let logsLoading = false;
let loadLogsInterval: any = null;
let logs: any = [];
let lastLog: any = null;
let followingInterval: any;
let followingLogs: any;
let logsEl: any;
let position = 0;
let selectedService: any = null;
let noContainer = false;
const { id } = $page.params;
onMount(async () => {
const { data } = await trpc.services.getServices.query({ id });
template = data.template;
service = data.service;
});
onDestroy(() => {
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
async function loadLogs() {
if (logsLoading) return;
try {
const { data } = await trpc.services.getLogs.query({
id,
containerId: selectedService,
since: Number(lastLog?.split(' ')[0]) || 0
});
if (data.noContainer) {
noContainer = true;
logs = [];
if (logs.length > 0) {
clearInterval(loadLogsInterval);
selectedService = null;
}
return;
} else {
noContainer = false;
}
if (data?.logs && data.logs[data.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(data.logs);
lastLog = data.logs[data.logs.length - 1];
}
} catch (error) {
return errorNotification(error);
}
}
function detect() {
if (position < logsEl.scrollTop) {
position = logsEl.scrollTop;
} else {
if (followingLogs) {
clearInterval(followingInterval);
followingLogs = false;
}
position = logsEl.scrollTop;
}
}
function followBuild() {
followingLogs = !followingLogs;
if (followingLogs) {
followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}, 1000);
} else {
clearInterval(followingInterval);
}
}
async function selectService(service: any, init: boolean = false) {
if (loadLogsInterval) clearInterval(loadLogsInterval);
if (followingInterval) clearInterval(followingInterval);
logs = [];
lastLog = null;
followingLogs = false;
selectedService = service;
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
}
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Service Logs</div>
</div>
</div>
{#if template}
<div class="grid grid-cols-3 gap-2 lg:gap-8 pb-4">
{#each Object.keys(template) as service}
<button
on:click={() => selectService(service, true)}
class:bg-primary={selectedService === service}
class:bg-coolgray-200={selectedService !== service}
class="w-full rounded p-5 hover:bg-primary font-bold"
>
{#if template[service].name}
{template[service].name || ''} <br /><span class="text-xs">({service})</span>
{:else}
<span>{service}</span>
{/if}
</button>
{/each}
</div>
{:else}
<div class="w-full flex justify-center font-bold text-xl">Loading components...</div>
{/if}
{#if selectedService}
<div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0}
{#if noContainer}
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
{/if}
{:else}
<div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2">
<button on:click={followBuild} class="btn btn-sm " class:bg-coollabs={followingLogs}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
>Streaming logs</button
>
{/if}
</div>
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div>
{/if}
</div>
{/if}

View File

@@ -0,0 +1,98 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let secrets = data.secrets;
import Secret from './components/Secret.svelte';
import { page } from '$app/stores';
import pLimit from 'p-limit';
import { addToast, appSession, trpc } from '$lib/store';
import { saveSecret } from './utils';
const limit = pLimit(1);
const { id } = $page.params;
let batchSecrets = '';
async function refreshSecrets() {
const { data } = await trpc.services.getSecrets.query({ id });
secrets = [...data.secrets];
}
async function getValues() {
if (!batchSecrets) return;
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => {
const [name, ...rest] = secret.split('=');
const value = rest.join('=');
const cleanValue = value?.replaceAll('"', '') || '';
return {
name: name.trim(),
value: cleanValue.trim(),
isNew: !secrets.find((secret: any) => name === secret.name)
};
});
await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) =>
limit(() => saveSecret({ name, value, serviceId: id, isNew }))
)
);
batchSecrets = '';
await refreshSecrets();
addToast({
message: 'Secrets saved.',
type: 'success'
});
}
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Secrets</div>
</div>
<div class="overflow-x-auto">
<table class="w-full border-separate text-left">
<thead>
<tr class="uppercase">
<th scope="col">Name</th>
<th scope="col uppercase">Value</th>
<th scope="col uppercase" class="w-96 text-center">Action</th>
</tr>
</thead>
<tbody class="space-y-2">
{#each secrets as secret}
{#key secret.id}
<tr>
<Secret
name={secret.name}
value={secret.value}
readonly={secret.readOnly}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
<tr>
<Secret isNewSecret on:refresh={refreshSecrets} />
</tr>
</tbody>
</table>
</div>
{#if $appSession.isAdmin}
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
<div class="flex flex-row space-x-2">
<div class="title font-bold pb-3 ">Paste <code>.env</code> file</div>
<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button>
</div>
</div>
<textarea
placeholder={`PORT=1337\nPASSWORD=supersecret`}
bind:value={batchSecrets}
class="mb-2 min-h-[200px] w-full"
/>
</form>
{/if}
</div>

View File

@@ -0,0 +1,16 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { PageLoad } from './$types';
export const ssr = false;
export const load: PageLoad = async ({ params }) => {
try {
const { id } = params;
const { data } = await trpc.services.getSecrets.query({ id });
return data;
} catch (err) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,101 @@
<script lang="ts">
export let name = '';
export let value = '';
export let readonly = false;
export let isNewSecret = false;
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast, appSession, trpc } from '$lib/store';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const { id } = $page.params;
async function removeSecret() {
try {
await trpc.services.deleteSecret.mutate({
name,
id
});
dispatch('refresh');
if (isNewSecret) {
name = '';
value = '';
}
} catch (error) {
return errorNotification(error);
}
}
async function saveSecret(isNew = false) {
if (!name) return errorNotification({ message: 'Name is required.' });
if (!value) return errorNotification({ message: 'Value is required.' });
try {
await trpc.services.createSecret.mutate({
name,
value,
id,
isNew
});
dispatch('refresh');
if (isNewSecret) {
name = '';
value = '';
}
addToast({
message: 'Secret saved.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
</script>
<td>
<input
style="min-width: 350px !important;"
id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:value={name}
required
placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret || readonly}
class="w-full"
class:bg-coolblack={!isNewSecret}
class:border={!isNewSecret}
class:border-dashed={!isNewSecret}
class:border-coolgray-300={!isNewSecret}
/>
</td>
<td>
<CopyPasswordField
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
disabled={readonly}
{readonly}
isPasswordField={true}
bind:value
placeholder="J$#@UIO%HO#$U%H"
inputStyle="min-width: 350px; !important"
/>
</td>
{#if $appSession.isAdmin}
<td>
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveSecret(true)}>Add</button>
</div>
{:else if !readonly}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveSecret(false)}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="btn btn-sm bg-error" on:click={removeSecret}>Remove</button>
</div>
</div>
{/if}
</td>
{/if}

View File

@@ -0,0 +1,78 @@
import { errorNotification } from '$lib/common';
import { trpc } from '$lib/store';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
serviceId: string;
};
export async function saveSecret({
isNew,
name,
value,
isBuildSecret,
isNewSecret
}: Props): Promise<void> {
if (!name) return errorNotification('Name is required');
if (!value) return errorNotification('Value is required');
try {
await trpc.services.createSecret.mutate({
name,
value,
isBuildSecret,
isNew: isNew || false
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
} catch (error) {
throw error;
}
}
export async function saveForm(formData: any, service: any) {
const settings = service.serviceSetting.map((setting: { name: string }) => setting.name);
const secrets = service.serviceSecret.map((secret: { name: string }) => secret.name);
const baseCoolifySetting = ['name', 'fqdn', 'exposePort', 'version'];
for (let field of formData) {
const [key, value] = field;
if (secrets.includes(key) && value) {
await trpc.services.createSecret.mutate({
name: key,
value
});
} else {
service.serviceSetting = service.serviceSetting.map((setting: any) => {
if (setting.name === key) {
setting.changed = true;
setting.value = value;
}
return setting;
});
if (!settings.includes(key) && !baseCoolifySetting.includes(key)) {
service.serviceSetting.push({
id: service.id,
name: key,
value: value,
isNew: true
});
}
if (baseCoolifySetting.includes(key)) {
service[key] = value;
}
}
}
await trpc.services.saveService.mutate(service);
const {
data: { service: reloadedService }
} = await trpc.services.getServices.query({ id: service.id });
return reloadedService;
}

View File

@@ -0,0 +1,73 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let persistentStorages = data.persistentStorages;
let template = data.template;
import { page } from '$app/stores';
import Storage from './components/Storage.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { appSession, trpc } from '$lib/store';
const { id } = $page.params;
async function refreshStorage() {
const { data } = await trpc.services.getStorages.query({ id });
persistentStorages = [...data.persistentStorages];
}
let services = Object.keys(template).map((service) => {
if (template[service]?.name) {
return {
name: template[service].name,
id: service
};
} else {
return service;
}
});
</script>
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">
Persistent Volumes <Explainer
position="dropdown-bottom"
explanation="You can specify any folder that you want to be persistent across deployments.<br><br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/example</span> between deployments.<br><br>Your application's data is copied to <span class='text-settings '>/app</span> inside the container, you can preserve data under it as well, like <span class='text-settings '>/app/db</span>.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
/>
</div>
</div>
{#if persistentStorages.filter((s) => s.predefined).length > 0}
<div class="title">Predefined Volumes</div>
<div class="w-full lg:px-0 px-4">
<div class="grid grid-col-1 lg:grid-cols-2 pt-2 gap-2">
<div class="font-bold uppercase">Container</div>
<div class="font-bold uppercase">Volume ID : Mount Dir</div>
</div>
</div>
{#each persistentStorages.filter((s) => s.predefined) as storage}
{#key storage.id}
<Storage on:refresh={refreshStorage} {storage} {services} />
{/key}
{/each}
{/if}
{#if persistentStorages.filter((s) => !s.predefined).length > 0}
<div class="title" class:pt-10={persistentStorages.filter((s) => s.predefined).length > 0}>
Custom Volumes
</div>
{#each persistentStorages.filter((s) => !s.predefined) as storage}
{#key storage.id}
<Storage on:refresh={refreshStorage} {storage} {services} />
{/key}
{/each}
{/if}
{#if $appSession.isAdmin}
<div class="title" class:pt-10={persistentStorages.filter((s) => s.predefined).length > 0}>
Add New Volume
</div>
<Storage on:refresh={refreshStorage} isNew {services} />
{/if}
</div>
</div>

View File

@@ -0,0 +1,16 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { PageLoad } from './$types';
export const ssr = false;
export const load: PageLoad = async ({ params }) => {
try {
const { id } = params;
const { data } = await trpc.services.getStorages.query({ id });
return data;
} catch (err) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,167 @@
<script lang="ts">
export let isNew = false;
export let storage: any = {};
export let services: any = [];
import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte';
import { errorNotification } from '$lib/common';
import { addToast, trpc } from '$lib/store';
const { id } = $page.params;
const dispatch = createEventDispatcher();
async function saveStorage(e: any) {
try {
const formData = new FormData(e.target);
let isNewStorage = true;
let newStorage: any = {
id: null,
containerId: null,
path: null
};
for (let field of formData) {
const [key, value] = field;
newStorage[key] = value;
}
newStorage.path = newStorage.path.startsWith('/') ? newStorage.path : `/${newStorage.path}`;
newStorage.path = newStorage.path.endsWith('/')
? newStorage.path.slice(0, -1)
: newStorage.path;
newStorage.path.replace(/\/\//g, '/');
await trpc.services.saveStorage.mutate({
id,
path: newStorage.path,
storageId: newStorage.id,
containerId: newStorage.containerId,
isNewStorage
});
dispatch('refresh');
if (isNew) {
storage.path = null;
storage.id = null;
}
if (isNewStorage) {
addToast({
message: 'Storage added',
type: 'success'
});
} else {
addToast({
message: 'Storage updated',
type: 'success'
});
}
} catch (error) {
return errorNotification(error);
}
}
async function removeStorage(removableStorage: any) {
try {
const { id: storageId, volumeName, path } = removableStorage;
const sure = confirm(
`Are you sure you want to delete this storage ${volumeName + ':' + path}?`
);
if (sure) {
await trpc.services.deleteStorage.mutate({ storageId });
dispatch('refresh');
addToast({
message: 'Storage deleted',
type: 'success'
});
}
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="w-full lg:px-0 px-4">
{#if storage.predefined}
<div class="grid grid-col-1 lg:grid-cols-2 pt-2 gap-2">
<div>
<input
id={storage.containerId}
disabled
readonly
class="w-full"
value={`${
services.find((s) => s.id === storage.containerId).name || storage.containerId
}`}
/>
</div>
<div>
<input
id={storage.volumeName}
disabled
readonly
class="w-full"
value={`${storage.volumeName}:${storage.path}`}
/>
</div>
</div>
{:else if isNew}
<form id="saveVolumesForm" on:submit|preventDefault={saveStorage}>
<div class="grid grid-col-1 lg:grid-cols-2 lg:space-x-4 pt-8">
<div class="flex flex-row">
<div class="flex flex-col w-full">
<label for="name" class="pb-2 uppercase font-bold">Container</label>
<select
form="saveVolumesForm"
name="containerId"
class="w-full lg:w-64"
disabled={storage.predefined}
readonly={storage.predefined}
bind:value={storage.containerId}
>
{#if services.length === 1}
{#if services[0].name}
<option selected value={services[0].id}>{services[0].name}</option>
{:else}
<option selected value={services[0]}>{services[0]}</option>
{/if}
{:else}
{#each services as service}
{#if service.name}
<option value={service.id}>{service.name}</option>
{:else}
<option value={service}>{service}</option>
{/if}
{/each}
{/if}
</select>
</div>
<div class="flex flex-col w-full">
<label for="name" class="pb-2 uppercase font-bold">Path</label>
<input
name="path"
disabled={storage.predefined}
readonly={storage.predefined}
class="w-full lg:w-64"
bind:value={storage.path}
required
placeholder="eg: /sqlite.db"
/>
</div>
</div>
<div class="pt-8">
<button type="submit" class="btn btn-sm btn-primary w-full lg:w-64">Add</button>
</div>
</div>
</form>
{:else}
<div class="flex lg:flex-row flex-col items-center gap-2 py-1">
<input
disabled
readonly
class="w-full"
value={`${services.find((s) => s.id === storage.containerId).name || storage.containerId}`}
/>
<input disabled readonly class="w-full" value={`${storage.volumeName}:${storage.path}`} />
<button
class="btn btn-sm btn-error"
on:click|stopPropagation|preventDefault={() => removeStorage(storage)}>Remove</button
>
</div>
{/if}
</div>

View File

@@ -0,0 +1,79 @@
import { errorNotification } from '$lib/common';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
serviceId: string;
};
export async function saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
serviceId
}: Props): Promise<void> {
if (!name) return errorNotification('Name is required');
if (!value) return errorNotification('Value is required');
try {
// await post(`/services/${serviceId}/secrets`, {
// name,
// value,
// isBuildSecret,
// isPRMRSecret,
// isNew: isNew || false
// });
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
} catch (error) {
throw error
}
}
export async function saveForm(formData: any, service: any) {
const settings = service.serviceSetting.map((setting: { name: string }) => setting.name);
const secrets = service.serviceSecret.map((secret: { name: string }) => secret.name);
const baseCoolifySetting = ['name', 'fqdn', 'exposePort', 'version'];
for (let field of formData) {
const [key, value] = field;
if (secrets.includes(key) && value) {
// await post(`/services/${service.id}/secrets`, {
// name: key,
// value,
// });
} else {
service.serviceSetting = service.serviceSetting.map((setting: any) => {
if (setting.name === key) {
setting.changed = true;
setting.value = value;
}
return setting;
});
if (!settings.includes(key) && !baseCoolifySetting.includes(key)) {
service.serviceSetting.push({
id: service.id,
name: key,
value: value,
isNew: true
});
}
if (baseCoolifySetting.includes(key)) {
service[key] = value;
}
}
}
// await post(`/services/${service.id}`, { ...service });
// const { service: reloadedService } = await get(`/services/${service.id}`);
// return reloadedService;
}

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import type { LayoutData } from './$types';
export let data: LayoutData;
let source = data.source.data.source;
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import { appSession, trpc } from '$lib/store';
import * as Icons from '$lib/components/icons';
import { goto } from '$app/navigation';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params;
async function deleteSource(name: string) {
const sure = confirm('Are you sure you want to delete ' + name + '?');
if (sure) {
try {
await trpc.sources.delete.mutate({ id });
await goto('/', { replaceState: true });
} catch (error) {
errorNotification(error);
}
}
}
</script>
{#if id !== 'new' && $appSession.teamId === '0'}
<nav class="nav-side">
<button
id="delete"
on:click={() => deleteSource(source.name)}
type="submit"
disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"><Icons.Delete /></button
>
</nav>
<Tooltip triggeredBy="#delete">Delete</Tooltip>
{/if}
<slot />

View File

@@ -0,0 +1,35 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { LayoutLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: LayoutLoad = async ({ params, url }) => {
const { pathname } = new URL(url);
const { id } = params;
try {
const source = await trpc.sources.getSourceById.query({ id });
if (!source) {
throw redirect(307, '/sources');
}
// if (
// configurationPhase &&
// pathname !== `/applications/${params.id}/configuration/${configurationPhase}`
// ) {
// throw redirect(302, `/applications/${params.id}/configuration/${configurationPhase}`);
// }
return {
source
};
} catch (err) {
if (err instanceof Error) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.' + '<br><br>' + err.message
});
}
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import type { LayoutData } from './$types';
export let data: LayoutData;
let source = data.source.data.source;
let settings = data.source.data.settings;
import { page } from '$app/stores';
import Source from './components/Source.svelte';
import New from './components/New.svelte';
const { id } = $page.params;
</script>
{#if id === 'new'}
<New bind:source bind:settings />
{:else}
<Source bind:source bind:settings />
{/if}

View File

@@ -0,0 +1,264 @@
<script lang="ts">
export let source: any;
export let settings: any;
import { page } from '$app/stores';
import { dashify, errorNotification, getAPIUrl, getDomain, getWebhookUrl } from '$lib/common';
import { addToast, appSession, trpc } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { dev } from '$app/environment';
const { id } = $page.params;
$: selfHosted = source.htmlUrl !== 'https://github.com';
let loading = false;
async function handleSubmit() {
loading = true;
try {
await trpc.sources.save.mutate({
id: source.id,
name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, ''),
isSystemWide: source.isSystemWide
});
return addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
async function newGithubApp() {
loading = true;
try {
const { id } = await trpc.sources.newGitHubApp.mutate({
id: source.id,
name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, ''),
organization: source.organization,
customPort: source.customPort,
isSystemWide: source.isSystemWide
});
const { organization, htmlUrl } = source;
const { fqdn, ipv4, ipv6 } = settings;
const host = dev ? getAPIUrl() : fqdn ? fqdn : `http://${ipv4 || ipv6}` || '';
const domain = getDomain(fqdn);
let url = 'settings/apps/new';
if (organization) url = `organizations/${organization}/settings/apps/new`;
const name = dashify(domain) || 'app';
const data = JSON.stringify({
name: `coolify-${name}`,
url: host,
hook_attributes: {
url: dev ? getWebhookUrl('github') : `${host}/webhooks/github/events`
},
redirect_url: `${host}/webhooks/github`,
callback_urls: [`${host}/login/github/app`],
public: false,
request_oauth_on_install: false,
setup_url: `${host}/webhooks/github/install?gitSourceId=${id}`,
setup_on_update: true,
default_permissions: {
contents: 'read',
metadata: 'read',
pull_requests: 'read',
emails: 'read'
},
default_events: ['pull_request', 'push']
});
const form = document.createElement('form');
form.setAttribute('method', 'post');
form.setAttribute('action', `${htmlUrl}/${url}?state=${id}`);
const input = document.createElement('input');
input.setAttribute('id', 'manifest');
input.setAttribute('name', 'manifest');
input.setAttribute('type', 'hidden');
input.setAttribute('value', data);
form.appendChild(input);
document.getElementsByTagName('body')[0].appendChild(form);
form.submit();
} catch (error) {
return errorNotification(error);
}
}
async function changeSettings(name: any, save: boolean) {
if ($appSession.teamId === '0') {
if (name === 'isSystemWide') {
source.isSystemWide = !source.isSystemWide;
}
if (save) {
await handleSubmit();
}
}
}
</script>
<div class="mx-auto max-w-6xl lg:px-6 px-3">
{#if !source.githubAppId}
<form on:submit|preventDefault={newGithubApp} class="py-4">
<div class="grid gap-1 lg:grid-flow-col pb-7">
<h1 class="title">General</h1>
{#if !source.githubAppId}
<div class="w-full flex flex-rpw justify-end">
<button class="btn btn-sm bg-sources mt-5 w-full lg:w-fit" type="submit"
>Save & Redirect to GitHub</button
>
</div>
{/if}
</div>
<div class="grid gap-2 grid-cols-2 auto-rows-max">
<label for="name">Name</label>
<input class="w-full" name="name" id="name" required bind:value={source.name} />
<label for="htmlUrl">HTML URL</label>
<input class="w-full" name="htmlUrl" id="htmlUrl" required bind:value={source.htmlUrl} />
<label for="apiUrl">API URL</label>
<input class="w-full" name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
<label for="customPort"
>Custom SSH Port <Explainer
explanation={'If you use a self-hosted version of Git, you can provide custom port for all the Git related actions.'}
/></label
>
<input
class="w-full"
name="customPort"
id="customPort"
disabled={!selfHosted || source.githubAppId}
readonly={!selfHosted || source.githubAppId}
required
value={source.customPort}
/>
<label for="organization" class="pt-2"
>Organization
<Explainer
explanation={"Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."}
/></label
>
<input
class="w-full"
name="organization"
id="organization"
placeholder="eg: coollabsio"
bind:value={source.organization}
/>
<Setting
customClass="pt-4"
id="autodeploy"
isCenter={false}
bind:setting={source.isSystemWide}
on:click={() => changeSettings('isSystemWide', false)}
title="System Wide Git"
description="System Wide Git are available to all the users in your Coolify instance. <br><br> <span class='font-bold text-warning'>Use with caution, as it can be a security risk.</span>"
/>
</div>
</form>
{:else if source.githubApp?.installationId}
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex lg:flex-row lg:justify-between flex-col space-y-3 w-full lg:items-center">
<h1 class="title">General</h1>
{#if $appSession.isAdmin && $appSession.teamId === '0'}
<div
class="flex flex-col lg:flex-row lg:space-x-4 lg:w-fit space-y-2 lg:space-y-0 w-full"
>
<button class="btn btn-sm bg-sources" type="submit" disabled={loading}
>{loading ? 'Saving...' : 'Save'}</button
>
<a
class="btn btn-sm"
href={`${source.htmlUrl}/${
source.htmlUrl === 'https://github.com' ? 'apps' : 'github-apps'
}/${source.githubApp.name}/installations/new`}>Change GitHub App Settings</a
>
</div>
{/if}
</div>
<div class="grid gap-2 grid-cols-2 auto-rows-max mt-4">
<label for="name">Name</label>
<input
class="w-full"
name="name"
id="name"
required
bind:value={source.name}
disabled={!$appSession.isAdmin}
/>
<label for="htmlUrl">HTML URL</label>
<input
class="w-full"
name="htmlUrl"
id="htmlUrl"
disabled={source.githubAppId}
readonly={source.githubAppId}
required
bind:value={source.htmlUrl}
/>
<label for="apiUrl">API URL</label>
<input
class="w-full"
name="apiUrl"
id="apiUrl"
required
disabled={source.githubAppId}
readonly={source.githubAppId}
bind:value={source.apiUrl}
/>
<label for="customPort"
>Custom SSH Port <Explainer
explanation="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/></label
>
<input
class="w-full"
name="customPort"
id="customPort"
disabled={!selfHosted}
readonly={!selfHosted}
required
value={source.customPort}
/>
<label for="organization" class="pt-2">Organization</label>
<input
class="w-full"
readonly
disabled
name="organization"
id="organization"
placeholder="eg: coollabsio"
bind:value={source.organization}
/>
{#if $appSession.isAdmin}
<Setting
customClass="pt-4"
id="autodeploy"
isCenter={false}
disabled={$appSession.teamId !== '0'}
bind:setting={source.isSystemWide}
on:click={() => changeSettings('isSystemWide', true)}
title="System Wide Git Source"
description="System Wide Git Sources are available to all the users in your Coolify instance. <br><br> <span class='font-bold text-warning'>Use with caution, as it can be a security risk.</span>"
/>
{/if}
</div>
</form>
{:else}
<div class="text-center">
<a
href={`${source.htmlUrl}/${
source.htmlUrl === 'https://github.com' ? 'apps' : 'github-apps'
}/${source.githubApp.name}/installations/new`}
>
<button class="box-selection bg-sources text-xl font-bold">Install Repositories</button></a
>
</div>
{/if}
</div>

View File

@@ -0,0 +1,333 @@
<script lang="ts">
export let source: any;
export let settings: any;
import { page } from '$app/stores';
import { onMount } from 'svelte';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification, getAPIUrl } from '$lib/common';
import { addToast, appSession, trpc } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte';
import { dev } from '$app/environment';
const { id } = $page.params;
let url = settings.fqdn ? settings.fqdn : window.location.origin;
if (dev) {
url = getAPIUrl();
}
let loading = false;
let oauthIdEl: HTMLInputElement;
let applicationType: string;
if (!source.gitlabAppId) {
source.gitlabApp = {
oauthId: null,
groupName: null,
appId: null,
appSecret: null
};
}
$: selfHosted = source.htmlUrl !== 'https://gitlab.com';
onMount(() => {
oauthIdEl && oauthIdEl.focus();
});
async function handleSubmit() {
if (loading) return;
loading = true;
if (!source.gitlabAppId) {
// New GitLab App
try {
const { id } = await trpc.sources.newGitLabApp.mutate({
id: source.id,
type: 'gitlab',
name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, ''),
oauthId: source.gitlabApp.oauthId,
appId: source.gitlabApp.appId,
appSecret: source.gitlabApp.appSecret,
groupName: source.gitlabApp.groupName,
customPort: source.customPort,
customUser: source.customUser
});
const from = $page.url.searchParams.get('from');
if (from) {
return window.location.assign(from);
}
return window.location.assign(`/sources/${id}`);
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
} else {
// Update GitLab App
try {
await trpc.sources.save.mutate({
id,
name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, ''),
customPort: source.customPort,
customUser: source.customUser
});
return addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
}
async function changeSettings() {
const {
htmlUrl,
gitlabApp: { oauthId }
} = source;
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 1000 / 2;
const newWindow = open(
`${htmlUrl}/oauth/applications/${oauthId}`,
'GitLab',
'resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow?.closed) {
clearInterval(timer);
}
}, 100);
}
async function checkOauthId() {
if (source.gitlabApp?.oauthId) {
try {
// await post(`/sources/${id}/check`, {
// oauthId: source.gitlabApp?.oauthId
// });
} catch (error) {
source.gitlabApp.oauthId = null;
oauthIdEl.focus();
return errorNotification(error);
}
}
}
function newApp() {
switch (applicationType) {
case 'user':
window.open(`${source.htmlUrl}/-/profile/applications`);
break;
case 'group':
if (!source.gitlabApp.groupName) {
return addToast({
message: 'Please enter a group name first.',
type: 'error'
});
}
window.open(
`${source.htmlUrl}/groups/${source.gitlabApp.groupName}/-/settings/applications`
);
break;
case 'instance':
break;
default:
break;
}
}
</script>
<div class="mx-auto max-w-6xl px-6">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex lg:flex-row lg:justify-between flex-col space-y-3 w-full lg:items-center">
<h1 class="title">General</h1>
<div class="flex flex-col lg:flex-row lg:space-x-4 lg:w-fit space-y-2 lg:space-y-0 w-full">
{#if $appSession.isAdmin}
<button type="submit" class="btn btn-sm bg-sources" disabled={loading}
>{loading ? 'Saving...' : 'Save'}</button
>
{#if source.gitlabAppId}
<button class="btn btn-sm" on:click|preventDefault={changeSettings}
>Change GitLab App Settings</button
>
{:else}
<button class="btn btn-sm" on:click|preventDefault|stopPropagation={newApp}
>Create new GitLab App manually</button
>
{/if}
{/if}
</div>
</div>
<div class="grid grid-flow-row gap-2 lg:px-10">
{#if !source.gitlabAppId}
<a
href="https://docs.coollabs.io/coolify/sources#how-to-integrate-with-gitlab"
class="font-bold "
target="_blank noreferrer"
rel="noopener noreferrer">Documentation and detailed instructions.</a
>
<div class="grid grid-cols-2 items-center">
<label for="type" class="text-base font-bold text-stone-100">Application Type</label>
<select name="type" id="type" class="lg:w-96 w-full" bind:value={applicationType}>
<option value="user">User owned application</option>
<option value="group">Group owned application</option>
{#if source.htmlUrl !== 'https://gitlab.com'}
<option value="instance">Instance-wide application (self-hosted)</option>
{/if}
</select>
</div>
{#if applicationType === 'group'}
<div class="grid grid-cols-2 items-center">
<label for="groupName" class="text-base font-bold text-stone-100">Group Name</label>
<input
class="w-full"
name="groupName"
id="groupName"
required
bind:value={source.gitlabApp.groupName}
/>
</div>
{/if}
{/if}
<div class="grid grid-flow-row gap-2">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input class="w-full" name="name" id="name" required bind:value={source.name} />
</div>
</div>
{#if source.gitlabApp.groupName}
<div class="grid grid-cols-2 items-center">
<label for="groupName" class="text-base font-bold text-stone-100">Group Name</label>
<input
class="w-full"
name="groupName"
id="groupName"
disabled={source.gitlabAppId}
readonly={source.gitlabAppId}
required
bind:value={source.gitlabApp.groupName}
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center">
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<input
class="w-full"
name="htmlUrl"
id="htmlUrl"
required
disabled={source.gitlabAppId}
readonly={source.gitlabAppId}
bind:value={source.htmlUrl}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input
class="w-full"
name="apiUrl"
id="apiUrl"
disabled={source.gitlabAppId}
readonly={source.gitlabAppId}
required
bind:value={source.apiUrl}
/>
</div>
{#if selfHosted}
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100"
>Custom SSH User <Explainer
explanation={'If you use a self-hosted version of Git, you can provide a custom SSH user for all the Git related actions.'}
/></label
>
<input
class="w-full"
name="customUser"
id="customUser"
disabled={!selfHosted}
readonly={!selfHosted}
required
bind:value={source.customUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100"
>Custom SSH Port <Explainer
explanation={'If you use a self-hosted version of Git, you can provide custom port for all the Git related actions.'}
/></label
>
<input
class="w-full"
name="customPort"
id="customPort"
disabled={!selfHosted}
readonly={!selfHosted}
required
bind:value={source.customPort}
/>
</div>
{/if}
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<label for="oauthId" class="pt-2 text-base font-bold text-stone-100"
>OAuth ID
{#if !source.gitlabAppId}
<Explainer
explanation="The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class=' text-settings' >in the URL</span> of your GitLab OAuth Application."
/>
{/if}</label
>
</div>
<input
class="w-full"
disabled={source.gitlabAppId}
readonly={source.gitlabAppId}
on:change={checkOauthId}
bind:this={oauthIdEl}
name="oauthId"
id="oauthId"
type="number"
required
bind:value={source.gitlabApp.oauthId}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="appId" class="text-base font-bold text-stone-100">Application ID</label>
<input
class="w-full"
name="appId"
id="appId"
disabled={source.gitlabAppId}
readonly={source.gitlabAppId}
required
bind:value={source.gitlabApp.appId}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="appSecret" class="text-base font-bold text-stone-100">Secret</label>
<CopyPasswordField
disabled={source.gitlabAppId}
readonly={source.gitlabAppId}
isPasswordField={true}
name="appSecret"
id="appSecret"
required
bind:value={source.gitlabApp.appSecret}
/>
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,62 @@
<script lang="ts">
export let source: any;
export let settings: any;
import Github from './Github.svelte';
import Gitlab from './Gitlab.svelte';
function setPredefined(type: string) {
switch (type) {
case 'github':
source.name = 'Github.com';
source.type = 'github';
source.htmlUrl = 'https://github.com';
source.apiUrl = 'https://api.github.com';
source.organization = undefined;
break;
case 'gitlab':
source.name = 'Gitlab.com';
source.type = 'gitlab';
source.htmlUrl = 'https://gitlab.com';
source.apiUrl = 'https://gitlab.com/api';
source.organization = undefined;
break;
case 'bitbucket':
source.name = 'Bitbucket.com';
source.type = 'bitbucket';
source.htmlUrl = 'https://bitbucket.com';
source.apiUrl = 'https://api.bitbucket.org';
source.organization = undefined;
break;
default:
break;
}
}
</script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-1 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
New Git Source
</div>
</div>
</div>
<div class="flex flex-col justify-center">
<div class="flex-col space-y-2 pb-10 text-center">
<div class="text-xl font-bold text-white">Select a git type</div>
<div class="flex justify-center space-x-2">
<button class="btn btn-sm" on:click={() => setPredefined('github')}>GitHub</button>
<button class="btn btn-sm" on:click={() => setPredefined('gitlab')}>GitLab</button>
</div>
</div>
{#if source?.type}
<div>
{#if source.type === 'github'}
<Github bind:source {settings} />
{:else if source.type === 'gitlab'}
<Gitlab bind:source {settings} />
{/if}
</div>
{/if}
</div>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import Github from './Github.svelte';
import Gitlab from './Gitlab.svelte';
export let source: any;
export let settings: any;
</script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration
</div>
<span class="text-xs">{source.name}</span>
</div>
{#if source?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="w-8">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if source?.type === 'github'}
<svg viewBox="0 0 128 128" class="w-8">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</div>
<div>
{#if source.type === 'github'}
<Github bind:source {settings} />
{:else if source.type === 'gitlab'}
<Gitlab bind:source {settings} />
{/if}
</div>

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
<rect width="200" height="200" rx="30" fill="#4422dd" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M148.883 112.106C148.124 111.916 147.491 111.726 146.921 111.473C146.5 111.286 146.148 111.064 145.839 110.807C145.635 110.637 145.547 110.369 145.572 110.104C145.877 106.923 145.541 104.118 145.845 100.964C147.111 88.1771 155.15 92.2285 162.367 90.1395C166.511 88.9722 170.655 86.6747 172.169 82.1555C172.417 81.4157 172.199 80.6114 171.684 80.0255C166.956 74.6505 161.723 69.9012 156.037 65.831C136.973 52.2582 112.197 46.6221 89.4444 49.9189C88.5825 50.0438 88.1307 50.9987 88.6016 51.7314C91.4833 56.2156 95.2849 59.8853 99.6447 62.5864C100.437 63.0773 100.119 64.1376 99.2092 63.9337C97.0711 63.4543 94.3238 62.5194 91.7401 60.6961C91.4911 60.5204 91.1704 60.4762 90.8873 60.5891C89.7323 61.0497 88.0643 61.7139 86.6859 62.3089C85.8925 62.6514 85.731 63.6747 86.3841 64.2407C97.8122 74.1467 114.437 75.6526 127.455 67.7254C128.248 67.2425 129.518 68.2348 129.262 69.1275C128.853 70.5574 128.375 72.523 127.867 75.1999C124.638 91.5322 115.332 90.2661 103.811 86.1514C80.7898 77.8076 67.7294 84.9328 56.1177 70.8281C55.3109 69.8481 53.8925 69.5083 52.9328 70.3392C50.5358 72.4145 49.1172 75.4437 49.1172 78.6816C49.1172 82.5166 51.0968 85.8018 54.0311 87.7682C54.3981 88.0142 54.8858 87.9102 55.1598 87.5636C55.8748 86.6586 56.4597 86.0587 57.1881 85.6794C57.9852 85.2641 58.374 86.4045 57.701 87.0003C55.2349 89.1839 54.527 91.7851 52.9154 96.913C50.3832 104.952 51.4594 113.182 39.6217 115.334C33.3546 115.651 33.4812 119.892 31.2023 126.222C28.5575 133.863 25.0942 137.247 18.6844 143.924C17.8078 144.837 17.7326 146.297 18.696 147.118C21.2564 149.301 23.8969 149.421 26.5812 148.315C33.228 145.53 38.3556 136.921 43.1666 131.35C48.5474 125.146 61.4613 127.805 71.21 121.728C76.4677 118.504 79.6266 114.386 78.6164 108.217C78.4537 107.224 79.5906 106.626 80.0029 107.545C80.7856 109.289 81.2988 111.149 81.5121 113.071C81.5681 113.575 82.0184 113.947 82.5245 113.919C93.0718 113.326 106.711 124.959 119.458 128.107C120.233 128.299 120.784 127.403 120.346 126.736C119.539 125.507 118.854 124.233 118.308 122.931C117.744 121.578 117.317 120.265 117.015 118.998C116.778 118.007 118.226 117.741 118.721 118.632C121.99 124.524 128.523 130.057 137.615 130.717C140.717 130.97 144.136 130.59 147.681 129.514C151.922 128.248 155.847 126.602 160.531 127.488C164.013 128.122 167.241 129.894 169.267 132.869C172.11 137.015 178.11 138.113 181.308 133.781C181.743 133.191 181.78 132.403 181.492 131.729C174.45 115.223 156.569 114.089 148.883 112.106Z" fill="white" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

1013
apps/server/devTags.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

7
apps/server/nodemon.json Normal file
View File

@@ -0,0 +1,7 @@
{
"watch": ["src"],
"ignore": ["src/**/*.test.ts"],
"ext": "ts,mjs,json,graphql",
"exec": "rimraf build && esbuild `find src \\( -name '*.ts' -o -name '*.js' \\)` --platform=node --outdir=build --format=cjs && node build",
"legacyWatch": true
}

View File

@@ -4,7 +4,8 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"build": "rimraf ../../build && tsc --outDir ../../build", "build": "rimraf ../../build && tsc --outDir ../../build",
"dev": "tsx watch --clear-screen=false src", "dev-old": "tsx watch --clear-screen=false src",
"dev": "nodemon",
"lint": "prettier --plugin-search-dir . --check . && eslint .", "lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .", "format": "prettier --plugin-search-dir . --write .",
"test-dev": "start-server-and-test 'tsx src/server' http-get://localhost:2022 'tsx src/client'", "test-dev": "start-server-and-test 'tsx src/server' http-get://localhost:2022 'tsx src/client'",
@@ -23,6 +24,7 @@
"@fastify/jwt": "6.5.0", "@fastify/jwt": "6.5.0",
"@fastify/static": "6.6.0", "@fastify/static": "6.6.0",
"@fastify/websocket": "7.1.1", "@fastify/websocket": "7.1.1",
"@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2", "@ladjs/graceful": "3.0.2",
"@prisma/client": "4.6.1", "@prisma/client": "4.6.1",
"@trpc/client": "10.1.0", "@trpc/client": "10.1.0",
@@ -43,7 +45,7 @@
"is-ip": "5.0.0", "is-ip": "5.0.0",
"is-port-reachable": "4.0.0", "is-port-reachable": "4.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "9.0.0",
"node-fetch": "3.3.0", "node-fetch": "3.3.0",
"p-all": "4.0.0", "p-all": "4.0.0",
"p-throttle": "5.0.0", "p-throttle": "5.0.0",
@@ -65,6 +67,8 @@
"@types/node-fetch": "2.6.2", "@types/node-fetch": "2.6.2",
"@types/shell-quote": "^1.7.1", "@types/shell-quote": "^1.7.1",
"@types/ws": "8.5.3", "@types/ws": "8.5.3",
"esbuild": "0.15.15",
"nodemon": "2.0.20",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"start-server-and-test": "1.14.0", "start-server-and-test": "1.14.0",

View File

@@ -0,0 +1,24 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
"isHttp2" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -186,6 +186,7 @@ model ApplicationSettings {
isPublicRepository Boolean @default(false) isPublicRepository Boolean @default(false)
isDBBranching Boolean @default(false) isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false) isCustomSSL Boolean @default(false)
isHttp2 Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
@@ -325,6 +326,7 @@ model GitSource {
apiUrl String? apiUrl String?
htmlUrl String? htmlUrl String?
customPort Int @default(22) customPort Int @default(22)
customUser String @default("git")
organization String? organization String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@@ -14,17 +14,16 @@ import {
import { import {
createDirectories, createDirectories,
decrypt, decrypt,
defaultComposeConfiguration,
getDomain, getDomain,
prisma, generateSecrets,
decryptApplication, decryptApplication,
isDev, pushToRegistry
pushToRegistry,
executeCommand,
generateSecrets
} from '../lib/common'; } from '../lib/common';
import * as importers from '../lib/importers'; import * as importers from '../lib/importers';
import * as buildpacks from '../lib/buildPacks'; import * as buildpacks from '../lib/buildPacks';
import { prisma } from '../prisma';
import { executeCommand } from '../lib/executeCommand';
import { defaultComposeConfiguration } from '../lib/docker';
(async () => { (async () => {
if (parentPort) { if (parentPort) {
@@ -196,7 +195,7 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({ await executeCommand({
debug: true, debug: true,
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d` command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml up -d`
}); });
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
} catch (error) { } catch (error) {
@@ -532,7 +531,48 @@ import * as buildpacks from '../lib/buildPacks';
}); });
if (forceRebuild) deployNeeded = true; if (forceRebuild) deployNeeded = true;
if ((!imageFoundLocally && !imageFoundRemotely) || deployNeeded) { if ((!imageFoundLocally && !imageFoundRemotely) || deployNeeded) {
if (buildpacks[buildPack]) if (buildPack === 'static') {
await buildpacks.staticApp({
dockerId: destinationDocker.id,
network: destinationDocker.network,
buildId,
applicationId,
domain,
name,
type,
volumes,
labels,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
publishDirectory,
debug,
commit,
tag,
workdir,
port: exposePort ? `${exposePort}:${port}` : port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
phpModules,
pythonWSGI,
pythonModule,
pythonVariable,
dockerFileLocation,
dockerComposeConfiguration,
dockerComposeFileLocation,
denoMainFile,
denoOptions,
baseImage,
baseBuildImage,
deploymentType,
forceRebuild
});
} else if (buildpacks[buildPack])
await buildpacks[buildPack]({ await buildpacks[buildPack]({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
network: destinationDocker.network, network: destinationDocker.network,
@@ -600,6 +640,7 @@ import * as buildpacks from '../lib/buildPacks';
} }
if (buildPack === 'compose') { if (buildPack === 'compose') {
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`;
try { try {
const { stdout: containers } = await executeCommand({ const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId, dockerId: destinationDockerId,
@@ -629,7 +670,7 @@ import * as buildpacks from '../lib/buildPacks';
buildId, buildId,
applicationId, applicationId,
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d` command: `docker compose --project-directory ${workdir} -f ${fileYaml} up -d`
}); });
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
await prisma.build.update({ await prisma.build.update({
@@ -724,7 +765,7 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({ await executeCommand({
debug, debug,
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d` command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml up -d`
}); });
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
} catch (error) { } catch (error) {
@@ -803,5 +844,8 @@ import * as buildpacks from '../lib/buildPacks';
while (true) { while (true) {
await th(); await th();
} }
} else process.exit(0); } else {
console.log('hello');
process.exit(0);
}
})(); })();

View File

@@ -1,9 +0,0 @@
import { parentPort } from 'node:worker_threads';
import process from 'node:process';
console.log('Hello TypeScript!');
// signal to parent that the job is done
if (parentPort) parentPort.postMessage('done');
// eslint-disable-next-line unicorn/no-process-exit
else process.exit(0);

View File

@@ -0,0 +1,843 @@
import {
base64Encode,
decrypt,
encrypt,
generateSecrets,
generateTimestamp,
getDomain,
isARM,
isDev,
version
} from '../common';
import { promises as fs } from 'fs';
import { day } from '../dayjs';
import { prisma } from '../../prisma';
import { executeCommand } from '../executeCommand';
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
const nodeBased = [
'react',
'preact',
'vuejs',
'svelte',
'gatsby',
'astro',
'eleventy',
'node',
'nestjs',
'nuxtjs',
'nextjs'
];
export function setDefaultBaseImage(
buildPack: string | null,
deploymentType: string | null = null
) {
const nodeVersions = [
{
value: 'node:lts',
label: 'node:lts'
},
{
value: 'node:18',
label: 'node:18'
},
{
value: 'node:17',
label: 'node:17'
},
{
value: 'node:16',
label: 'node:16'
},
{
value: 'node:14',
label: 'node:14'
},
{
value: 'node:12',
label: 'node:12'
}
];
const staticVersions = [
{
value: 'webdevops/nginx:alpine',
label: 'webdevops/nginx:alpine'
},
{
value: 'webdevops/apache:alpine',
label: 'webdevops/apache:alpine'
},
{
value: 'nginx:alpine',
label: 'nginx:alpine'
},
{
value: 'httpd:alpine',
label: 'httpd:alpine (Apache)'
}
];
const rustVersions = [
{
value: 'rust:latest',
label: 'rust:latest'
},
{
value: 'rust:1.60',
label: 'rust:1.60'
},
{
value: 'rust:1.60-buster',
label: 'rust:1.60-buster'
},
{
value: 'rust:1.60-bullseye',
label: 'rust:1.60-bullseye'
},
{
value: 'rust:1.60-slim-buster',
label: 'rust:1.60-slim-buster'
},
{
value: 'rust:1.60-slim-bullseye',
label: 'rust:1.60-slim-bullseye'
},
{
value: 'rust:1.60-alpine3.14',
label: 'rust:1.60-alpine3.14'
},
{
value: 'rust:1.60-alpine3.15',
label: 'rust:1.60-alpine3.15'
}
];
const phpVersions = [
{
value: 'webdevops/php-apache:8.2',
label: 'webdevops/php-apache:8.2'
},
{
value: 'webdevops/php-nginx:8.2',
label: 'webdevops/php-nginx:8.2'
},
{
value: 'webdevops/php-apache:8.1',
label: 'webdevops/php-apache:8.1'
},
{
value: 'webdevops/php-nginx:8.1',
label: 'webdevops/php-nginx:8.1'
},
{
value: 'webdevops/php-apache:8.0',
label: 'webdevops/php-apache:8.0'
},
{
value: 'webdevops/php-nginx:8.0',
label: 'webdevops/php-nginx:8.0'
},
{
value: 'webdevops/php-apache:7.4',
label: 'webdevops/php-apache:7.4'
},
{
value: 'webdevops/php-nginx:7.4',
label: 'webdevops/php-nginx:7.4'
},
{
value: 'webdevops/php-apache:7.3',
label: 'webdevops/php-apache:7.3'
},
{
value: 'webdevops/php-nginx:7.3',
label: 'webdevops/php-nginx:7.3'
},
{
value: 'webdevops/php-apache:7.2',
label: 'webdevops/php-apache:7.2'
},
{
value: 'webdevops/php-nginx:7.2',
label: 'webdevops/php-nginx:7.2'
},
{
value: 'webdevops/php-apache:7.1',
label: 'webdevops/php-apache:7.1'
},
{
value: 'webdevops/php-nginx:7.1',
label: 'webdevops/php-nginx:7.1'
},
{
value: 'webdevops/php-apache:7.0',
label: 'webdevops/php-apache:7.0'
},
{
value: 'webdevops/php-nginx:7.0',
label: 'webdevops/php-nginx:7.0'
},
{
value: 'webdevops/php-apache:5.6',
label: 'webdevops/php-apache:5.6'
},
{
value: 'webdevops/php-nginx:5.6',
label: 'webdevops/php-nginx:5.6'
},
{
value: 'webdevops/php-apache:8.2-alpine',
label: 'webdevops/php-apache:8.2-alpine'
},
{
value: 'webdevops/php-nginx:8.2-alpine',
label: 'webdevops/php-nginx:8.2-alpine'
},
{
value: 'webdevops/php-apache:8.1-alpine',
label: 'webdevops/php-apache:8.1-alpine'
},
{
value: 'webdevops/php-nginx:8.1-alpine',
label: 'webdevops/php-nginx:8.1-alpine'
},
{
value: 'webdevops/php-apache:8.0-alpine',
label: 'webdevops/php-apache:8.0-alpine'
},
{
value: 'webdevops/php-nginx:8.0-alpine',
label: 'webdevops/php-nginx:8.0-alpine'
},
{
value: 'webdevops/php-apache:7.4-alpine',
label: 'webdevops/php-apache:7.4-alpine'
},
{
value: 'webdevops/php-nginx:7.4-alpine',
label: 'webdevops/php-nginx:7.4-alpine'
},
{
value: 'webdevops/php-apache:7.3-alpine',
label: 'webdevops/php-apache:7.3-alpine'
},
{
value: 'webdevops/php-nginx:7.3-alpine',
label: 'webdevops/php-nginx:7.3-alpine'
},
{
value: 'webdevops/php-apache:7.2-alpine',
label: 'webdevops/php-apache:7.2-alpine'
},
{
value: 'webdevops/php-nginx:7.2-alpine',
label: 'webdevops/php-nginx:7.2-alpine'
},
{
value: 'webdevops/php-apache:7.1-alpine',
label: 'webdevops/php-apache:7.1-alpine'
},
{
value: 'php:8.1-fpm',
label: 'php:8.1-fpm'
},
{
value: 'php:8.0-fpm',
label: 'php:8.0-fpm'
},
{
value: 'php:8.1-fpm-alpine',
label: 'php:8.1-fpm-alpine'
},
{
value: 'php:8.0-fpm-alpine',
label: 'php:8.0-fpm-alpine'
}
];
const pythonVersions = [
{
value: 'python:3.10-alpine',
label: 'python:3.10-alpine'
},
{
value: 'python:3.10-buster',
label: 'python:3.10-buster'
},
{
value: 'python:3.10-bullseye',
label: 'python:3.10-bullseye'
},
{
value: 'python:3.10-slim-bullseye',
label: 'python:3.10-slim-bullseye'
},
{
value: 'python:3.9-alpine',
label: 'python:3.9-alpine'
},
{
value: 'python:3.9-buster',
label: 'python:3.9-buster'
},
{
value: 'python:3.9-bullseye',
label: 'python:3.9-bullseye'
},
{
value: 'python:3.9-slim-bullseye',
label: 'python:3.9-slim-bullseye'
},
{
value: 'python:3.8-alpine',
label: 'python:3.8-alpine'
},
{
value: 'python:3.8-buster',
label: 'python:3.8-buster'
},
{
value: 'python:3.8-bullseye',
label: 'python:3.8-bullseye'
},
{
value: 'python:3.8-slim-bullseye',
label: 'python:3.8-slim-bullseye'
},
{
value: 'python:3.7-alpine',
label: 'python:3.7-alpine'
},
{
value: 'python:3.7-buster',
label: 'python:3.7-buster'
},
{
value: 'python:3.7-bullseye',
label: 'python:3.7-bullseye'
},
{
value: 'python:3.7-slim-bullseye',
label: 'python:3.7-slim-bullseye'
}
];
const herokuVersions = [
{
value: 'heroku/builder:22',
label: 'heroku/builder:22'
},
{
value: 'heroku/buildpacks:20',
label: 'heroku/buildpacks:20'
},
{
value: 'heroku/builder-classic:22',
label: 'heroku/builder-classic:22'
}
];
let payload: any = {
baseImage: null,
baseBuildImage: null,
baseImages: [],
baseBuildImages: []
};
if (nodeBased.includes(buildPack)) {
if (deploymentType === 'static') {
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine';
payload.baseImages = isARM(process.arch)
? staticVersions.filter((version) => !version.value.includes('webdevops'))
: staticVersions;
payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions;
} else {
payload.baseImage = 'node:lts';
payload.baseImages = nodeVersions;
payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions;
}
}
if (staticApps.includes(buildPack)) {
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine';
payload.baseImages = isARM(process.arch)
? staticVersions.filter((version) => !version.value.includes('webdevops'))
: staticVersions;
payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions;
}
if (buildPack === 'python') {
payload.baseImage = 'python:3.10-alpine';
payload.baseImages = pythonVersions;
}
if (buildPack === 'rust') {
payload.baseImage = 'rust:latest';
payload.baseBuildImage = 'rust:latest';
payload.baseImages = rustVersions;
payload.baseBuildImages = rustVersions;
}
if (buildPack === 'deno') {
payload.baseImage = 'denoland/deno:latest';
}
if (buildPack === 'php') {
payload.baseImage = isARM(process.arch)
? 'php:8.1-fpm-alpine'
: 'webdevops/php-apache:8.2-alpine';
payload.baseImages = isARM(process.arch)
? phpVersions.filter((version) => !version.value.includes('webdevops'))
: phpVersions;
}
if (buildPack === 'laravel') {
payload.baseImage = isARM(process.arch)
? 'php:8.1-fpm-alpine'
: 'webdevops/php-apache:8.2-alpine';
payload.baseImages = isARM(process.arch)
? phpVersions.filter((version) => !version.value.includes('webdevops'))
: phpVersions;
payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions;
}
if (buildPack === 'heroku') {
payload.baseImage = 'heroku/buildpacks:20';
payload.baseImages = herokuVersions;
}
return payload;
}
export const setDefaultConfiguration = async (data: any) => {
let {
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory,
dockerFileLocation,
dockerComposeFileLocation,
denoMainFile
} = data;
//@ts-ignore
const template = scanningTemplates[buildPack];
if (!port) {
port = template?.port || 3000;
if (buildPack === 'static') port = 80;
else if (buildPack === 'node') port = 3000;
else if (buildPack === 'php') port = 80;
else if (buildPack === 'python') port = 8000;
}
if (!installCommand && buildPack !== 'static' && buildPack !== 'laravel')
installCommand = template?.installCommand || 'yarn install';
if (!startCommand && buildPack !== 'static' && buildPack !== 'laravel')
startCommand = template?.startCommand || 'yarn start';
if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (baseDirectory.endsWith('/') && baseDirectory !== '/')
baseDirectory = baseDirectory.slice(0, -1);
}
if (dockerFileLocation) {
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1);
} else {
dockerFileLocation = '/Dockerfile';
}
if (dockerComposeFileLocation) {
if (!dockerComposeFileLocation.startsWith('/'))
dockerComposeFileLocation = `/${dockerComposeFileLocation}`;
if (dockerComposeFileLocation.endsWith('/'))
dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1);
} else {
dockerComposeFileLocation = '/Dockerfile';
}
if (!denoMainFile) {
denoMainFile = 'main.ts';
}
return {
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory,
dockerFileLocation,
dockerComposeFileLocation,
denoMainFile
};
};
export const scanningTemplates = {
'@sveltejs/kit': {
buildPack: 'nodejs'
},
astro: {
buildPack: 'astro'
},
'@11ty/eleventy': {
buildPack: 'eleventy'
},
svelte: {
buildPack: 'svelte'
},
'@nestjs/core': {
buildPack: 'nestjs'
},
next: {
buildPack: 'nextjs'
},
nuxt: {
buildPack: 'nuxtjs'
},
'react-scripts': {
buildPack: 'react'
},
'parcel-bundler': {
buildPack: 'static'
},
'@vue/cli-service': {
buildPack: 'vuejs'
},
vuejs: {
buildPack: 'vuejs'
},
gatsby: {
buildPack: 'gatsby'
},
'preact-cli': {
buildPack: 'react'
}
};
export const saveBuildLog = async ({
line,
buildId,
applicationId
}: {
line: string;
buildId: string;
applicationId: string;
}): Promise<any> => {
if (buildId === 'undefined' || buildId === 'null' || !buildId) return;
if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return;
const { default: got } = await import('got');
if (typeof line === 'object' && line) {
if (line.shortMessage) {
line = line.shortMessage + '\n' + line.stderr;
} else {
line = JSON.stringify(line);
}
}
if (line && typeof line === 'string' && line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `[${generateTimestamp()}] ${line}`;
const fluentBitUrl = isDev
? process.env.COOLIFY_CONTAINER_DEV === 'true'
? 'http://coolify-fluentbit:24224'
: 'http://localhost:24224'
: 'http://coolify-fluentbit:24224';
if (isDev && !process.env.COOLIFY_CONTAINER_DEV) {
console.debug(`[${applicationId}] ${addTimestamp}`);
}
try {
return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, {
json: {
line: encrypt(line)
}
});
} catch (error) {
return await prisma.buildLog.create({
data: {
line: addTimestamp,
buildId,
time: Number(day().valueOf()),
applicationId
}
});
}
};
export async function copyBaseConfigurationFiles(
buildPack,
workdir,
buildId,
applicationId,
baseImage
) {
try {
if (buildPack === 'php') {
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
await saveBuildLog({
line: 'Copied default configuration file for PHP.',
buildId,
applicationId
});
} else if (baseImage?.includes('nginx')) {
await fs.writeFile(
`${workdir}/nginx.conf`,
`user nginx;
worker_processes auto;
error_log /docker.stdout;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /docker.stdout main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name localhost;
location / {
root /app;
index index.html;
try_files $uri $uri/index.html $uri/ /index.html =404;
}
error_page 404 /50x.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /app;
}
}
}
`
);
}
// TODO: Add more configuration files for other buildpacks, like apache2, etc.
} catch (error) {
throw new Error(error);
}
}
export function checkPnpm(installCommand = null, buildCommand = null, startCommand = null) {
return (
installCommand?.includes('pnpm') ||
buildCommand?.includes('pnpm') ||
startCommand?.includes('pnpm')
);
}
export async function saveDockerRegistryCredentials({ url, username, password, workdir }) {
if (!username || !password) {
return null;
}
let decryptedPassword = decrypt(password);
const location = `${workdir}/.docker`;
try {
await fs.mkdir(`${workdir}/.docker`);
} catch (error) {
// console.log(error);
}
const payload = JSON.stringify({
auths: {
[url]: {
auth: Buffer.from(`${username}:${decryptedPassword}`).toString('base64')
}
}
});
await fs.writeFile(`${location}/config.json`, payload);
return location;
}
export async function buildImage({
applicationId,
tag,
workdir,
buildId,
dockerId,
isCache = false,
debug = false,
dockerFileLocation = '/Dockerfile',
commit,
forceRebuild = false
}) {
if (isCache) {
await saveBuildLog({ line: `Building cache image...`, buildId, applicationId });
} else {
await saveBuildLog({ line: `Building production image...`, buildId, applicationId });
}
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`;
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`;
let location = null;
const { dockerRegistry } = await prisma.application.findUnique({
where: { id: applicationId },
select: { dockerRegistry: true }
});
if (dockerRegistry) {
const { url, username, password } = dockerRegistry;
location = await saveDockerRegistryCredentials({ url, username, password, workdir });
}
await executeCommand({
stream: true,
debug,
buildId,
applicationId,
dockerId,
command: `docker ${location ? `--config ${location}` : ''} build ${
forceRebuild ? '--no-cache' : ''
} --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}`
});
const { status } = await prisma.build.findUnique({ where: { id: buildId } });
if (status === 'canceled') {
throw new Error('Canceled.');
}
}
export function makeLabelForSimpleDockerfile({ applicationId, port, type }) {
return [
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.applicationId=${applicationId}`,
`coolify.type=standalone-application`
];
}
export function makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId = null,
buildPack,
repository,
branch,
projectId,
port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
}) {
if (pullmergeRequestId) {
const protocol = fqdn.startsWith('https://') ? 'https' : 'http';
const domain = getDomain(fqdn);
fqdn = `${protocol}://${pullmergeRequestId}.${domain}`;
}
return [
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.applicationId=${applicationId}`,
`coolify.type=standalone-application`,
`coolify.name=${name}`,
`coolify.configuration=${base64Encode(
JSON.stringify({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
})
)}`
];
}
export async function buildCacheImageWithNode(data, imageForBuild) {
const {
workdir,
buildId,
baseDirectory,
installCommand,
buildCommand,
secrets,
pullmergeRequestId
} = data;
const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
Dockerfile.push(env);
});
}
if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
if (installCommand) {
Dockerfile.push(`RUN ${installCommand}`);
}
Dockerfile.push(`RUN ${buildCommand}`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true });
}
export async function buildCacheImageForLaravel(data, imageForBuild) {
const { workdir, buildId, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
Dockerfile.push(env);
});
}
Dockerfile.push(`COPY *.json *.mix.js /app/`);
Dockerfile.push(`COPY resources /app/resources`);
Dockerfile.push(`RUN yarn install && yarn production`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true });
}
export async function buildCacheImageWithCargo(data, imageForBuild) {
const { applicationId, workdir, buildId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push('COPY . .');
Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json');
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true });
}

View File

@@ -0,0 +1,137 @@
import { promises as fs } from 'fs';
import { saveBuildLog } from './common';
import yaml from 'js-yaml';
import { generateSecrets } from '../common';
import { defaultComposeConfiguration } from '../docker';
import { executeCommand } from '../executeCommand';
export default async function (data) {
let {
applicationId,
debug,
buildId,
dockerId,
network,
volumes,
labels,
workdir,
baseDirectory,
secrets,
pullmergeRequestId,
dockerComposeConfiguration,
dockerComposeFileLocation
} = data;
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`;
const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8');
const dockerComposeYaml = yaml.load(dockerComposeRaw);
if (!dockerComposeYaml.services) {
throw 'No Services found in docker-compose file.';
}
let envs = [];
let buildEnvs = [];
if (secrets.length > 0) {
envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)];
buildEnvs = [...buildEnvs, ...generateSecrets(secrets, pullmergeRequestId, true, null, true)];
}
const composeVolumes = [];
if (volumes.length > 0) {
for (const volume of volumes) {
let [v, path] = volume.split(':');
composeVolumes[v] = {
name: v
};
}
}
let networks = {};
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
value['container_name'] = `${applicationId}-${key}`;
let environment = typeof value['environment'] === 'undefined' ? [] : value['environment'];
if (Object.keys(environment).length > 0) {
environment = Object.entries(environment).map(([key, value]) => `${key}=${value}`);
}
value['environment'] = [...environment, ...envs];
let build = typeof value['build'] === 'undefined' ? [] : value['build'];
if (typeof build === 'string') {
build = { context: build };
}
const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args'];
let finalArgs = [...buildEnvs];
if (Object.keys(buildArgs).length > 0) {
for (const arg of buildArgs) {
const [key, _] = arg.split('=');
if (finalArgs.filter((env) => env.startsWith(key)).length === 0) {
finalArgs.push(arg);
}
}
}
value['build'] = {
...build,
args: finalArgs
};
value['labels'] = labels;
// TODO: If we support separated volume for each service, we need to add it here
if (value['volumes']?.length > 0) {
value['volumes'] = value['volumes'].map((volume) => {
let [v, path, permission] = volume.split(':');
if (!path) {
path = v;
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
} else {
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
}
composeVolumes[v] = {
name: v
};
return `${v}:${path}${permission ? ':' + permission : ''}`;
});
}
if (volumes.length > 0) {
for (const volume of volumes) {
value['volumes'].push(volume);
}
}
if (dockerComposeConfiguration[key].port) {
value['expose'] = [dockerComposeConfiguration[key].port];
}
if (value['networks']?.length > 0) {
value['networks'].forEach((network) => {
networks[network] = {
name: network
};
});
}
value['networks'] = [...(value['networks'] || ''), network];
dockerComposeYaml.services[key] = {
...dockerComposeYaml.services[key],
restart: defaultComposeConfiguration(network).restart,
deploy: defaultComposeConfiguration(network).deploy
};
}
if (Object.keys(composeVolumes).length > 0) {
dockerComposeYaml['volumes'] = { ...composeVolumes };
}
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } });
await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml));
await executeCommand({
debug,
buildId,
applicationId,
dockerId,
command: `docker compose --project-directory ${workdir} -f ${fileYaml} pull`
});
await saveBuildLog({ line: 'Pulling images from Compose file...', buildId, applicationId });
await executeCommand({
debug,
buildId,
applicationId,
dockerId,
command: `docker compose --project-directory ${workdir} -f ${fileYaml} build --progress plain`
});
await saveBuildLog({ line: 'Building images from Compose file...', buildId, applicationId });
}

View File

@@ -0,0 +1,52 @@
import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {
workdir,
port,
baseDirectory,
secrets,
pullmergeRequestId,
denoMainFile,
denoOptions,
buildId
} = data;
const Dockerfile: Array<string> = [];
let depsFound = false;
try {
await fs.readFile(`${workdir}${baseDirectory || ''}/deps.ts`);
depsFound = true;
} catch (error) {}
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
Dockerfile.push(env);
});
}
if (depsFound) {
Dockerfile.push(`COPY .${baseDirectory || ''}/deps.ts /app`);
Dockerfile.push(`RUN deno cache deps.ts`);
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN deno cache ${denoMainFile}`);
Dockerfile.push(`ENV NO_COLOR true`);
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { baseImage, baseBuildImage } = data;
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,27 @@
import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildImage } from './common';
export default async function (data) {
let { workdir, buildId, baseDirectory, secrets, pullmergeRequestId, dockerFileLocation } = data;
const file = `${workdir}${baseDirectory}${dockerFileLocation}`;
data.workdir = `${workdir}${baseDirectory}`;
const DockerfileRaw = await fs.readFile(`${file}`, 'utf8');
const Dockerfile: Array<string> = DockerfileRaw.toString().trim().split('\n');
Dockerfile.forEach((line, index) => {
if (line.startsWith('FROM')) {
Dockerfile.splice(index + 1, 0, `LABEL coolify.buildId=${buildId}`);
}
});
if (secrets.length > 0) {
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
Dockerfile.forEach((line, index) => {
if (line.startsWith('FROM')) {
Dockerfile.splice(index + 1, 0, env);
}
});
});
}
await fs.writeFile(`${data.workdir}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage(data);
}

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