Merge pull request #853 from coollabsio/next

v3.12.11
This commit is contained in:
Andras Bacsai 2023-01-16 12:44:04 +01:00 committed by GitHub
commit bfd3020031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
131 changed files with 18689 additions and 2366 deletions

4
apps/api/.gitignore vendored
View File

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

View File

@ -1,11 +1,9 @@
[
{ "name": "directus-postgresql", "image": "directus/directus", "tags": ["9.22"] },
{ "name": "whoogle", "image": "benbusby/whoogle-search", "tags": ["0.8.1"] },
{ "name": "libretranslate", "image": "libretranslate/libretranslate", "tags": ["v1.3.8"] },
{
"name": "appsmith",
"image": "appsmith/appsmith-ce",
"tags": [
"v1.9.3",
"v1.9.1",
"v1.8.15",
"v1.8.12",
@ -34,8 +32,7 @@
"v1.6.5",
"v1.6.3",
"v1.6.1",
"v1.5.30",
"v1.5.28"
"v1.5.30"
]
},
{
@ -74,6 +71,42 @@
"0.3.1"
]
},
{
"name": "directus-postgresql",
"image": "directus/directus",
"tags": [
"9.22.3",
"9.22.0",
"9.21.0",
"9.20.4",
"9.20.2",
"9.20.0",
"9.19.2",
"9.18.0",
"9.17.4",
"9.17.2",
"9.17.0",
"9.16.0",
"9.15.0",
"9.14.5",
"9.14.3",
"9.14.0",
"9.13.0",
"9.12.2",
"9.12.0",
"9.11.0",
"9.10.0",
"9.9.0",
"9.8.0",
"9.7.0",
"9.6.0",
"9.5.2",
"9.5.0",
"9.4.2",
"9.4.0",
"9.3.0"
]
},
{
"name": "fider",
"image": "getfider/fider",
@ -114,6 +147,9 @@
"name": "ghost-mariadb",
"image": "bitnami/ghost",
"tags": [
"5.30.1",
"5.30.0",
"5.29.0",
"5.28.0",
"5.27.0",
"5.26.4",
@ -141,9 +177,6 @@
"5.22.4",
"5.22.3",
"5.22.2",
"5.22.1",
"5.22.0",
"5.21.0",
"4.48.8"
]
},
@ -151,6 +184,8 @@
"name": "ghost-mysql",
"image": "library/ghost",
"tags": [
"5.30.0",
"5.29.0",
"5.28.0",
"5.27.0",
"5.26.4",
@ -178,15 +213,15 @@
"5.17.2",
"5.17.1",
"5.17.0",
"5.16.2",
"5.14.2",
"5.14.1"
"5.16.2"
]
},
{
"name": "ghost-only",
"image": "library/ghost",
"tags": [
"5.30.0",
"5.29.0",
"5.28.0",
"5.27.0",
"5.26.4",
@ -214,9 +249,7 @@
"5.17.2",
"5.17.1",
"5.17.0",
"5.16.2",
"5.14.2",
"5.14.1"
"5.16.2"
]
},
{
@ -373,6 +406,7 @@
"7.0.0",
"6.0.1",
"6.0.0",
"20.0.3",
"20.0.2",
"20.0.1",
"20.0.0",
@ -404,38 +438,12 @@
{
"name": "lavalink",
"image": "fredboat/lavalink",
"tags": [
"v3.7",
"v3.6",
"v3-vda0b3a4b3916a7b1a2b79702de1143c3a6939810-SNAPSHOT",
"v3-vc92690c425390bd20f6c51643c67ba79ab85b7e0-SNAPSHOT",
"v3-vab81dcd46adf3e8a961dd57eacd2a1bde1233e6c-SNAPSHOT",
"v3-v9c9432704d6a4badfcbd06a57597c54bed8f4326-SNAPSHOT",
"v3-v3.0",
"v3-v3",
"v3-v124f8fae7dab299f9cdf1cb4c1715be455497286-SNAPSHOT",
"v3-",
"v3",
"v2.0.1",
"v2.0",
"v2",
"update-udpqueue-vb4a439d6147dbd8641ea4f265e8efc9f1e16e2d3-SNAPSHOT",
"update-udpqueue-",
"update-udpqueue",
"revert-713-fix-error-for-loading-jda-nas",
"refactor-github-actions",
"patch-update-lp",
"patch-update-github-actions",
"patch-more-configurable-github-actions",
"patch-lavaplayer-update",
"patch-lavaplayer-bump",
"patch-build-number",
"next-api-vd4db194cac7a839a3899857f1f6d7b910369309d-SNAPSHOT",
"next-api-vc2e018d5ffef54b2d17244b3d213e31723a084d6-SNAPSHOT",
"next-api-v42cb5f7c58e98d1911e87bffb35aee0a235b85f8-SNAPSHOT",
"next-api-v31a243bda80badbd7d643f68fc1f87e99639060f-SNAPSHOT",
"next-api-v17f6884434c2d70d1704b2322a951d9f07af8865-SNAPSHOT"
]
"tags": ["3.7.0", "3.6.1", "3.5.1", "v2.0.1"]
},
{
"name": "libretranslate",
"image": "libretranslate/libretranslate",
"tags": ["v1.3.8", "v1.3.6", "v1.3.4", "v1.3.2", "v1.3.0", "v1.2.8"]
},
{
"name": "meilisearch",
@ -477,6 +485,7 @@
"name": "minio",
"image": "minio/minio",
"tags": [
"RELEASE.2023-01-12T02-06-16Z",
"RELEASE.2023-01-06T18-11-18Z",
"RELEASE.2023-01-02T09-40-09Z",
"RELEASE.2022-12-12T19-27-27Z",
@ -505,8 +514,7 @@
"RELEASE.2022-09-01T23-53-36Z.fips",
"RELEASE.2022-08-26T19-53-15Z.fips",
"RELEASE.2022-08-25T07-17-05Z.fips",
"RELEASE.2022-08-22T23-53-06Z.hotfix.5fa3967bb",
"RELEASE.2022-08-22T23-53-06Z"
"RELEASE.2022-08-22T23-53-06Z.hotfix.5fa3967bb"
]
},
{
@ -549,77 +557,56 @@
"name": "nocodb",
"image": "nocodb/nocodb",
"tags": [
"0.101.2",
"0.101.0",
"0.100.1",
"0.99.1",
"0.98.4",
"0.98.2",
"0.98.0",
"0.96.4",
"0.96.2",
"0.96.0",
"0.92.3",
"0.91.10",
"0.91.9",
"0.91.7",
"0.91.0",
"0.90.10",
"0.90.7",
"0.90.4",
"0.90.2",
"0.90.0",
"0.84.15",
"0.84.12",
"0.84.8",
"0.84.6",
"0.84.2",
"0.84.1",
"0.83.6",
"0.83.3",
"0.83.1",
"0.82.0",
"0.81.0",
"0.11.46"
"0.99.2",
"0.99.0",
"0.98.3",
"0.98.1",
"0.97.0",
"0.96.3",
"0.96.1",
"0.92.4",
"0.92.0",
"0.91.8",
"0.91.6",
"0.91.1",
"0.90.11",
"0.90.8",
"0.90.5",
"0.90.3",
"0.90.1",
"0.84.16",
"0.84.14",
"0.84.10",
"0.84.9",
"0.84.7",
"0.84.3",
"0.83.8",
"0.83.5",
"0.83.2",
"0.83.0"
]
},
{
"name": "openblocks",
"image": "openblocksdev/openblocks-ce",
"tags": ["latest", "heroku", "beta", "1.1.3", "1.1.2", "1.1.1", "1.1.0", "1.0.21"]
"tags": ["1.1.3", "1.1.1", "1.0.21"]
},
{
"name": "plausibleanalytics-arm",
"image": "plausible/analytics",
"tags": [
"v1.5.1",
"v1.5.0-rc.2",
"v1.5.0-rc.1",
"v1.5.0",
"v1.5",
"v1.4.4",
"v1.4.3",
"v1.4.2",
"v1.4.1",
"v1.4.0.rc.0",
"v1.4.0-rc.0",
"v1.4.0",
"v1.4",
"v1.3.0-rc.1",
"v1.3.0-rc.0",
"v1.3.0",
"v1.3",
"v1.2.1",
"v1.2.0",
"v1.2-rc.1",
"v1.2-rc.0",
"v1.2",
"v1.1.1",
"v1.1.0",
"v1.1",
"v1.0.0",
"v1.0",
"v1",
"stable",
"master"
"v1.0.0"
]
},
{
@ -627,55 +614,26 @@
"image": "plausible/analytics",
"tags": [
"v1.5.1",
"v1.5.0-rc.2",
"v1.5.0-rc.1",
"v1.5.0",
"v1.5",
"v1.4.4",
"v1.4.3",
"v1.4.2",
"v1.4.1",
"v1.4.0.rc.0",
"v1.4.0-rc.0",
"v1.4.0",
"v1.4",
"v1.3.0-rc.1",
"v1.3.0-rc.0",
"v1.3.0",
"v1.3",
"v1.2.1",
"v1.2.0",
"v1.2-rc.1",
"v1.2-rc.0",
"v1.2",
"v1.1.1",
"v1.1.0",
"v1.1",
"v1.0.0",
"v1.0",
"v1",
"stable",
"master"
]
},
{
"name": "pocketbase",
"image": "coollabsio/pocketbase",
"tags": [
"0.8.0-arm64",
"0.8.0-amd64",
"0.8.0-aarch64",
"0.8.0",
"0.10.2-arm64",
"0.10.2-amd64",
"0.10.2-aarch64",
"0.10.2"
"v1.0.0"
]
},
{ "name": "pocketbase", "image": "coollabsio/pocketbase", "tags": ["0.11.0", "0.10.2", "0.8.0"] },
{
"name": "searxng",
"image": "searxng/searxng",
"tags": [
"2023.01.15-52d41559",
"2023.01.15-13b0c251",
"2023.01.14-b720a495",
"2023.01.14-449aebae",
"2023.01.14-18d895ff",
"2023.01.09-afd71a6c",
"2023.01.09-a90ed481",
"2023.01.08-54e63839",
@ -700,18 +658,14 @@
"2022.12.26-0d489617",
"2022.12.23-e8f72d70",
"2022.12.23-a2d506d4",
"2022.12.22-d75ae7c8",
"2022.12.16-f5bd73d9",
"2022.12.16-b9274821",
"2022.12.16-42ca37a6",
"2022.12.16-2a51c856",
"2022.12.16-0dac581c"
"2022.12.22-d75ae7c8"
]
},
{
"name": "trilium",
"image": "zadam/trilium",
"tags": [
"0.58.4",
"0.57.4",
"0.57.2",
"0.56.1",
@ -740,8 +694,7 @@
"0.45.7",
"0.45.5",
"0.45.3",
"0.44.8",
"0.44.6"
"0.44.8"
]
},
{
@ -907,6 +860,15 @@
"image": "weblate/weblate",
"tags": [
"latest",
"edge-2023-01-13-e824b551f23c3679467e38b06366744a06aa3b0c",
"edge-2023-01-13-468b996565e6b62edb78d40b515c476e0d860273",
"edge-2023-01-12-fe3d58b14f119eb5501220e9f096949c2e1ec2d3",
"edge-2023-01-12-112f75f9ee9e118ad493215f89742e6e091be8d0",
"edge-2023-01-11-f7bb190993e329d1529694e8cc7f5e0a80ccd615",
"edge-2023-01-11-e8ef3183aa7723f32c2b60c7c3b89910f2c7c593",
"edge-2023-01-11-155231f6cde18a65e3f35093d66dd0ce93aa7154",
"edge-2023-01-10-e47516e4022f87c019e61998b556b69111187aa9",
"edge-2023-01-10-98c6b38c746165adb27b2a8e93a74fa9ab64f17c",
"edge-2023-01-10-1df5c9dd96a6d8650f6881942fecbe33e1884295",
"edge-2023-01-09-7029b7b6c630be7cdac07d1629573dd2b81bc05f",
"edge-2023-01-09-4b05a878aa25b2c544a4e77027769b5934ec561f",
@ -926,16 +888,24 @@
"edge-2022-12-24-3e1503494ce06ad6ff32f02db1a7d59224e5c860",
"edge-2022-12-21-cac4b09f943fe97700e3a33b7caf23277d2fcc11",
"edge-2022-12-21-3a8dd1bf66a7295f3512346bc1c97d55c5649dcf",
"edge-2022-12-16-e93caa3b014543b716b946f2c7fbf4a8f9be6099",
"edge-2022-12-16-318a467d2e529a081e9ea9dbad993c1736ff1a00",
"edge-2022-12-16-1af41ec4bd3838f967d88b68dec8195419e01e6f",
"edge-2022-12-16-02e9d020b01d004655c3af20c68a30f6c4645c1a",
"edge-2022-12-15-a6af1384a0831b17c43da7262f80d0cfbc766835",
"edge-2022-12-15-a1c9f77b301a9e23fc05ef2adc4694cceb632c25",
"edge-2022-12-15-1305f7115ef79b75e638b097772680d9cadbd4d0",
"edge-2022-12-14-b400145f05687e647bd4c8192be99f7f04373fb5",
"edge-2022-12-12-c0db193a3baacd107c5f2c28c6e0af89c3d5afa3",
"edge-2022-12-09-647d40c67cf405870ba71a01584a42cfaec5915f"
"edge-2022-12-16-e93caa3b014543b716b946f2c7fbf4a8f9be6099"
]
},
{
"name": "whoogle",
"image": "benbusby/whoogle-search",
"tags": [
"0.8.0",
"0.7.3",
"0.7.1",
"0.6.0",
"0.5.3",
"0.5.1",
"0.4.1",
"0.3.2",
"v0.3.0",
"0.1.2",
"0.1.0"
]
},
{

View File

@ -111,7 +111,7 @@
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
@ -166,7 +166,7 @@
defaultValue: "false"
description: "Disable or enable web ui. True or false."
- templateVersion: 1.0.0
defaultVersion: 0.8.1
defaultVersion: 0.8.0
documentation: https://github.com/benbusby/whoogle-search
type: whoogle
name: Whoogle Search
@ -223,7 +223,7 @@
ports:
- "3000"
- templateVersion: 1.0.0
defaultVersion: "0.10.2"
defaultVersion: "0.11.0"
documentation: https://pocketbase.io/docs/
type: pocketbase
name: Pocketbase
@ -381,7 +381,7 @@
defaultValue: plausible.js
description: This is the default script name.
- templateVersion: 1.0.0
defaultVersion: "1.17"
defaultVersion: "1.18"
documentation: https://docs.gitea.io
type: gitea
name: Gitea
@ -594,7 +594,7 @@
defaultValue: $$generate_password
required: true
- templateVersion: 1.0.0
defaultVersion: v1.8.9
defaultVersion: v1.9.3
documentation: https://docs.appsmith.com/getting-started/setup/instance-configuration/
type: appsmith
name: Appsmith
@ -627,7 +627,7 @@
defaultValue: "true"
description: ""
- templateVersion: 1.0.0
defaultVersion: 0.57.4
defaultVersion: 0.58.4
documentation: https://hub.docker.com/r/zadam/trilium
description: "A hierarchical note taking application with focus on building large personal knowledge bases."
labels:
@ -647,7 +647,7 @@
- "8080"
variables: []
- templateVersion: 1.0.0
defaultVersion: 1.18.5
defaultVersion: 1.19.4
documentation: https://hub.docker.com/r/louislam/uptime-kuma
description: A free & fancy self-hosted monitoring tool.
labels:
@ -664,7 +664,7 @@
- "3001"
variables: []
- templateVersion: 1.0.0
defaultVersion: "5.8"
defaultVersion: "6.0"
documentation: https://hub.docker.com/r/silviof/docker-languagetool
description: "A multilingual grammar, style and spell checker."
type: languagetool
@ -679,7 +679,7 @@
- "8010"
variables: []
- templateVersion: 1.0.0
defaultVersion: 1.26.0
defaultVersion: 1.27.0
documentation: https://hub.docker.com/r/vaultwarden/server
description: "Bitwarden compatible server written in Rust."
type: vaultwarden
@ -697,7 +697,7 @@
- "80"
variables: []
- templateVersion: 1.0.0
defaultVersion: 9.3.1
defaultVersion: 9.3.2
documentation: https://hub.docker.com/r/grafana/grafana
type: grafana
name: Grafana
@ -718,7 +718,7 @@
- "3000"
variables: []
- templateVersion: 1.0.0
defaultVersion: 1.1.2
defaultVersion: 1.2.0
documentation: https://appwrite.io/docs
type: appwrite
name: Appwrite
@ -1888,7 +1888,7 @@
defaultValue: weblate
description: ""
- templateVersion: 1.0.0
defaultVersion: 2022.12.12-966e9c3c
defaultVersion: 2023.01.15-52d41559
documentation: https://docs.searxng.org/
type: searxng
name: SearXNG
@ -1961,7 +1961,7 @@
defaultValue: $$generate_password
description: ""
- templateVersion: 1.0.0
defaultVersion: v3.0.0
defaultVersion: v3.0.2
documentation: https://glitchtip.com/documentation
type: glitchtip
name: GlitchTip
@ -2183,7 +2183,7 @@
defaultValue: glitchtip
description: ""
- templateVersion: 1.0.0
defaultVersion: v2.16.0
defaultVersion: v2.16.1
documentation: https://hasura.io/docs/latest/index/
type: hasura
name: Hasura
@ -2663,7 +2663,7 @@
description: ""
showOnConfiguration: true
- templateVersion: 1.0.0
defaultVersion: v0.30.1
defaultVersion: v0.30.5
documentation: https://docs.meilisearch.com/learn/getting_started/quick_start.html
type: meilisearch
name: MeiliSearch
@ -2693,7 +2693,7 @@
showOnConfiguration: true
- templateVersion: 1.0.0
ignore: true
defaultVersion: latest
defaultVersion: 5.30.0
documentation: https://docs.ghost.org
arch: amd64
type: ghost-mariadb
@ -2811,7 +2811,7 @@
defaultValue: $$generate_password
description: ""
- templateVersion: 1.0.0
defaultVersion: "5.25.3"
defaultVersion: 5.30.0
documentation: https://docs.ghost.org
type: ghost-only
name: Ghost
@ -2875,7 +2875,7 @@
placeholder: "ghost_db"
required: true
- templateVersion: 1.0.0
defaultVersion: "5.25.3"
defaultVersion: 5.30.0
documentation: https://docs.ghost.org
type: ghost-mysql
name: Ghost
@ -2952,7 +2952,7 @@
defaultValue: $$generate_password
description: ""
- templateVersion: 1.0.0
defaultVersion: php8.1
defaultVersion: php8.2
documentation: https://wordpress.org/
type: wordpress
name: WordPress
@ -3042,7 +3042,7 @@
description: ""
readOnly: true
- templateVersion: 1.0.0
defaultVersion: php8.1
defaultVersion: php8.2
documentation: https://wordpress.org/
type: wordpress-only
name: WordPress
@ -3116,7 +3116,7 @@
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', 0);
- templateVersion: 1.0.0
defaultVersion: 4.9.0
defaultVersion: 4.9.1
documentation: https://coder.com/docs/coder-oss/latest
type: vscodeserver
name: VSCode Server
@ -3131,7 +3131,6 @@
depends_on: []
image: "codercom/code-server:$$core_version"
volumes:
- "$$id-config-data:/home/coder/.local/share/code-server"
- "$$id-vscodeserver-data:/home/coder"
- "$$id-keys-directory:/root/.ssh"
- "$$id-theme-and-plugin-directory:/root/.local/share/code-server"
@ -3147,7 +3146,7 @@
description: ""
showOnConfiguration: true
- templateVersion: 1.0.0
defaultVersion: RELEASE.2022-12-12T19-27-27Z
defaultVersion: RELEASE.2023-01-12T02-06-16Z
documentation: https://min.io/docs/minio
type: minio
name: MinIO
@ -3206,7 +3205,7 @@
description: ""
showOnConfiguration: true
- templateVersion: 1.0.0
defaultVersion: 0.21.1
defaultVersion: stable
documentation: https://fider.io/docs
type: fider
name: Fider
@ -3325,7 +3324,7 @@
defaultValue: $$generate_username
description: ""
- templateVersion: 1.0.0
defaultVersion: 0.207.0
defaultVersion: 0.210.1
documentation: https://docs.n8n.io
type: n8n
name: n8n.io
@ -3356,7 +3355,7 @@
defaultValue: $$generate_fqdn
description: ""
- templateVersion: 1.0.0
defaultVersion: stable
defaultVersion: v1.5.1
documentation: https://plausible.io/doc/
arch: amd64
type: plausibleanalytics
@ -3502,7 +3501,7 @@
defaultValue: plausible.js
description: This is the default script name.
- templateVersion: 1.0.0
defaultVersion: 0.99.1
defaultVersion: 0.101.2
documentation: https://docs.nocodb.com
type: nocodb
name: NocoDB

View File

@ -225,8 +225,22 @@ async function getTagsTemplates() {
const { default: got } = await import('got');
try {
if (isDev) {
const templates = await fs.readFile('./devTemplates.yaml', 'utf8');
const tags = await fs.readFile('./devTags.json', 'utf8');
let templates = await fs.readFile('./devTemplates.yaml', '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('./tags.json', tags);
console.log('[004] Tags and templates loaded in dev mode...');

View File

@ -196,7 +196,7 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({
debug: true,
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 });
} catch (error) {
@ -601,6 +601,7 @@ import * as buildpacks from '../lib/buildPacks';
}
if (buildPack === 'compose') {
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`;
try {
const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId,
@ -630,7 +631,7 @@ import * as buildpacks from '../lib/buildPacks';
buildId,
applicationId,
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 prisma.build.update({
@ -725,7 +726,7 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({
debug,
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 });
} catch (error) {

View File

@ -26,8 +26,10 @@ export default async function (data) {
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 = [];
@ -43,8 +45,22 @@ export default async function (data) {
let networks = {};
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
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];
let build = typeof value['build'] === 'undefined' ? [] : value['build'];
if (Object.keys(build).length > 0) {
build = Object.entries(build).map(([key, value]) => `${key}=${value}`);
}
value['build'] = {
...build,
args: [...(build?.args || []), ...buildEnvs]
};
value['labels'] = labels;
// TODO: If we support separated volume for each service, we need to add it here
if (value['volumes']?.length > 0) {
@ -90,12 +106,13 @@ export default async function (data) {
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } });
await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml));
console.log(yaml.dump(dockerComposeYaml));
await executeCommand({
debug,
buildId,
applicationId,
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 executeCommand({
@ -103,7 +120,7 @@ export default async function (data) {
buildId,
applicationId,
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 });
}

View File

@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common
import { scheduler } from './scheduler';
import type { ExecaChildProcess } from 'execa';
export const version = '3.12.10';
export const version = '3.12.11';
export const isDev = process.env.NODE_ENV === 'development';
export const sentryDSN =
'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
@ -1912,27 +1912,28 @@ export function generateSecrets(
secrets: Array<any>,
pullmergeRequestId: string,
isBuild = false,
port = null
port = null,
compose = false
): Array<string> {
const envs = [];
const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret);
const normalSecrets = secrets.filter((s) => !s.isPRMRSecret);
if (pullmergeRequestId && isPRMRSecret.length > 0) {
isPRMRSecret.forEach((secret) => {
if (isBuild && !secret.isBuildSecret) {
if ((isBuild && !secret.isBuildSecret) || (!isBuild && secret.isBuildSecret)) {
return;
}
const build = isBuild && secret.isBuildSecret;
envs.push(parseSecret(secret, build));
envs.push(parseSecret(secret, compose ? false : build));
});
}
if (!pullmergeRequestId && normalSecrets.length > 0) {
normalSecrets.forEach((secret) => {
if (isBuild && !secret.isBuildSecret) {
if ((isBuild && !secret.isBuildSecret) || (!isBuild && secret.isBuildSecret)) {
return;
}
const build = isBuild && secret.isBuildSecret;
envs.push(parseSecret(secret, build));
envs.push(parseSecret(secret, compose ? false : build));
});
}
const portFound = envs.filter((env) => env.startsWith('PORT'));

View File

@ -122,6 +122,9 @@ export async function cleanupUnconfiguredApplications(request: FastifyRequest<an
include: { settings: true, destinationDocker: true, teams: true }
});
for (const application of applications) {
if (application?.buildPack === 'compose') {
continue;
}
if (
!application.buildPack ||
!application.destinationDockerId ||
@ -670,7 +673,7 @@ export async function restartApplication(
await executeCommand({
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();
}
@ -746,6 +749,7 @@ export async function deleteApplication(
await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: id } });
await prisma.previewApplication.deleteMany({ where: { applicationId: id } });
if (teamId === '0') {
await prisma.application.deleteMany({ where: { id } });
} else {
@ -1451,7 +1455,7 @@ export async function restartPreview(
await executeCommand({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` });
await executeCommand({
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();
}
@ -1605,12 +1609,7 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
.split('\n')
.map((l) => ansi(l))
.filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout);
const sortedLogs = logs.sort((a, b) =>
day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1
);
return { logs: sortedLogs };
// }
return { logs: stripLogsStderr.concat(stripLogsStdout) };
} catch (error) {
const { statusCode, stderr } = error;
if (stderr.startsWith('Error: No such container')) {

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,9 @@
[
{ "name": "directus-postgresql", "image": "directus/directus", "tags": ["9.22"] },
{ "name": "whoogle", "image": "benbusby/whoogle-search", "tags": ["0.8.1"] },
{ "name": "libretranslate", "image": "libretranslate/libretranslate", "tags": ["v1.3.8"] },
{
"name": "appsmith",
"image": "appsmith/appsmith-ce",
"tags": [
"v1.9.3",
"v1.9.1",
"v1.8.15",
"v1.8.12",
@ -34,8 +32,7 @@
"v1.6.5",
"v1.6.3",
"v1.6.1",
"v1.5.30",
"v1.5.28"
"v1.5.30"
]
},
{
@ -74,6 +71,42 @@
"0.3.1"
]
},
{
"name": "directus-postgresql",
"image": "directus/directus",
"tags": [
"9.22.3",
"9.22.0",
"9.21.0",
"9.20.4",
"9.20.2",
"9.20.0",
"9.19.2",
"9.18.0",
"9.17.4",
"9.17.2",
"9.17.0",
"9.16.0",
"9.15.0",
"9.14.5",
"9.14.3",
"9.14.0",
"9.13.0",
"9.12.2",
"9.12.0",
"9.11.0",
"9.10.0",
"9.9.0",
"9.8.0",
"9.7.0",
"9.6.0",
"9.5.2",
"9.5.0",
"9.4.2",
"9.4.0",
"9.3.0"
]
},
{
"name": "fider",
"image": "getfider/fider",
@ -114,6 +147,9 @@
"name": "ghost-mariadb",
"image": "bitnami/ghost",
"tags": [
"5.30.1",
"5.30.0",
"5.29.0",
"5.28.0",
"5.27.0",
"5.26.4",
@ -141,9 +177,6 @@
"5.22.4",
"5.22.3",
"5.22.2",
"5.22.1",
"5.22.0",
"5.21.0",
"4.48.8"
]
},
@ -151,6 +184,8 @@
"name": "ghost-mysql",
"image": "library/ghost",
"tags": [
"5.30.0",
"5.29.0",
"5.28.0",
"5.27.0",
"5.26.4",
@ -178,15 +213,15 @@
"5.17.2",
"5.17.1",
"5.17.0",
"5.16.2",
"5.14.2",
"5.14.1"
"5.16.2"
]
},
{
"name": "ghost-only",
"image": "library/ghost",
"tags": [
"5.30.0",
"5.29.0",
"5.28.0",
"5.27.0",
"5.26.4",
@ -214,9 +249,7 @@
"5.17.2",
"5.17.1",
"5.17.0",
"5.16.2",
"5.14.2",
"5.14.1"
"5.16.2"
]
},
{
@ -373,6 +406,7 @@
"7.0.0",
"6.0.1",
"6.0.0",
"20.0.3",
"20.0.2",
"20.0.1",
"20.0.0",
@ -404,38 +438,12 @@
{
"name": "lavalink",
"image": "fredboat/lavalink",
"tags": [
"v3.7",
"v3.6",
"v3-vda0b3a4b3916a7b1a2b79702de1143c3a6939810-SNAPSHOT",
"v3-vc92690c425390bd20f6c51643c67ba79ab85b7e0-SNAPSHOT",
"v3-vab81dcd46adf3e8a961dd57eacd2a1bde1233e6c-SNAPSHOT",
"v3-v9c9432704d6a4badfcbd06a57597c54bed8f4326-SNAPSHOT",
"v3-v3.0",
"v3-v3",
"v3-v124f8fae7dab299f9cdf1cb4c1715be455497286-SNAPSHOT",
"v3-",
"v3",
"v2.0.1",
"v2.0",
"v2",
"update-udpqueue-vb4a439d6147dbd8641ea4f265e8efc9f1e16e2d3-SNAPSHOT",
"update-udpqueue-",
"update-udpqueue",
"revert-713-fix-error-for-loading-jda-nas",
"refactor-github-actions",
"patch-update-lp",
"patch-update-github-actions",
"patch-more-configurable-github-actions",
"patch-lavaplayer-update",
"patch-lavaplayer-bump",
"patch-build-number",
"next-api-vd4db194cac7a839a3899857f1f6d7b910369309d-SNAPSHOT",
"next-api-vc2e018d5ffef54b2d17244b3d213e31723a084d6-SNAPSHOT",
"next-api-v42cb5f7c58e98d1911e87bffb35aee0a235b85f8-SNAPSHOT",
"next-api-v31a243bda80badbd7d643f68fc1f87e99639060f-SNAPSHOT",
"next-api-v17f6884434c2d70d1704b2322a951d9f07af8865-SNAPSHOT"
]
"tags": ["3.7.0", "3.6.1", "3.5.1", "v2.0.1"]
},
{
"name": "libretranslate",
"image": "libretranslate/libretranslate",
"tags": ["v1.3.8", "v1.3.6", "v1.3.4", "v1.3.2", "v1.3.0", "v1.2.8"]
},
{
"name": "meilisearch",
@ -477,6 +485,7 @@
"name": "minio",
"image": "minio/minio",
"tags": [
"RELEASE.2023-01-12T02-06-16Z",
"RELEASE.2023-01-06T18-11-18Z",
"RELEASE.2023-01-02T09-40-09Z",
"RELEASE.2022-12-12T19-27-27Z",
@ -505,8 +514,7 @@
"RELEASE.2022-09-01T23-53-36Z.fips",
"RELEASE.2022-08-26T19-53-15Z.fips",
"RELEASE.2022-08-25T07-17-05Z.fips",
"RELEASE.2022-08-22T23-53-06Z.hotfix.5fa3967bb",
"RELEASE.2022-08-22T23-53-06Z"
"RELEASE.2022-08-22T23-53-06Z.hotfix.5fa3967bb"
]
},
{
@ -549,77 +557,56 @@
"name": "nocodb",
"image": "nocodb/nocodb",
"tags": [
"0.101.2",
"0.101.0",
"0.100.1",
"0.99.1",
"0.98.4",
"0.98.2",
"0.98.0",
"0.96.4",
"0.96.2",
"0.96.0",
"0.92.3",
"0.91.10",
"0.91.9",
"0.91.7",
"0.91.0",
"0.90.10",
"0.90.7",
"0.90.4",
"0.90.2",
"0.90.0",
"0.84.15",
"0.84.12",
"0.84.8",
"0.84.6",
"0.84.2",
"0.84.1",
"0.83.6",
"0.83.3",
"0.83.1",
"0.82.0",
"0.81.0",
"0.11.46"
"0.99.2",
"0.99.0",
"0.98.3",
"0.98.1",
"0.97.0",
"0.96.3",
"0.96.1",
"0.92.4",
"0.92.0",
"0.91.8",
"0.91.6",
"0.91.1",
"0.90.11",
"0.90.8",
"0.90.5",
"0.90.3",
"0.90.1",
"0.84.16",
"0.84.14",
"0.84.10",
"0.84.9",
"0.84.7",
"0.84.3",
"0.83.8",
"0.83.5",
"0.83.2",
"0.83.0"
]
},
{
"name": "openblocks",
"image": "openblocksdev/openblocks-ce",
"tags": ["latest", "heroku", "beta", "1.1.3", "1.1.2", "1.1.1", "1.1.0", "1.0.21"]
"tags": ["1.1.3", "1.1.1", "1.0.21"]
},
{
"name": "plausibleanalytics-arm",
"image": "plausible/analytics",
"tags": [
"v1.5.1",
"v1.5.0-rc.2",
"v1.5.0-rc.1",
"v1.5.0",
"v1.5",
"v1.4.4",
"v1.4.3",
"v1.4.2",
"v1.4.1",
"v1.4.0.rc.0",
"v1.4.0-rc.0",
"v1.4.0",
"v1.4",
"v1.3.0-rc.1",
"v1.3.0-rc.0",
"v1.3.0",
"v1.3",
"v1.2.1",
"v1.2.0",
"v1.2-rc.1",
"v1.2-rc.0",
"v1.2",
"v1.1.1",
"v1.1.0",
"v1.1",
"v1.0.0",
"v1.0",
"v1",
"stable",
"master"
"v1.0.0"
]
},
{
@ -627,55 +614,26 @@
"image": "plausible/analytics",
"tags": [
"v1.5.1",
"v1.5.0-rc.2",
"v1.5.0-rc.1",
"v1.5.0",
"v1.5",
"v1.4.4",
"v1.4.3",
"v1.4.2",
"v1.4.1",
"v1.4.0.rc.0",
"v1.4.0-rc.0",
"v1.4.0",
"v1.4",
"v1.3.0-rc.1",
"v1.3.0-rc.0",
"v1.3.0",
"v1.3",
"v1.2.1",
"v1.2.0",
"v1.2-rc.1",
"v1.2-rc.0",
"v1.2",
"v1.1.1",
"v1.1.0",
"v1.1",
"v1.0.0",
"v1.0",
"v1",
"stable",
"master"
]
},
{
"name": "pocketbase",
"image": "coollabsio/pocketbase",
"tags": [
"0.8.0-arm64",
"0.8.0-amd64",
"0.8.0-aarch64",
"0.8.0",
"0.10.2-arm64",
"0.10.2-amd64",
"0.10.2-aarch64",
"0.10.2"
"v1.0.0"
]
},
{ "name": "pocketbase", "image": "coollabsio/pocketbase", "tags": ["0.11.0", "0.10.2", "0.8.0"] },
{
"name": "searxng",
"image": "searxng/searxng",
"tags": [
"2023.01.15-52d41559",
"2023.01.15-13b0c251",
"2023.01.14-b720a495",
"2023.01.14-449aebae",
"2023.01.14-18d895ff",
"2023.01.09-afd71a6c",
"2023.01.09-a90ed481",
"2023.01.08-54e63839",
@ -700,18 +658,14 @@
"2022.12.26-0d489617",
"2022.12.23-e8f72d70",
"2022.12.23-a2d506d4",
"2022.12.22-d75ae7c8",
"2022.12.16-f5bd73d9",
"2022.12.16-b9274821",
"2022.12.16-42ca37a6",
"2022.12.16-2a51c856",
"2022.12.16-0dac581c"
"2022.12.22-d75ae7c8"
]
},
{
"name": "trilium",
"image": "zadam/trilium",
"tags": [
"0.58.4",
"0.57.4",
"0.57.2",
"0.56.1",
@ -740,8 +694,7 @@
"0.45.7",
"0.45.5",
"0.45.3",
"0.44.8",
"0.44.6"
"0.44.8"
]
},
{
@ -907,6 +860,15 @@
"image": "weblate/weblate",
"tags": [
"latest",
"edge-2023-01-13-e824b551f23c3679467e38b06366744a06aa3b0c",
"edge-2023-01-13-468b996565e6b62edb78d40b515c476e0d860273",
"edge-2023-01-12-fe3d58b14f119eb5501220e9f096949c2e1ec2d3",
"edge-2023-01-12-112f75f9ee9e118ad493215f89742e6e091be8d0",
"edge-2023-01-11-f7bb190993e329d1529694e8cc7f5e0a80ccd615",
"edge-2023-01-11-e8ef3183aa7723f32c2b60c7c3b89910f2c7c593",
"edge-2023-01-11-155231f6cde18a65e3f35093d66dd0ce93aa7154",
"edge-2023-01-10-e47516e4022f87c019e61998b556b69111187aa9",
"edge-2023-01-10-98c6b38c746165adb27b2a8e93a74fa9ab64f17c",
"edge-2023-01-10-1df5c9dd96a6d8650f6881942fecbe33e1884295",
"edge-2023-01-09-7029b7b6c630be7cdac07d1629573dd2b81bc05f",
"edge-2023-01-09-4b05a878aa25b2c544a4e77027769b5934ec561f",
@ -926,16 +888,24 @@
"edge-2022-12-24-3e1503494ce06ad6ff32f02db1a7d59224e5c860",
"edge-2022-12-21-cac4b09f943fe97700e3a33b7caf23277d2fcc11",
"edge-2022-12-21-3a8dd1bf66a7295f3512346bc1c97d55c5649dcf",
"edge-2022-12-16-e93caa3b014543b716b946f2c7fbf4a8f9be6099",
"edge-2022-12-16-318a467d2e529a081e9ea9dbad993c1736ff1a00",
"edge-2022-12-16-1af41ec4bd3838f967d88b68dec8195419e01e6f",
"edge-2022-12-16-02e9d020b01d004655c3af20c68a30f6c4645c1a",
"edge-2022-12-15-a6af1384a0831b17c43da7262f80d0cfbc766835",
"edge-2022-12-15-a1c9f77b301a9e23fc05ef2adc4694cceb632c25",
"edge-2022-12-15-1305f7115ef79b75e638b097772680d9cadbd4d0",
"edge-2022-12-14-b400145f05687e647bd4c8192be99f7f04373fb5",
"edge-2022-12-12-c0db193a3baacd107c5f2c28c6e0af89c3d5afa3",
"edge-2022-12-09-647d40c67cf405870ba71a01584a42cfaec5915f"
"edge-2022-12-16-e93caa3b014543b716b946f2c7fbf4a8f9be6099"
]
},
{
"name": "whoogle",
"image": "benbusby/whoogle-search",
"tags": [
"0.8.0",
"0.7.3",
"0.7.1",
"0.6.0",
"0.5.3",
"0.5.1",
"0.4.1",
"0.3.2",
"v0.3.0",
"0.1.2",
"0.1.0"
]
},
{

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';
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 {
if (error instanceof Error) {
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);
let extension = 'png';
let svgs = [
'mattermost',
'repman',
'directus',
'pocketbase',
'gitea',
'languagetool',

View File

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

View File

@ -351,13 +351,17 @@
}
async function reloadCompose() {
if (loading.reloadCompose) return;
if (!$appSession.tokens.github && !isPublicRepository) {
const { token } = await get(`/applications/${id}/configuration/githubToken`);
$appSession.tokens.github = token;
}
loading.reloadCompose = true;
try {
if (application.gitSource.type === 'github') {
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation
: `/${application.dockerComposeFileLocation}`;
? application.dockerComposeFileLocation
: `/${application.dockerComposeFileLocation}`;
const headers = isPublicRepository
? {}
: {
@ -381,20 +385,20 @@
}
}
if (application.gitSource.type === 'gitlab') {
if (!$appSession.tokens.gitlab) {
if (!$appSession.tokens.gitlab && !isPublicRepository) {
await getGitlabToken();
}
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation.substring(1) // Remove the '/' from the start
: application.dockerComposeFileLocation;
? 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 lastIndex = composeLocation.lastIndexOf('/') + 1;
const path = composeLocation.substring(0, lastIndex);
const fileName = composeLocation.substring(lastIndex);
const headers = isPublicRepository
? {}
: {
@ -407,8 +411,7 @@
...headers
});
const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) =>
file.name === fileName && file.type === 'blob'
(file: { name: string; type: string }) => file.name === fileName && file.type === 'blob'
);
const id = dockerComposeFileYml.id;

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 () => {
const { data } = await trpc.applications.getApplicationById.query({ id });
application = data;
if (data.dockerComposeFile) {
if (application.dockerComposeFile && application.buildPack === 'compose') {
services = normalizeDockerServices(JSON.parse(data.dockerComposeFile).services);
} else {
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

View File

@ -24,6 +24,7 @@
"@fastify/jwt": "6.5.0",
"@fastify/static": "6.6.0",
"@fastify/websocket": "7.1.1",
"@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2",
"@prisma/client": "4.6.1",
"@trpc/client": "10.1.0",

View File

@ -14,17 +14,16 @@ import {
import {
createDirectories,
decrypt,
defaultComposeConfiguration,
getDomain,
prisma,
generateSecrets,
decryptApplication,
isDev,
pushToRegistry,
executeCommand,
generateSecrets
pushToRegistry
} from '../lib/common';
import * as importers from '../lib/importers';
import * as buildpacks from '../lib/buildPacks';
import { prisma } from '../prisma';
import { executeCommand } from '../lib/executeCommand';
import { defaultComposeConfiguration } from '../lib/docker';
(async () => {
if (parentPort) {
@ -196,7 +195,7 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({
debug: true,
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 });
} catch (error) {
@ -532,7 +531,48 @@ import * as buildpacks from '../lib/buildPacks';
});
if (forceRebuild) deployNeeded = true;
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]({
dockerId: destinationDocker.id,
network: destinationDocker.network,
@ -600,6 +640,7 @@ import * as buildpacks from '../lib/buildPacks';
}
if (buildPack === 'compose') {
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`;
try {
const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId,
@ -629,7 +670,7 @@ import * as buildpacks from '../lib/buildPacks';
buildId,
applicationId,
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 prisma.build.update({
@ -724,7 +765,7 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({
debug,
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 });
} catch (error) {
@ -803,5 +844,8 @@ import * as buildpacks from '../lib/buildPacks';
while (true) {
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,127 @@
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 (Object.keys(build).length > 0) {
build = Object.entries(build).map(([key, value]) => `${key}=${value}`);
}
value['build'] = {
...build,
args: [...(build?.args || []), ...buildEnvs]
};
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);
}

View File

@ -0,0 +1,28 @@
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, imageforBuild): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageforBuild}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { baseImage, baseBuildImage } = data;
await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,17 @@
import { executeCommand } from "../executeCommand";
import { saveBuildLog } from "./common";
export default async function (data: any): Promise<void> {
const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory, baseImage } = data
try {
await saveBuildLog({ line: `Building production image...`, buildId, applicationId });
await executeCommand({
buildId,
debug,
dockerId,
command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder ${baseImage}`
})
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,41 @@
import node from './node';
import staticApp from './static';
import docker from './docker';
import gatsby from './gatsby';
import svelte from './svelte';
import react from './react';
import nestjs from './nestjs';
import nextjs from './nextjs';
import nuxtjs from './nuxtjs';
import vuejs from './vuejs';
import php from './php';
import rust from './rust';
import astro from './static';
import eleventy from './static';
import python from './python';
import deno from './deno';
import laravel from './laravel';
import heroku from './heroku';
import compose from './compose';
export {
node,
staticApp,
docker,
gatsby,
svelte,
react,
nestjs,
nextjs,
nuxtjs,
vuejs,
php,
rust,
astro,
eleventy,
python,
deno,
laravel,
heroku,
compose
};

View File

@ -0,0 +1,46 @@
import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildCacheImageForLaravel, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const { workdir, applicationId, tag, buildId, port, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
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(`ENV WEB_DOCUMENT_ROOT /app/public`);
Dockerfile.push(`COPY --chown=application:application composer.* ./`);
Dockerfile.push(`COPY --chown=application:application database/ database/`);
Dockerfile.push(
`RUN composer install --ignore-platform-reqs --no-interaction --no-plugins --no-scripts --prefer-dist`
);
Dockerfile.push(
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/public/js/ /app/public/js/`
);
Dockerfile.push(
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/public/css/ /app/public/css/`
);
Dockerfile.push(
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json`
);
Dockerfile.push(`COPY --chown=application:application . ./`);
Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
const { baseImage, baseBuildImage } = data;
try {
await buildCacheImageForLaravel(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,31 @@
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const { buildId, applicationId, tag, port, startCommand, workdir, baseDirectory } = data;
const Dockerfile: Array<string> = [];
const isPnpm = startCommand.includes('pnpm');
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
}
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${baseDirectory || ''} ./`);
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { baseImage, baseBuildImage } = data;
await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,66 @@
import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildCacheImageWithNode, buildImage, checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {
applicationId,
buildId,
tag,
workdir,
publishDirectory,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId,
deploymentType,
baseImage
} = data;
const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
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 (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
}
if (deploymentType === 'node') {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`RUN ${buildCommand}`);
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`);
} else if (deploymentType === 'static') {
if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`EXPOSE 80`);
}
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { baseImage, baseBuildImage, deploymentType, buildCommand } = data;
if (deploymentType === 'node') {
await createDockerfile(data, baseImage);
await buildImage(data);
} else if (deploymentType === 'static') {
if (buildCommand) await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
}
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,49 @@
import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildImage, checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {
workdir,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId,
buildId
} = data;
const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
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 (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`);
if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`);
}
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { baseImage } = data;
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,66 @@
import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildCacheImageWithNode, buildImage, checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {
applicationId,
buildId,
tag,
workdir,
publishDirectory,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId,
deploymentType,
baseImage
} = data;
const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
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 (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
}
if (deploymentType === 'node') {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`RUN ${buildCommand}`);
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`);
} else if (deploymentType === 'static') {
if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
Dockerfile.push(`EXPOSE 80`);
}
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { baseImage, baseBuildImage, deploymentType, buildCommand } = data;
if (deploymentType === 'node') {
await createDockerfile(data, baseImage);
await buildImage(data);
} else if (deploymentType === 'static') {
if (buildCommand) await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
}
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,50 @@
import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildImage } from './common';
const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
const { workdir, baseDirectory, buildId, port, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = [];
let composerFound = false;
try {
await fs.readFile(`${workdir}${baseDirectory || ''}/composer.json`);
composerFound = true;
} catch (error) {}
Dockerfile.push(`FROM ${image}`);
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(`COPY .${baseDirectory || ''} /app`);
if (htaccessFound) {
Dockerfile.push(`COPY .${baseDirectory || ''}/.htaccess ./`);
}
if (composerFound) {
Dockerfile.push(`RUN composer install`);
}
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
const { workdir, baseDirectory, baseImage } = data;
try {
let htaccessFound = false;
try {
await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`);
htaccessFound = true;
} catch (e) {
//
}
await createDockerfile(data, baseImage, htaccessFound);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,67 @@
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,
pythonWSGI,
pythonModule,
pythonVariable,
buildId
} = data;
const Dockerfile: Array<string> = [];
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 (pythonWSGI?.toLowerCase() === 'gunicorn') {
Dockerfile.push(`RUN pip install gunicorn`);
} else if (pythonWSGI?.toLowerCase() === 'uvicorn') {
Dockerfile.push(`RUN pip install uvicorn`);
} else if (pythonWSGI?.toLowerCase() === 'uwsgi') {
Dockerfile.push(`RUN apk add --no-cache uwsgi-python3`);
// Dockerfile.push(`RUN pip install --no-cache-dir uwsgi`)
}
try {
await fs.stat(`${workdir}${baseDirectory || ''}/requirements.txt`);
Dockerfile.push(`COPY .${baseDirectory || ''}/requirements.txt ./`);
Dockerfile.push(`RUN pip install --no-cache-dir -r .${baseDirectory || ''}/requirements.txt`);
} catch (e) {
//
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`EXPOSE ${port}`);
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
Dockerfile.push(`CMD gunicorn -w=4 -b=0.0.0.0:8000 ${pythonModule}:${pythonVariable}`);
} else if (pythonWSGI?.toLowerCase() === 'uvicorn') {
Dockerfile.push(`CMD uvicorn ${pythonModule}:${pythonVariable} --port ${port} --host 0.0.0.0`);
} else if (pythonWSGI?.toLowerCase() === 'uwsgi') {
Dockerfile.push(
`CMD uwsgi --master -p 4 --http-socket 0.0.0.0:8000 --uid uwsgi --plugins python3 --protocol uwsgi --wsgi ${pythonModule}:${pythonVariable}`
);
} else {
Dockerfile.push(`CMD python ${pythonModule}`);
}
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,28 @@
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { baseImage, baseBuildImage } = data;
await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,40 @@
import { promises as fs } from 'fs';
import TOML from '@iarna/toml';
import { buildCacheImageWithCargo, buildImage } from './common';
import { executeCommand } from '../executeCommand';
const createDockerfile = async (data, image, name): Promise<void> => {
const { workdir, port, applicationId, tag, buildId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target target`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`);
Dockerfile.push(`COPY . .`);
Dockerfile.push(`RUN cargo build --release --bin ${name}`);
Dockerfile.push('FROM debian:buster-slim');
Dockerfile.push('WORKDIR /app');
Dockerfile.push(
`RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*`
);
Dockerfile.push(`RUN update-ca-certificates`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target/release/${name} ${name}`);
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ["/app/${name}"]`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { workdir, baseImage, baseBuildImage } = data;
const { stdout: cargoToml } = await executeCommand({ command: `cat ${workdir}/Cargo.toml` });
const parsedToml: any = TOML.parse(cargoToml);
const name = parsedToml.package.name;
await buildCacheImageWithCargo(data, baseBuildImage);
await createDockerfile(data, baseImage, name);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,54 @@
import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {
applicationId,
tag,
workdir,
buildCommand,
baseDirectory,
publishDirectory,
secrets,
pullmergeRequestId,
baseImage,
buildId,
port
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
if (baseImage?.includes('httpd')) {
Dockerfile.push('WORKDIR /usr/local/apache2/htdocs/');
} else {
Dockerfile.push('WORKDIR /app');
}
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
Dockerfile.push(env);
});
}
if (buildCommand) {
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
} else {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
}
if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { baseImage, baseBuildImage } = data;
if (data.buildCommand) await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,28 @@
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { baseImage, baseBuildImage } = data;
await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,28 @@
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const { baseImage, baseBuildImage } = data;
await buildCacheImageWithNode(data, baseBuildImage);
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {
throw error;
}
}

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