Compare commits

...

2 Commits

Author SHA1 Message Date
Andras Bacsai
b4c836afbd v1.0.23 (#68)
# Features 
- Build environment variables for NodeJS builds
- Initial monorepo support (more tests needed!)

# Fixes
- Fix wrong redirects
- Logout fix for the session manager
2021-07-16 23:42:14 +02:00
Andras Bacsai
2d0f22b379 v1.0.22 (#67) 2021-06-24 23:31:08 +02:00
48 changed files with 972 additions and 744 deletions

View File

@@ -54,7 +54,6 @@ With Github integration
- [VSCode Server](https://github.com/cdr/code-server) - [VSCode Server](https://github.com/cdr/code-server)
- [MinIO](https://min.io) - [MinIO](https://min.io)
## Support ## Support
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai) - Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)

View File

@@ -1,7 +1,7 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.", "description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
"version": "1.0.21", "version": "1.0.23",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev:docker:start": "docker-compose -f docker-compose-dev.yml up -d", "dev:docker:start": "docker-compose -f docker-compose-dev.yml up -d",
@@ -14,8 +14,8 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.26", "@sveltejs/adapter-node": "1.0.0-next.33",
"@sveltejs/kit": "1.0.0-next.115", "@sveltejs/kit": "1.0.0-next.125",
"@types/dockerode": "^3.2.3", "@types/dockerode": "^3.2.3",
"@typescript-eslint/eslint-plugin": "^4.26.1", "@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1", "@typescript-eslint/parser": "^4.26.1",
@@ -30,7 +30,7 @@
"prettier-plugin-svelte": "^2.3.0", "prettier-plugin-svelte": "^2.3.0",
"svelte": "^3.38.2", "svelte": "^3.38.2",
"svelte-preprocess": "^4.7.3", "svelte-preprocess": "^4.7.3",
"tailwindcss": "2.2.0", "tailwindcss": "2.2.4",
"tslib": "^2.2.0", "tslib": "^2.2.0",
"typescript": "^4.3.2", "typescript": "^4.3.2",
"vite": "^2.3.6" "vite": "^2.3.6"
@@ -50,6 +50,7 @@
"generate-password": "^1.6.0", "generate-password": "^1.6.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"microtip": "^0.2.2",
"mongoose": "^5.12.13", "mongoose": "^5.12.13",
"shelljs": "^0.8.4", "shelljs": "^0.8.4",
"svelte-kit-cookie-session": "^1.0.6", "svelte-kit-cookie-session": "^1.0.6",

130
pnpm-lock.yaml generated
View File

@@ -2,8 +2,8 @@ lockfileVersion: 5.3
specifiers: specifiers:
'@iarna/toml': ^2.2.5 '@iarna/toml': ^2.2.5
'@sveltejs/adapter-node': 1.0.0-next.26 '@sveltejs/adapter-node': 1.0.0-next.33
'@sveltejs/kit': 1.0.0-next.115 '@sveltejs/kit': 1.0.0-next.125
'@types/dockerode': ^3.2.3 '@types/dockerode': ^3.2.3
'@typescript-eslint/eslint-plugin': ^4.26.1 '@typescript-eslint/eslint-plugin': ^4.26.1
'@typescript-eslint/parser': ^4.26.1 '@typescript-eslint/parser': ^4.26.1
@@ -24,6 +24,7 @@ specifiers:
generate-password: ^1.6.0 generate-password: ^1.6.0
js-yaml: ^4.1.0 js-yaml: ^4.1.0
jsonwebtoken: ^8.5.1 jsonwebtoken: ^8.5.1
microtip: ^0.2.2
mongoose: ^5.12.13 mongoose: ^5.12.13
postcss: ^8.3.0 postcss: ^8.3.0
postcss-load-config: ^3.0.1 postcss-load-config: ^3.0.1
@@ -35,7 +36,7 @@ specifiers:
svelte-preprocess: ^4.7.3 svelte-preprocess: ^4.7.3
svelte-select: ^3.17.0 svelte-select: ^3.17.0
systeminformation: ^5.7.7 systeminformation: ^5.7.7
tailwindcss: 2.2.0 tailwindcss: 2.2.4
tslib: ^2.2.0 tslib: ^2.2.0
typescript: ^4.3.2 typescript: ^4.3.2
unique-names-generator: ^4.5.0 unique-names-generator: ^4.5.0
@@ -55,6 +56,7 @@ dependencies:
generate-password: 1.6.0 generate-password: 1.6.0
js-yaml: 4.1.0 js-yaml: 4.1.0
jsonwebtoken: 8.5.1 jsonwebtoken: 8.5.1
microtip: 0.2.2
mongoose: 5.12.13 mongoose: 5.12.13
shelljs: 0.8.4 shelljs: 0.8.4
svelte-kit-cookie-session: 1.0.6 svelte-kit-cookie-session: 1.0.6
@@ -63,8 +65,8 @@ dependencies:
unique-names-generator: 4.5.0 unique-names-generator: 4.5.0
devDependencies: devDependencies:
'@sveltejs/adapter-node': 1.0.0-next.26 '@sveltejs/adapter-node': 1.0.0-next.33
'@sveltejs/kit': 1.0.0-next.115_svelte@3.38.2 '@sveltejs/kit': 1.0.0-next.125_svelte@3.38.2
'@types/dockerode': 3.2.3 '@types/dockerode': 3.2.3
'@typescript-eslint/eslint-plugin': 4.26.1_c8cbd5e7f5f92609ec78d991aced454b '@typescript-eslint/eslint-plugin': 4.26.1_c8cbd5e7f5f92609ec78d991aced454b
'@typescript-eslint/parser': 4.26.1_eslint@7.28.0+typescript@4.3.2 '@typescript-eslint/parser': 4.26.1_eslint@7.28.0+typescript@4.3.2
@@ -79,7 +81,7 @@ devDependencies:
prettier-plugin-svelte: 2.3.0_prettier@2.3.1+svelte@3.38.2 prettier-plugin-svelte: 2.3.0_prettier@2.3.1+svelte@3.38.2
svelte: 3.38.2 svelte: 3.38.2
svelte-preprocess: 4.7.3_ddfd8490a44ef0de606159ff3aef985a svelte-preprocess: 4.7.3_ddfd8490a44ef0de606159ff3aef985a
tailwindcss: 2.2.0_6daa0ece57b4377652e73c9c66c3b94c tailwindcss: 2.2.4_6daa0ece57b4377652e73c9c66c3b94c
tslib: 2.2.0 tslib: 2.2.0
typescript: 4.3.2 typescript: 4.3.2
vite: 2.3.6 vite: 2.3.6
@@ -200,46 +202,48 @@ packages:
rollup: ^1.20.0||^2.0.0 rollup: ^1.20.0||^2.0.0
dependencies: dependencies:
estree-walker: 2.0.2 estree-walker: 2.0.2
picomatch: 2.2.3 picomatch: 2.3.0
dev: true dev: true
/@sveltejs/adapter-node/1.0.0-next.26: /@sveltejs/adapter-node/1.0.0-next.33:
resolution: {integrity: sha512-jhlCBJH/Gyge0u5sx67UgEEHzoBk69Z9pXXtvERXrDyfmbao5v55zEaroPucc089CHKQvLRQJ3mGU81QZmk31Q==} resolution: {integrity: sha512-0mABbAWQ5Dg7eSptvpX+fmZ0tgifscgNwgtg4iX6GRhWGizTYYBgp4yAHl4IXPqlAQkW7Lh0gw3+G2tQcs2qcQ==}
dependencies: dependencies:
esbuild: 0.12.6 esbuild: 0.12.6
tiny-glob: 0.2.9
dev: true dev: true
/@sveltejs/kit/1.0.0-next.115_svelte@3.38.2: /@sveltejs/kit/1.0.0-next.125_svelte@3.38.2:
resolution: {integrity: sha512-XbkL9hI7LI0Dfum9crtBIeENqdJEjMfDaX3qQTTgdYkhlb4KNNYErmDWo8bJNJKEfdV0BJBVN/I0wiat3cd/yA==} resolution: {integrity: sha512-msYbZak97rW2Cgk08NiJW5Sz4S6XnVcjwTXrMo1G4MkYn8cZeL/eKWhuYrjGFvv4U3bYOZhTj/WfAzXNVEFX0A==}
engines: {node: ^12.20 || ^14.13.1 || >= 16} engines: {node: ^12.20 || >=14.13}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
svelte: ^3.38.2 svelte: ^3.34.0
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 1.0.0-next.11_svelte@3.38.2+vite@2.3.7 '@sveltejs/vite-plugin-svelte': 1.0.0-next.12_svelte@3.38.2+vite@2.4.2
cheap-watch: 1.0.3 cheap-watch: 1.0.3
sade: 1.7.4 sade: 1.7.4
svelte: 3.38.2 svelte: 3.38.2
vite: 2.3.7 vite: 2.4.2
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- supports-color - supports-color
dev: true dev: true
/@sveltejs/vite-plugin-svelte/1.0.0-next.11_svelte@3.38.2+vite@2.3.7: /@sveltejs/vite-plugin-svelte/1.0.0-next.12_svelte@3.38.2+vite@2.4.2:
resolution: {integrity: sha512-EYR1I145k5rflVqhPwk3442m3bkYimTKSHM9uO5KdomXzt+GS9ZSBJQE3/wy1Di9V8OnGa3oKpckI3OZsHkTIA==} resolution: {integrity: sha512-cuyNkJ6leptfv+7qL/fWQ7EpGWdguosFOUI0z93oQUmFTcX7QxJ5h+QI3NQyktBzlKL/761L8BbG2hHNkVbLIQ==}
engines: {node: ^12.20 || ^14.13.1 || >= 16} engines: {node: ^12.20 || ^14.13.1 || >= 16}
peerDependencies: peerDependencies:
svelte: ^3.38.2 svelte: ^3.34.0
vite: ^2.3.7 vite: ^2.3.7
dependencies: dependencies:
'@rollup/pluginutils': 4.1.0 '@rollup/pluginutils': 4.1.0
chalk: 4.1.1
debug: 4.3.2 debug: 4.3.2
kleur: 4.1.4
magic-string: 0.25.7
require-relative: 0.8.7 require-relative: 0.8.7
svelte: 3.38.2 svelte: 3.38.2
svelte-hmr: 0.14.4_svelte@3.38.2 svelte-hmr: 0.14.5_svelte@3.38.2
vite: 2.3.7 vite: 2.4.2
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- supports-color - supports-color
@@ -1146,6 +1150,12 @@ packages:
is-arrayish: 0.2.1 is-arrayish: 0.2.1
dev: true dev: true
/esbuild/0.12.15:
resolution: {integrity: sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==}
hasBin: true
requiresBuild: true
dev: true
/esbuild/0.12.6: /esbuild/0.12.6:
resolution: {integrity: sha512-RDvVLvAjsq/kIZJoneMiUOH7EE7t2QaW7T3Q7EdQij14+bZbDq5sndb0tTanmHIFSqZVMBMMyqzVHkS3dJobeA==} resolution: {integrity: sha512-RDvVLvAjsq/kIZJoneMiUOH7EE7t2QaW7T3Q7EdQij14+bZbDq5sndb0tTanmHIFSqZVMBMMyqzVHkS3dJobeA==}
hasBin: true hasBin: true
@@ -1487,6 +1497,10 @@ packages:
type-fest: 0.20.2 type-fest: 0.20.2
dev: true dev: true
/globalyzer/0.1.0:
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
dev: true
/globby/11.0.3: /globby/11.0.3:
resolution: {integrity: sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==} resolution: {integrity: sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1499,6 +1513,10 @@ packages:
slash: 3.0.0 slash: 3.0.0
dev: true dev: true
/globrex/0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
dev: true
/graceful-fs/4.2.6: /graceful-fs/4.2.6:
resolution: {integrity: sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==} resolution: {integrity: sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==}
dev: true dev: true
@@ -1759,6 +1777,11 @@ packages:
resolution: {integrity: sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==} resolution: {integrity: sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==}
dev: false dev: false
/kleur/4.1.4:
resolution: {integrity: sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==}
engines: {node: '>=6'}
dev: true
/levn/0.4.1: /levn/0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -1767,6 +1790,11 @@ packages:
type-check: 0.4.0 type-check: 0.4.0
dev: true dev: true
/lilconfig/2.0.3:
resolution: {integrity: sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==}
engines: {node: '>=10'}
dev: true
/lines-and-columns/1.1.6: /lines-and-columns/1.1.6:
resolution: {integrity: sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=} resolution: {integrity: sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=}
dev: true dev: true
@@ -1841,6 +1869,12 @@ packages:
dependencies: dependencies:
yallist: 4.0.0 yallist: 4.0.0
/magic-string/0.25.7:
resolution: {integrity: sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==}
dependencies:
sourcemap-codec: 1.4.8
dev: true
/make-dir/3.1.0: /make-dir/3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1870,6 +1904,10 @@ packages:
picomatch: 2.3.0 picomatch: 2.3.0
dev: true dev: true
/microtip/0.2.2:
resolution: {integrity: sha512-oah38eH5vSHVFP6yXjbKWOYt92mav++0j3zh844h1vhOscqEg7Rf4agDEIwUTFCcAPPdlhYUQMe6eZfHgD+4oQ==}
dev: false
/min-indent/1.0.1: /min-indent/1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -2153,11 +2191,6 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/picomatch/2.2.3:
resolution: {integrity: sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==}
engines: {node: '>=8.6'}
dev: true
/picomatch/2.3.0: /picomatch/2.3.0:
resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==} resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
@@ -2248,6 +2281,20 @@ packages:
import-cwd: 3.0.0 import-cwd: 3.0.0
dev: true dev: true
/postcss-load-config/3.1.0:
resolution: {integrity: sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==}
engines: {node: '>= 10'}
peerDependencies:
ts-node: '>=9.0.0'
peerDependenciesMeta:
ts-node:
optional: true
dependencies:
import-cwd: 3.0.0
lilconfig: 2.0.3
yaml: 1.10.2
dev: true
/postcss-merge-longhand/5.0.2_postcss@8.3.0: /postcss-merge-longhand/5.0.2_postcss@8.3.0:
resolution: {integrity: sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw==} resolution: {integrity: sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw==}
engines: {node: ^10 || ^12 || >=14.0} engines: {node: ^10 || ^12 || >=14.0}
@@ -2806,6 +2853,10 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/sourcemap-codec/1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
dev: true
/sparse-bitfield/3.0.3: /sparse-bitfield/3.0.3:
resolution: {integrity: sha1-/0rm5oZWBWuks+eSqzM004JzyhE=} resolution: {integrity: sha1-/0rm5oZWBWuks+eSqzM004JzyhE=}
dependencies: dependencies:
@@ -2927,8 +2978,8 @@ packages:
has-flag: 4.0.0 has-flag: 4.0.0
dev: true dev: true
/svelte-hmr/0.14.4_svelte@3.38.2: /svelte-hmr/0.14.5_svelte@3.38.2:
resolution: {integrity: sha512-kItFF7vqzStckSigoFmMnxJpTOdB9TWnQAW6Js+yAB4277tLbJIIE5KBlGHNmJNpA7MguqidsPB27Uw5UzQPCA==} resolution: {integrity: sha512-3O+kkbT1XKAomKB0LRcdY8JUTzONoNZ8rSH4iEdG7piIYsw+KkXpTkbbU1Sc1yPY4onfXkmCrHElYsxr0V1Snw==}
peerDependencies: peerDependencies:
svelte: '>=3.19.0' svelte: '>=3.19.0'
dependencies: dependencies:
@@ -3035,8 +3086,8 @@ packages:
strip-ansi: 6.0.0 strip-ansi: 6.0.0
dev: true dev: true
/tailwindcss/2.2.0_6daa0ece57b4377652e73c9c66c3b94c: /tailwindcss/2.2.4_6daa0ece57b4377652e73c9c66c3b94c:
resolution: {integrity: sha512-vzyictuac60cUfky6R4gFW98glcc/UxpaCH+Mt9dq+LEPdZq2Dpvo5iYpPaemutOIjfeiY0Y8j0ZgJG3wBaFDQ==} resolution: {integrity: sha512-OdBCPgazNNsknSP+JfrPzkay9aqKjhKtFhbhgxHgvEFdHy/GuRPo2SCJ4w1SFTN8H6FPI4m6qD/Jj20NWY1GkA==}
engines: {node: '>=12.13.0'} engines: {node: '>=12.13.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -3067,7 +3118,7 @@ packages:
object-hash: 2.2.0 object-hash: 2.2.0
postcss: 8.3.0 postcss: 8.3.0
postcss-js: 3.0.3 postcss-js: 3.0.3
postcss-load-config: 3.0.1 postcss-load-config: 3.1.0
postcss-nested: 5.0.5_postcss@8.3.0 postcss-nested: 5.0.5_postcss@8.3.0
postcss-selector-parser: 6.0.6 postcss-selector-parser: 6.0.6
postcss-value-parser: 4.1.0 postcss-value-parser: 4.1.0
@@ -3076,6 +3127,8 @@ packages:
reduce-css-calc: 2.1.8 reduce-css-calc: 2.1.8
resolve: 1.20.0 resolve: 1.20.0
tmp: 0.2.1 tmp: 0.2.1
transitivePeerDependencies:
- ts-node
dev: true dev: true
/tar-fs/2.0.1: /tar-fs/2.0.1:
@@ -3118,6 +3171,13 @@ packages:
resolution: {integrity: sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=} resolution: {integrity: sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=}
dev: true dev: true
/tiny-glob/0.2.9:
resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}
dependencies:
globalyzer: 0.1.0
globrex: 0.1.2
dev: true
/tmp/0.2.1: /tmp/0.2.1:
resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==}
engines: {node: '>=8.17.0'} engines: {node: '>=8.17.0'}
@@ -3222,13 +3282,13 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/vite/2.3.7: /vite/2.4.2:
resolution: {integrity: sha512-Y0xRz11MPYu/EAvzN94+FsOZHbSvO6FUvHv127CyG7mV6oDoay2bw+g5y9wW3Blf8OY3chaz3nc/DcRe1IQ3Nw==} resolution: {integrity: sha512-2MifxD2I9fjyDmmEzbULOo3kOUoqX90A58cT6mECxoVQlMYFuijZsPQBuA14mqSwvV3ydUsqnq+BRWXyO9Qa+w==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
hasBin: true hasBin: true
dependencies: dependencies:
esbuild: 0.12.6 esbuild: 0.12.15
postcss: 8.3.0 postcss: 8.3.5
resolve: 1.20.0 resolve: 1.20.0
rollup: 2.46.0 rollup: 2.46.0
optionalDependencies: optionalDependencies:

View File

@@ -8,7 +8,6 @@
<link rel="dns-prefetch" href="https://cdn.coollabs.io/" /> <link rel="dns-prefetch" href="https://cdn.coollabs.io/" />
<link rel="preconnect" href="https://cdn.coollabs.io/" crossorigin="" /> <link rel="preconnect" href="https://cdn.coollabs.io/" crossorigin="" />
<link rel="stylesheet" href="https://cdn.coollabs.io/fonts/montserrat/montserrat.css" /> <link rel="stylesheet" href="https://cdn.coollabs.io/fonts/montserrat/montserrat.css" />
<link rel="stylesheet" href="https://cdn.coollabs.io/css/microtip-0.2.2.min.css" />
%svelte.head% %svelte.head%
</head> </head>
<body> <body>

View File

@@ -1,6 +1,6 @@
<script> <script>
import { VITE_GITHUB_APP_NAME } from '$lib/consts'; import { VITE_GITHUB_APP_NAME } from '$lib/consts';
import { application, isPullRequestPermissionsGranted } from '$store'; import { application, isPullRequestPermissionsGranted, originalDomain } from '$store';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import TooltipInfo from '$components/TooltipInfo.svelte'; import TooltipInfo from '$components/TooltipInfo.svelte';
import { request } from '$lib/request'; import { request } from '$lib/request';
@@ -122,7 +122,7 @@
async function setPreviewDeployment() { async function setPreviewDeployment() {
if ($application.general.isPreviewDeploymentEnabled) { if ($application.general.isPreviewDeploymentEnabled) {
const result = window.confirm( const result = window.confirm(
"Are you sure? It will delete all PR deployments - it's NOT reversible!" "DANGER ZONE! It will delete all PR deployments. It's NOT reversible! Are you sure?"
); );
if (result) { if (result) {
loading.previewDeployment = true; loading.previewDeployment = true;
@@ -194,9 +194,12 @@
} }
} }
onMount(() => { onMount(() => {
if (!$application.publish.domain) domainInput.focus(); if (!$application.publish.domain) {
domainInput.focus();
} else {
$originalDomain = $application.publish.domain;
}
}); });
</script> </script>
<div> <div>
@@ -378,7 +381,9 @@
</span> </span>
</button> </button>
{#if loading.previewDeployment} {#if loading.previewDeployment}
<div class="absolute left-0 bottom-0 -mb-4 -ml-2 text-xs font-bold">{$application.general.isPreviewDeploymentEnabled ? 'Enabling...' : 'Disabling...' }</div> <div class="absolute left-0 bottom-0 -mb-4 -ml-2 text-xs font-bold">
{$application.general.isPreviewDeploymentEnabled ? 'Enabling...' : 'Disabling...'}
</div>
{/if} {/if}
</div> </div>
{:else} {:else}
@@ -438,6 +443,10 @@
<input <input
bind:this={domainInput} bind:this={domainInput}
class="border-2" class="border-2"
disabled={$page.path !== '/application/new'}
class:cursor-not-allowed={$page.path !== '/application/new'}
class:bg-warmGray-900={$page.path !== '/application/new'}
class:hover:bg-warmGray-900={$page.path !== '/application/new'}
class:placeholder-red-500={$application.publish.domain == null || class:placeholder-red-500={$application.publish.domain == null ||
$application.publish.domain == ''} $application.publish.domain == ''}
class:border-red-500={$application.publish.domain == null || class:border-red-500={$application.publish.domain == null ||
@@ -455,7 +464,15 @@
}/api`} }/api`}
/></label /></label
> >
<input id="Path" bind:value={$application.publish.path} placeholder="/" /> <input
id="Path"
bind:value={$application.publish.path}
disabled={$page.path !== '/application/new'}
class:cursor-not-allowed={$page.path !== '/application/new'}
class:bg-warmGray-900={$page.path !== '/application/new'}
class:hover:bg-warmGray-900={$page.path !== '/application/new'}
placeholder="/"
/>
</div> </div>
</div> </div>
<label for="Port" class:text-warmGray-800={!buildpacks[$application.build.pack].port.active} <label for="Port" class:text-warmGray-800={!buildpacks[$application.build.pack].port.active}
@@ -590,5 +607,4 @@
.buildpack { .buildpack {
@apply px-6 py-2 mx-2 my-2 bg-warmGray-800 w-48 ease-in-out hover:scale-105 text-center rounded border-2 border-transparent border-dashed cursor-pointer transition duration-100; @apply px-6 py-2 mx-2 my-2 bg-warmGray-800 w-48 ease-in-out hover:scale-105 text-center rounded border-2 border-transparent border-dashed cursor-pointer transition duration-100;
} }
</style> </style>

View File

@@ -19,37 +19,29 @@
async function getPRDeployments() { async function getPRDeployments() {
const { configuration } = await request(`/api/v1/application/config`, $session, { const { configuration } = await request(`/api/v1/application/config`, $session, {
body: { body: {
name: $application.repository.name, nickname: $application.general.nickname
organization: $application.repository.organization,
branch: $application.repository.branch
} }
}); });
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0); $prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
} }
async function removePR(prConfiguration) { async function removePR(prConfiguration) {
const result = window.confirm("Are you sure? It's NOT reversible!"); const result = window.confirm("DANGER ZONE! It's NOT reversible! Are you sure?");
if (result) { if (result) {
await request(`/api/v1/application/remove`, $session, { await request(`/api/v1/application/remove`, $session, {
body: { body: {
organization: prConfiguration.repository.organization, nickname: prConfiguration.general.nickname
name: prConfiguration.repository.name,
branch: prConfiguration.repository.branch,
domain: prConfiguration.publish.domain
} }
}); });
browser && toast.push('PR deployment removed.'); browser && toast.push('PR deployment removed.');
const { configuration } = await request(`/api/v1/application/config`, $session, { const { configuration } = await request(`/api/v1/application/config`, $session, {
body: { body: {
name: prConfiguration.repository.name, nickname: prConfiguration.general.nickname
organization: prConfiguration.repository.organization,
branch: prConfiguration.repository.branch
} }
}); });
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0); $prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
} }
} }
</script> </script>
<div class="text-2xl font-bold border-gradient w-48">Pull Requests</div> <div class="text-2xl font-bold border-gradient w-48">Pull Requests</div>

View File

@@ -1,80 +1,128 @@
<script> <script>
import { application } from "$store"; import { application } from '$store';
import BuildEnv from '../BuildEnv.svelte';
let secret = { let secret = {
name: null, name: null,
value: null, value: null,
}; isBuild: false
let foundSecret = null; };
async function saveSecret() { let foundSecret = null;
if (secret.name && secret.value) { async function saveSecret() {
const found = $application.publish.secrets.find( if (secret.name && secret.value) {
s => s.name === secret.name, const found = $application.publish.secrets.find((s) => s.name === secret.name);
); if (!found) {
if (!found) { $application.publish.secrets = [
$application.publish.secrets = [ ...$application.publish.secrets,
...$application.publish.secrets, {
{ name: secret.name,
name: secret.name, value: secret.value,
value: secret.value, isBuild: secret.isBuild
}, }
]; ];
secret = { secret = {
name: null, name: null,
value: null, value: null,
}; isBuild: false
} else { };
foundSecret = found; } else {
} foundSecret = found;
} }
} }
}
async function removeSecret(name) { async function removeSecret(name) {
foundSecret = null foundSecret = null;
$application.publish.secrets = [ $application.publish.secrets = [...$application.publish.secrets.filter((s) => s.name !== name)];
...$application.publish.secrets.filter(s => s.name !== name), }
];
}
</script> </script>
<div class="text-2xl font-bold border-gradient w-24">Secrets</div>
<div class="max-w-xl mx-auto text-center pt-4">
<div class="text-left text-base font-bold tracking-tight text-warmGray-400">
New Secret
</div>
<div class="flex space-x-4">
<input id="secretName" bind:value="{secret.name}" placeholder="Name" class="w-64 border-2 border-transparent" />
<input id="secretValue" bind:value="{secret.value}" placeholder="Value" class="w-64 border-2 border-transparent" />
<button class="icon hover:text-green-500" on:click="{saveSecret}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
{#if $application.publish.secrets.length > 0}
<div class="py-4">
{#each $application.publish.secrets as s}
<div class="flex space-x-4">
<input
id="{s.name}"
value="{s.name}"
disabled
class="border-2 bg-transparent border-transparent w-64"
class:border-red-600="{foundSecret && foundSecret.name === s.name}"
/>
<input
id="{s.createdAt}"
value="SAVED"
disabled
class="border-2 bg-transparent border-transparent w-64"
/>
<button class="icon hover:text-red-500" on:click="{() => removeSecret(s.name)}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div> <div class="text-2xl font-bold border-gradient w-24">Secrets</div>
{/each} <div class="max-w-3xl mx-auto text-center pt-4">
</div> <div class="flex space-x-4">
{/if} <div class="grid grid-flow-row">
<label for="secretName">Secret Name</label>
<input
id="secretName"
bind:value={secret.name}
placeholder="Name"
class="w-64 border-2 border-transparent"
/>
</div>
<div class="grid grid-flow-row">
<label for="secretValue">Secret Value</label>
<input
id="secretValue"
bind:value={secret.value}
placeholder="Value"
class="w-64 border-2 border-transparent"
/>
</div>
<div class="grid grid-flow-row">
<label for="buildVariable">Is build variable?</label>
<div class="mt-2 w-full">
<BuildEnv {secret} />
</div>
</div>
<div class="mt-6">
<button class="icon hover:text-green-500" on:click={saveSecret}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
</button>
</div>
</div>
{#if $application.publish.secrets.length > 0}
<div class="pt-1">
{#each $application.publish.secrets as secret}
<div class="flex space-x-4 space-y-2">
<input
id={secret.name}
value={secret.name}
disabled
class="border-2 bg-transparent border-transparent w-64 hover:bg-transparent"
class:border-red-600={foundSecret && foundSecret.name === secret.name}
/>
<input
id={secret.createdAt}
value="SAVED"
disabled
class="border-2 bg-transparent border-transparent w-64 hover:bg-transparent"
/>
<div class="flex justify-center items-center px-12">
<BuildEnv {secret} readOnly />
</div>
<button class="icon hover:text-red-500" on:click={() => removeSecret(secret.name)}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
{/each}
</div>
{/if}
</div> </div>

View File

@@ -0,0 +1,55 @@
<script>
export let secret;
export let readOnly = false;
function isBuildSet() {
if (!readOnly) secret.isBuild = !secret.isBuild;
}
</script>
<button
id="buildVariable"
type="button"
aria-pressed="false"
on:click={isBuildSet}
class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-100"
class:bg-green-600={secret.isBuild}
class:bg-warmGray-700={!secret.isBuild}
class:opacity-50={readOnly}
class:cursor-not-allowed={readOnly}
>
<span class="sr-only">Use setting</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transition ease-in-out duration-200 transform"
class:translate-x-5={secret.isBuild}
class:translate-x-0={!secret.isBuild}
>
<span
class=" ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
class:opacity-0={secret.isBuild}
class:opacity-100={!secret.isBuild}
aria-hidden="true"
>
<svg class="bg-white h-3 w-3 text-red-600" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
class="ease-out duration-100 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
aria-hidden="true"
class:opacity-100={secret.isBuild}
class:opacity-0={!secret.isBuild}
>
<svg class="bg-white h-3 w-3 text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</button>

View File

@@ -19,6 +19,7 @@
import Tabs from '$components/Application/Tabs.svelte'; import Tabs from '$components/Application/Tabs.svelte';
import Repositories from '$components/Application/Repositories.svelte'; import Repositories from '$components/Application/Repositories.svelte';
import Login from '$components/Application/Login.svelte'; import Login from '$components/Application/Login.svelte';
import { dashify } from '$lib/common';
let loading = { let loading = {
github: false, github: false,
branches: false branches: false
@@ -26,15 +27,7 @@
let branches = []; let branches = [];
let relogin = false; let relogin = false;
let permissions = {}; let permissions = {};
function dashify(str: string, options?: any) {
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();
}
async function getGithubRepos(id, page) { async function getGithubRepos(id, page) {
return await request( return await request(
`https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`, `https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`,
@@ -174,7 +167,6 @@
}, 100); }, 100);
} }
} }
</script> </script>
<div in:fade={{ duration: 100 }}> <div in:fade={{ duration: 100 }}>

View File

@@ -1,5 +1,5 @@
<script> <script>
import { application, initialApplication, initConf } from '$store'; import { application, initialApplication, initConf, originalDomain } from '$store';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import Tooltip from '$components/Tooltip.svelte'; import Tooltip from '$components/Tooltip.svelte';
@@ -9,15 +9,12 @@
import { browser } from '$app/env'; import { browser } from '$app/env';
async function removeApplication() { async function removeApplication() {
const result = window.confirm( const result = window.confirm(
"Are you sure? It will delete all deployments, including PR's - it's NOT reversible!" "DANGER ZONE! It will delete all deployments, including PR's. It's NOT reversible! Are you sure?"
); );
if (result) { if (result) {
await request(`/api/v1/application/remove`, $session, { await request(`/api/v1/application/remove`, $session, {
body: { body: {
organization: $application.repository.organization, nickname: $application.general.nickname
name: $application.repository.name,
branch: $application.repository.branch,
domain: $application.publish.domain
} }
}); });
@@ -46,16 +43,14 @@
$initConf = JSON.parse(JSON.stringify($application)); $initConf = JSON.parse(JSON.stringify($application));
if (browser) { if (browser) {
toast.push('Application deployment queued.'); toast.push('Application deployment queued.');
goto( goto(`/application/${$application.general.nickname}/logs/${$application.general.deployId}`, {
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs/${$application.general.deployId}`, replaceState: true
{ replaceState: true } });
);
} }
} catch (error) { } catch (error) {
browser && toast.push(error.error || error || 'Ooops something went wrong.'); // browser && toast.push(error.error || error || 'Ooops something went wrong.');
} }
} }
</script> </script>
<nav class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4 z-50"> <nav class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4 z-50">
@@ -131,10 +126,7 @@
class:cursor-not-allowed={$page.path === '/application/new'} class:cursor-not-allowed={$page.path === '/application/new'}
class:text-blue-400={/logs\/*/.test($page.path)} class:text-blue-400={/logs\/*/.test($page.path)}
class:bg-warmGray-700={/logs\/*/.test($page.path)} class:bg-warmGray-700={/logs\/*/.test($page.path)}
on:click={() => on:click={() => goto(`/application/${$application.general.nickname}/logs`)}
goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`
)}
> >
<svg <svg
class="w-6" class="w-6"
@@ -160,10 +152,7 @@
$page.path === '/application/new'} $page.path === '/application/new'}
class:bg-warmGray-700={$page.path.endsWith('configuration') || class:bg-warmGray-700={$page.path.endsWith('configuration') ||
$page.path === '/application/new'} $page.path === '/application/new'}
on:click={() => on:click={() => goto(`/application/${$application.general.nickname}/configuration`)}
goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`
)}
> >
<svg <svg
class="w-6" class="w-6"

View File

@@ -27,28 +27,6 @@
} }
} }
async function load() { async function load() {
const found = $dashboard?.applications?.deployed.find((deployment) => {
if (
deployment.configuration.repository.organization === $application.repository.organization &&
deployment.configuration.repository.name === $application.repository.name &&
deployment.configuration.repository.branch === $application.repository.branch
) {
return deployment;
}
});
if (found) {
$application = { ...found.configuration };
if ($page.path === '/application/new') {
if (browser) {
toast.push('This repository & branch is already defined. Redirecting...');
goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
{ replaceState: true }
);
}
}
return;
}
if ($page.path === '/application/new') { if ($page.path === '/application/new') {
try { try {
const dir = await request( const dir = await request(

View File

@@ -101,7 +101,7 @@ export async function handle({ request, resolve }) {
cookie: { path: '/', secure: true } cookie: { path: '/', secure: true }
}); });
} catch (error) { } catch (error) {
console.log(error) console.log(error);
return { return {
status: 302, status: 302,
headers: { headers: {
@@ -124,7 +124,7 @@ export async function handle({ request, resolve }) {
if (!session['set-cookie']) { if (!session['set-cookie']) {
if (!session?.data?.coolToken && !publicPages.includes(request.path)) { if (!session?.data?.coolToken && !publicPages.includes(request.path)) {
return { return {
status: 301, status: 302,
headers: { headers: {
location: '/' location: '/'
} }

View File

@@ -1,8 +1,8 @@
import { docker } from '$lib/api/docker'; import { docker } from '$lib/api/docker';
import Deployment from '$models/Deployment'; import Deployment from '$models/Deployment';
import { execShellAsync } from '../common'; import { execShellAsync } from '../common';
import crypto from 'crypto';
export async function deleteSameDeployments(configuration) { export async function deleteSameDeployments(configuration, originalDomain = null) {
await ( await (
await docker.engine.listServices() await docker.engine.listServices()
) )
@@ -12,7 +12,7 @@ export async function deleteSameDeployments(configuration) {
if ( if (
running.repository.id === configuration.repository.id && running.repository.id === configuration.repository.id &&
running.repository.branch === configuration.repository.branch && running.repository.branch === configuration.repository.branch &&
running.publish.domain === configuration.publish.domain running.publish.domain === originalDomain || configuration.publish.domain
) { ) {
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`); await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`);
} }

View File

@@ -4,7 +4,8 @@ import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-
import { docker } from '$lib/api/docker'; import { docker } from '$lib/api/docker';
import { baseServiceConfiguration } from './common'; import { baseServiceConfiguration } from './common';
import { execShellAsync } from '../common'; import { execShellAsync } from '../common';
import { promises as fs } from 'fs';
import Configuration from '$models/Configuration';
function getUniq() { function getUniq() {
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 }); return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 });
} }
@@ -12,7 +13,7 @@ function getUniq() {
export function setDefaultConfiguration(configuration) { export function setDefaultConfiguration(configuration) {
const nickname = configuration.general.nickname || getUniq(); const nickname = configuration.general.nickname || getUniq();
const deployId = cuid(); const deployId = cuid();
const shaBase = JSON.stringify({ repository: configuration.repository }); const shaBase = JSON.stringify({ path: configuration.publish.path, domain: configuration.publish.domain });
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex'); const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex');
configuration.build.container.name = sha256.slice(0, 15); configuration.build.container.name = sha256.slice(0, 15);
@@ -51,7 +52,7 @@ export function setDefaultConfiguration(configuration) {
if (configuration.publish.directory.startsWith('/')) if (configuration.publish.directory.startsWith('/'))
configuration.publish.directory = configuration.publish.directory.replace('/', ''); configuration.publish.directory = configuration.publish.directory.replace('/', '');
if (configuration.build.pack === 'static' || configuration.build.pack === 'nodejs') { if (configuration.build.pack === 'nodejs') {
if (!configuration.build.command.installation) if (!configuration.build.command.installation)
configuration.build.command.installation = 'yarn install'; configuration.build.command.installation = 'yarn install';
} }
@@ -81,34 +82,37 @@ export function setDefaultConfiguration(configuration) {
} }
export async function precheckDeployment(configuration) { export async function precheckDeployment(configuration) {
const services = (await docker.engine.listServices()).filter( const services = await Configuration.find({
(r) => 'publish.domain': configuration.publish.domain,
r.Spec.Labels.managedBy === 'coolify' && 'publish.path': configuration.publish.path
r.Spec.Labels.type === 'application' && })
JSON.parse(r.Spec.Labels.configuration).publish.domain === configuration.publish.domain // const services = (await docker.engine.listServices()).filter(
); // (r) =>
// r.Spec.Labels.managedBy === 'coolify' &&
// r.Spec.Labels.type === 'application' &&
// JSON.parse(r.Spec.Labels.configuration).publish.domain === configuration.publish.domain
// );
let foundService = false; let foundService = false;
let configChanged = false; let configChanged = false;
let imageChanged = false; let imageChanged = false;
let forceUpdate = false; let forceUpdate = false;
for (const service of services) { for (const service of services) {
const running = JSON.parse(service.Spec.Labels.configuration); // const running = JSON.parse(service.Spec.Labels.configuration);
if (running) {
if ( if (
running.repository.id === configuration.repository.id && service.repository.id === configuration.repository.id &&
running.repository.branch === configuration.repository.branch service.repository.branch === configuration.repository.branch
) { ) {
foundService = true; foundService = true;
// Base service configuration changed // Base service configuration changed
if ( if (
!running.build.container.baseSHA || !service.build.container.baseSHA ||
running.build.container.baseSHA !== configuration.build.container.baseSHA service.build.container.baseSHA !== configuration.build.container.baseSHA
) { ) {
forceUpdate = true; forceUpdate = true;
} }
// If the deployment is in error state, forceUpdate // If the deployment is in error state, forceUpdate
const state = await execShellAsync( const state = await execShellAsync(
`docker stack ps ${running.build.container.name} --format '{{ json . }}'` `docker stack ps ${service.build.container.name} --format '{{ json . }}'`
); );
const isError = state const isError = state
.split('\n') .split('\n')
@@ -116,7 +120,7 @@ export async function precheckDeployment(configuration) {
.map((s) => JSON.parse(s)) .map((s) => JSON.parse(s))
.filter( .filter(
(n) => (n) =>
n.DesiredState !== 'Running' && n.Image.split(':')[1] === running.build.container.tag n.DesiredState !== 'Running' && n.Image.split(':')[1] === service.build.container.tag
); );
if (isError.length > 0) { if (isError.length > 0) {
forceUpdate = true; forceUpdate = true;
@@ -145,7 +149,7 @@ export async function precheckDeployment(configuration) {
return true; return true;
}; };
const runningWithoutContainer = JSON.parse(JSON.stringify(running)); const runningWithoutContainer = JSON.parse(JSON.stringify(service));
delete runningWithoutContainer.build.container; delete runningWithoutContainer.build.container;
const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration)); const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration));
@@ -162,16 +166,16 @@ export async function precheckDeployment(configuration) {
} }
// If only the image changed // If only the image changed
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true; if (service.build.container.tag !== configuration.build.container.tag) imageChanged = true;
// If build pack changed, forceUpdate the service // If build pack changed, forceUpdate the service
if (running.build.pack !== configuration.build.pack) forceUpdate = true; if (service.build.pack !== configuration.build.pack) forceUpdate = true;
if ( if (
configuration.general.isPreviewDeploymentEnabled && configuration.general.isPreviewDeploymentEnabled &&
configuration.general.pullRequest !== 0 configuration.general.pullRequest !== 0
) )
forceUpdate = true; forceUpdate = true;
} }
}
} }
if (forceUpdate) { if (forceUpdate) {
imageChanged = false; imageChanged = false;

View File

@@ -5,7 +5,7 @@ import { deleteSameDeployments, purgeImagesContainers } from './cleanup';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { delay, execShellAsync } from '../common'; import { delay, execShellAsync } from '../common';
export default async function (configuration, imageChanged) { export default async function (configuration, nextStep) {
const generateEnvs = {}; const generateEnvs = {};
for (const secret of configuration.publish.secrets) { for (const secret of configuration.publish.secrets) {
generateEnvs[secret.name] = secret.value; generateEnvs[secret.name] = secret.value;
@@ -56,23 +56,25 @@ export default async function (configuration, imageChanged) {
}; };
await saveAppLog('### Publishing.', configuration); await saveAppLog('### Publishing.', configuration);
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack)); await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack));
if (imageChanged) { if (nextStep === 2) {
// console.log('image changed') // console.log('image changed')
await execShellAsync( await execShellAsync(
`docker service update --image ${containerName}:${containerTag} ${containerName}_${containerName}` `docker service update --image ${containerName}:${containerTag} ${containerName}_${containerName}`
); );
} else { } else {
// console.log('new deployment or force deployment or config changed') // console.log('new deployment or force deployment or config changed')
await deleteSameDeployments(configuration); // if (originalDomain !== configuration.publish.domain) {
// await deleteSameDeployments(configuration, originalDomain);
// } else {
// await deleteSameDeployments(configuration);
// }
// await deleteSameDeployments(configuration);
await execShellAsync( await execShellAsync(
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy --prune -c - ${containerName}` `cat ${configuration.general.workdir}/stack.yml | docker stack deploy --prune -c - ${containerName}`
); );
} }
async function purgeImagesAsync(found) {
await delay(10000);
await purgeImagesContainers(found, true);
}
purgeImagesAsync(configuration);
await saveAppLog('### Published done!', configuration); await saveAppLog('### Published done!', configuration);
} }

View File

@@ -1,9 +1,10 @@
import { docker, streamEvents } from '$lib/api/docker'; import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
const buildImageNodeDocker = (configuration, prodBuild) => { const buildImageNodeDocker = (configuration, prodBuild, generateEnvs) => {
return [ return [
'FROM node:lts', 'FROM node:lts',
...generateEnvs,
'WORKDIR /usr/src/app', 'WORKDIR /usr/src/app',
`COPY ${configuration.build.directory}/package*.json ./`, `COPY ${configuration.build.directory}/package*.json ./`,
configuration.build.command.installation && `RUN ${configuration.build.command.installation}`, configuration.build.command.installation && `RUN ${configuration.build.command.installation}`,
@@ -13,18 +14,29 @@ const buildImageNodeDocker = (configuration, prodBuild) => {
].join('\n'); ].join('\n');
}; };
export async function buildImage(configuration, cacheBuild?: boolean, prodBuild?: boolean) { export async function buildImage(configuration, cacheBuild?: boolean, prodBuild?: boolean) {
// TODO: Edit secrets
// TODO: Add secret from .env file / json
const generateEnvs = [];
const dotEnv = []
for (const secret of configuration.publish.secrets) {
dotEnv.push(`${secret.name}=${secret.value}`)
if (secret.isBuild) generateEnvs.push(`ENV ${secret.name}=${secret.value}`)
}
await fs.writeFile(
`${configuration.general.workdir}/.env`,
dotEnv.join('\n')
)
await fs.writeFile( await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`, `${configuration.general.workdir}/Dockerfile`,
buildImageNodeDocker(configuration, prodBuild) buildImageNodeDocker(configuration, prodBuild, generateEnvs)
); );
const stream = await docker.engine.buildImage( const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir }, { src: ['.'], context: configuration.general.workdir },
{ {
t: `${configuration.build.container.name}:${ t: `${configuration.build.container.name}:${cacheBuild
cacheBuild ? `${configuration.build.container.tag}-cache`
? `${configuration.build.container.tag}-cache` : configuration.build.container.tag
: configuration.build.container.tag }`
}`
} }
); );
await streamEvents(stream, configuration); await streamEvents(stream, configuration);

View File

@@ -0,0 +1,76 @@
import Configuration from "$models/Configuration";
import { compareObjects, execShellAsync } from "../common";
export default async function (configuration) {
/*
0 => nothing changed, no need to redeploy
1 => force update
2 => configuration changed
3 => continue normally
*/
const currentConfiguration = await Configuration.findOne({
'general.nickname': configuration.general.nickname
})
if (currentConfiguration) {
// Base service configuration changed
if (
!currentConfiguration.build.container.baseSHA ||
currentConfiguration.build.container.baseSHA !== configuration.build.container.baseSHA
) {
return 1
}
// If the deployment is in error state, forceUpdate
try {
const state = await execShellAsync(
`docker stack ps ${currentConfiguration.build.container.name} --format '{{ json . }}'`
);
const isError = state
.split('\n')
.filter((n) => n)
.map((s) => JSON.parse(s))
.filter(
(n) =>
n.DesiredState !== 'Running' && n.Image.split(':')[1] === currentConfiguration.build.container.tag
);
if (isError.length > 0) {
return 1
}
} catch(error) {
console.log(error)
}
// If previewDeployments enabled
if (
currentConfiguration.general.isPreviewDeploymentEnabled &&
currentConfiguration.general.pullRequest !== 0
) {
return 1
}
// If build pack changed, forceUpdate the service
if (currentConfiguration.build.pack !== configuration.build.pack) {
return 1
}
const currentConfigurationCompare = JSON.parse(JSON.stringify(currentConfiguration));
const configurationCompare = JSON.parse(JSON.stringify(configuration));
delete currentConfigurationCompare.build.container;
delete configurationCompare.build.container;
if (
!compareObjects(currentConfigurationCompare.build, configurationCompare.build) ||
!compareObjects(currentConfigurationCompare.publish, configurationCompare.publish) ||
currentConfigurationCompare.general.isPreviewDeploymentEnabled !==
configurationCompare.general.isPreviewDeploymentEnabled
) {
return 1
}
if (currentConfiguration.build.container.tag !== configuration.build.container.tag) {
return 2
}
return 0
}
return 3
}

View File

@@ -0,0 +1,44 @@
import Configuration from "$models/Configuration";
import Deployment from "$models/Deployment";
export default async function (configuration) {
// Check if deployment is already queued
const alreadyQueued = await Deployment.find({
path: configuration.publish.path,
domain: configuration.publish.domain,
progress: { $in: ['queued', 'inprogress'] }
});
if (alreadyQueued.length > 0) {
return {
status: 200,
body: {
success: false,
message: 'Deployment already queued.'
}
};
}
const { id, organization, name, branch } = configuration.repository;
const { domain, path } = configuration.publish;
const { deployId, nickname } = configuration.general;
// Save new deployment
await new Deployment({
repoId: id,
branch,
deployId,
domain,
organization,
name,
nickname
}).save();
await Configuration.findOneAndUpdate(
{
'publish.domain': domain,
'publish.path': path,
'general.pullRequest': { $in: [null, 0] }
},
{ ...configuration },
{ upsert: true, new: true }
);
return
}

View File

@@ -1,26 +1,28 @@
import Deployment from '$models/Deployment'; import Deployment from '$models/Deployment';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import buildContainer from './buildContainer'; import buildContainer from './buildContainer';
import { purgeImagesContainers } from './cleanup';
import { updateServiceLabels } from './configuration'; import { updateServiceLabels } from './configuration';
import copyFiles from './copyFiles'; import copyFiles from './copyFiles';
import deploy from './deploy'; import deploy from './deploy';
import { saveAppLog } from './logging'; import { saveAppLog } from './logging';
export default async function (configuration, imageChanged) { export default async function (configuration, nextStep) {
const { id, organization, name, branch } = configuration.repository; const { id, organization, name, branch } = configuration.repository;
const { domain } = configuration.publish; const { domain } = configuration.publish;
const { deployId } = configuration.general; const { deployId } = configuration.general;
try { try {
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration); await saveAppLog(`### Successfully queued.`, configuration);
await copyFiles(configuration); await copyFiles(configuration);
await buildContainer(configuration); await buildContainer(configuration);
await deploy(configuration, imageChanged); await deploy(configuration, nextStep);
await Deployment.findOneAndUpdate( await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain }, { repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' } { repoId: id, branch, deployId, organization, name, domain, progress: 'done' }
); );
await updateServiceLabels(configuration); await updateServiceLabels(configuration);
await purgeImagesContainers(configuration);
} catch (error) { } catch (error) {
await Deployment.findOneAndUpdate( await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain }, { repoId: id, branch, deployId, organization, name, domain },

View File

@@ -46,3 +46,27 @@ export function delay(t) {
}, t); }, t);
}); });
} }
export function compareObjects(a, b) {
if (a === b) return true;
if (typeof a != 'object' || typeof b != 'object' || a == null || b == null) return false;
const keysA = Object.keys(a),
keysB = Object.keys(b);
if (keysA.length != keysB.length) return false;
for (const key of keysA) {
if (!keysB.includes(key)) return false;
if (typeof a[key] === 'function' || typeof b[key] === 'function') {
if (a[key].toString() != b[key].toString()) return false;
} else {
if (!compareObjects(a[key], b[key])) return false;
}
}
return true;
};

9
src/lib/common.ts Normal file
View File

@@ -0,0 +1,9 @@
export function dashify(str: string, options?: any) {
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();
}

View File

@@ -1,2 +1,8 @@
export const publicPages = ['/', '/api/v1/login/github/app', '/api/v1/webhooks/deploy', '/success', '/api/v1/login/email'] export const publicPages = [
export const VITE_GITHUB_APP_NAME = import.meta.env.VITE_GITHUB_APP_NAME '/',
'/api/v1/login/github/app',
'/api/v1/webhooks/deploy',
'/success',
'/api/v1/login/email'
];
export const VITE_GITHUB_APP_NAME = import.meta.env.VITE_GITHUB_APP_NAME;

View File

@@ -45,7 +45,11 @@ const ConfigurationSchema = new Schema({
domain: { type: String, required: true }, domain: { type: String, required: true },
path: { type: String }, path: { type: String },
port: { type: Number }, port: { type: Number },
secrets: { type: Array } secrets: [{
name: { type: String },
value: { type: String },
isBuild: { type: Boolean, default: false },
}]
} }
}); });

View File

@@ -9,7 +9,7 @@
if (!publicPages.includes(path)) { if (!publicPages.includes(path)) {
if (!session.session.isLoggedIn) { if (!session.session.isLoggedIn) {
return { return {
status: 301, status: 302,
redirect: '/' redirect: '/'
}; };
} }
@@ -17,7 +17,7 @@
} }
if (!publicPages.includes(path)) { if (!publicPages.includes(path)) {
return { return {
status: 301, status: 302,
redirect: '/' redirect: '/'
}; };
} }
@@ -26,6 +26,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import 'microtip/microtip.css';
import '../app.postcss'; import '../app.postcss';
export let initDashboard; export let initDashboard;
import { onMount } from 'svelte'; import { onMount } from 'svelte';

View File

@@ -1,6 +1,5 @@
import { setDefaultConfiguration } from '$lib/api/applications/configuration'; import { setDefaultConfiguration } from '$lib/api/applications/configuration';
import { saveServerLog } from '$lib/api/applications/logging'; import { saveServerLog } from '$lib/api/applications/logging';
import { docker } from '$lib/api/docker';
import Configuration from '$models/Configuration'; import Configuration from '$models/Configuration';
import type { Request } from '@sveltejs/kit'; import type { Request } from '@sveltejs/kit';
@@ -8,16 +7,16 @@ export async function post(request: Request) {
try { try {
const { DOMAIN } = process.env; const { DOMAIN } = process.env;
const configuration = setDefaultConfiguration(request.body); const configuration = setDefaultConfiguration(request.body);
const configurationFound = await Configuration.find({ const sameDomainAndPath = await Configuration.find({
'repository.id': { $ne: configuration.repository.id }, 'publish.path': configuration.publish.path,
'publish.domain': configuration.publish.domain 'publish.domain': configuration.publish.domain
}).select('-_id -__v -createdAt -updatedAt'); }).select('-_id -__v -createdAt -updatedAt');
if (configurationFound.length > 0 || configuration.publish.domain === DOMAIN) { if (sameDomainAndPath.length > 1 || configuration.publish.domain === DOMAIN) {
return { return {
status: 200, status: 200,
body: { body: {
success: false, success: false,
message: 'Domain already in use.' message: 'Domain/path are already in use.'
} }
}; };
} }

View File

@@ -3,14 +3,11 @@ import Configuration from '$models/Configuration';
import type { Request } from '@sveltejs/kit'; import type { Request } from '@sveltejs/kit';
export async function post(request: Request) { export async function post(request: Request) {
const { name, organization, branch }: any = request.body || {}; const { nickname }: any = request.body || {};
if (name && organization && branch) { if (nickname) {
const configurationFound = await Configuration.find({ const configurationFound = await Configuration.find({
'repository.name': name, 'general.nickname': nickname
'repository.organization': organization,
'repository.branch': branch
}).select('-_id -__v -createdAt -updatedAt'); }).select('-_id -__v -createdAt -updatedAt');
if (configurationFound) { if (configurationFound) {
return { return {
status: 200, status: 200,
@@ -28,22 +25,8 @@ export async function post(request: Request) {
const configuration = r.Spec.Labels.configuration const configuration = r.Spec.Labels.configuration
? JSON.parse(r.Spec.Labels.configuration) ? JSON.parse(r.Spec.Labels.configuration)
: null; : null;
if (branch) {
if ( if (configuration.general.nickname === nickname) return r;
configuration.repository.name === name &&
configuration.repository.organization === organization &&
configuration.repository.branch === branch
) {
return r;
}
} else {
if (
configuration.repository.name === name &&
configuration.repository.organization === organization
) {
return r;
}
}
return null; return null;
}); });

View File

@@ -5,6 +5,8 @@ import cloneRepository from '$lib/api/applications/cloneRepository';
import { cleanupTmp } from '$lib/api/common'; import { cleanupTmp } from '$lib/api/common';
import queueAndBuild from '$lib/api/applications/queueAndBuild'; import queueAndBuild from '$lib/api/applications/queueAndBuild';
import Configuration from '$models/Configuration'; import Configuration from '$models/Configuration';
import preChecks from '$lib/api/applications/preChecks';
import preTasks from '$lib/api/applications/preTasks';
export async function post(request: Request) { export async function post(request: Request) {
const configuration = setDefaultConfiguration(request.body); const configuration = setDefaultConfiguration(request.body);
@@ -18,10 +20,8 @@ export async function post(request: Request) {
} }
try { try {
await cloneRepository(configuration); await cloneRepository(configuration);
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment( const nextStep = await preChecks(configuration);
configuration if (nextStep === 0) {
);
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
cleanupTmp(configuration.general.workdir); cleanupTmp(configuration.general.workdir);
return { return {
status: 200, status: 200,
@@ -31,50 +31,9 @@ export async function post(request: Request) {
} }
}; };
} }
const alreadyQueued = await Deployment.find({ await preTasks(configuration)
repoId: configuration.repository.id,
branch: configuration.repository.branch,
organization: configuration.repository.organization,
name: configuration.repository.name,
domain: configuration.publish.domain,
progress: { $in: ['queued', 'inprogress'] }
});
if (alreadyQueued.length > 0) {
return {
status: 200,
body: {
success: false,
message: 'Already in the queue.'
}
};
}
const { id, organization, name, branch } = configuration.repository;
const { domain } = configuration.publish;
const { deployId, nickname, pullRequest } = configuration.general;
await new Deployment({ queueAndBuild(configuration, nextStep);
repoId: id,
branch,
deployId,
domain,
organization,
name,
nickname
}).save();
await Configuration.findOneAndUpdate(
{
'repository.id': id,
'repository.organization': organization,
'repository.name': name,
'repository.branch': branch,
'general.pullRequest': { $in: [null, 0] }
},
{ ...configuration },
{ upsert: true, new: true }
);
queueAndBuild(configuration, imageChanged);
return { return {
status: 201, status: 201,
body: { body: {
@@ -86,23 +45,7 @@ export async function post(request: Request) {
}; };
} catch (error) { } catch (error) {
console.log(error); console.log(error);
await Deployment.findOneAndUpdate( await Deployment.findOneAndUpdate({ nickname: configuration.general.nickname }, { $set: { progress: 'failed' } });
{
repoId: configuration.repository.id,
branch: configuration.repository.branch,
organization: configuration.repository.organization,
name: configuration.repository.name,
domain: configuration.publish.domain
},
{
repoId: configuration.repository.id,
branch: configuration.repository.branch,
organization: configuration.repository.organization,
name: configuration.repository.name,
domain: configuration.publish.domain,
progress: 'failed'
}
);
return { return {
status: 500, status: 500,
body: { body: {

View File

@@ -4,63 +4,47 @@ import ApplicationLog from '$models/ApplicationLog';
import { delay, execShellAsync } from '$lib/api/common'; import { delay, execShellAsync } from '$lib/api/common';
import Configuration from '$models/Configuration'; import Configuration from '$models/Configuration';
async function purgeImagesAsync(found) {
await delay(10000);
await purgeImagesContainers(found, true);
}
export async function post(request: Request) { export async function post(request: Request) {
const { organization, name, branch, domain } = request.body; const { nickname } = request.body;
try { try {
const configurationFound = await Configuration.findOne({ const configurationFound = await Configuration.findOne({
'repository.organization': organization, 'general.nickname': nickname
'repository.name': name,
'repository.branch': branch,
'publish.domain': domain
}); });
if (configurationFound) { if (configurationFound) {
const id = configurationFound._id; const id = configurationFound._id;
if (configurationFound?.general?.pullRequest === 0) { if (configurationFound?.general?.pullRequest === 0) {
// Main deployment deletion request; deleting main + PRs // Main deployment deletion request; deleting main + PRs
const allConfiguration = await Configuration.find({ const allConfiguration = await Configuration.find({
'repository.name': name, 'publish.domain': { $regex: `.*${configurationFound.publish.domain}`, $options: 'i' },
'repository.organization': organization, 'publish.path': configurationFound.publish.path
'repository.branch': branch
}); });
for (const config of allConfiguration) { for (const config of allConfiguration) {
await Configuration.findOneAndRemove({
'repository.name': config.repository.name,
'repository.organization': config.repository.organization,
'repository.branch': config.repository.branch
});
await execShellAsync(`docker stack rm ${config.build.container.name}`); await execShellAsync(`docker stack rm ${config.build.container.name}`);
} }
const deploys = await Deployment.find({ organization, branch, name }); await Configuration.deleteMany({
'publish.domain': { $regex: `.*${configurationFound.publish.domain}`, $options: 'i' },
'publish.path': configurationFound.publish.path
});
const deploys = await Deployment.find({ nickname });
for (const deploy of deploys) { for (const deploy of deploys) {
await ApplicationLog.deleteMany({ deployId: deploy.deployId }); await ApplicationLog.deleteMany({ deployId: deploy.deployId });
await Deployment.deleteMany({ deployId: deploy.deployId }); await Deployment.deleteMany({ deployId: deploy.deployId });
} }
purgeImagesAsync(configurationFound);
} else { } else {
// Delete only PRs // Delete only PRs
await Configuration.findByIdAndRemove(id); await Configuration.findByIdAndRemove(id);
await execShellAsync(`docker stack rm ${configurationFound.build.container.name}`); await execShellAsync(`docker stack rm ${configurationFound.build.container.name}`);
const deploys = await Deployment.find({ organization, branch, name, domain }); const deploys = await Deployment.find({ nickname });
for (const deploy of deploys) { for (const deploy of deploys) {
await ApplicationLog.deleteMany({ deployId: deploy.deployId }); await ApplicationLog.deleteMany({ deployId: deploy.deployId });
await Deployment.deleteMany({ deployId: deploy.deployId }); await Deployment.deleteMany({ deployId: deploy.deployId });
} }
purgeImagesAsync(configurationFound);
} }
} }
return { return {
status: 200, status: 200,
body: { body: {}
organization,
name,
branch
}
}; };
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -9,97 +9,96 @@ import type { Request } from '@sveltejs/kit';
const saltRounds = 15; const saltRounds = 15;
export async function post(request: Request) { export async function post(request: Request) {
const { email, password } = request.body const { email, password } = request.body;
const { JWT_SIGN_KEY } = process.env; const { JWT_SIGN_KEY } = process.env;
const settings = await Settings.findOne({ applicationName: 'coolify' }); const settings = await Settings.findOne({ applicationName: 'coolify' });
const registeredUsers = await User.find().countDocuments(); const registeredUsers = await User.find().countDocuments();
const foundUser = await User.findOne({ email }); const foundUser = await User.findOne({ email });
try { try {
let uid = cuid(); let uid = cuid();
if (foundUser) { if (foundUser) {
if (foundUser.type === 'github') { if (foundUser.type === 'github') {
return { return {
status: 500, status: 500,
body: { body: {
error: 'Wrong password or email address.' error: 'Wrong password or email address.'
} }
}; };
} }
uid = foundUser.uid; uid = foundUser.uid;
if (!await bcrypt.compare(password, foundUser.password)) { if (!(await bcrypt.compare(password, foundUser.password))) {
return { return {
status: 500, status: 500,
body: { body: {
error: 'Wrong password or email address.' error: 'Wrong password or email address.'
} }
}; };
} }
} else { } else {
if (registeredUsers === 0) { if (registeredUsers === 0) {
const newUser = new User({ const newUser = new User({
_id: new mongoose.Types.ObjectId(), _id: new mongoose.Types.ObjectId(),
email, email,
uid, uid,
type: 'email', type: 'email',
password: await bcrypt.hash(password, saltRounds) password: await bcrypt.hash(password, saltRounds)
}); });
const defaultSettings = new Settings({ const defaultSettings = new Settings({
_id: new mongoose.Types.ObjectId() _id: new mongoose.Types.ObjectId()
}); });
try { try {
await newUser.save(); await newUser.save();
await defaultSettings.save(); await defaultSettings.save();
} catch (error) { } catch (error) {
return { return {
status: 500, status: 500,
error: error.message || error error: error.message || error
}; };
} }
} else { } else {
if (!settings?.allowRegistration) { if (!settings?.allowRegistration) {
return { return {
status: 500, status: 500,
body: { body: {
error: 'Registration disabled, enable it in settings.' error: 'Registration disabled, enable it in settings.'
} }
}; };
} else { } else {
const newUser = new User({ const newUser = new User({
_id: new mongoose.Types.ObjectId(), _id: new mongoose.Types.ObjectId(),
email, email,
uid, uid,
type: 'email', type: 'email',
password: await bcrypt.hash(password, saltRounds) password: await bcrypt.hash(password, saltRounds)
}); });
try { try {
await newUser.save(); await newUser.save();
} catch (error) { } catch (error) {
return { return {
status: 500, status: 500,
error: error.message || error error: error.message || error
}; };
} }
} }
} }
} }
const coolToken = jsonwebtoken.sign({}, JWT_SIGN_KEY, { const coolToken = jsonwebtoken.sign({}, JWT_SIGN_KEY, {
expiresIn: 15778800, expiresIn: 15778800,
algorithm: 'HS256', algorithm: 'HS256',
audience: 'coolLabs', audience: 'coolLabs',
issuer: 'coolLabs', issuer: 'coolLabs',
jwtid: uid, jwtid: uid,
subject: `User:${uid}`, subject: `User:${uid}`,
notBefore: -1000 notBefore: -1000
}); });
request.locals.session.data = { coolToken, ghToken: null }; request.locals.session.data = { coolToken, ghToken: null };
return { return {
status: 200, status: 200,
body: { body: {
message: "Successfully logged in." message: 'Successfully logged in.'
} }
}; };
} catch (error) { } catch (error) {
return { status: 500, body: { error: error.message || error } }; return { status: 500, body: { error: error.message || error } };
} }
}
}

View File

@@ -1,7 +1,6 @@
import type { Request } from '@sveltejs/kit'; import type { Request } from '@sveltejs/kit';
export async function del(request: Request) { export async function del(request: Request) {
request.locals.session.destroy = true; request.locals.session.destroy()
return { return {
body: { body: {
ok: true ok: true

View File

@@ -2,24 +2,26 @@ import { saveServerLog } from '$lib/api/applications/logging';
import { execShellAsync } from '$lib/api/common'; import { execShellAsync } from '$lib/api/common';
import type { Request } from '@sveltejs/kit'; import type { Request } from '@sveltejs/kit';
export async function post(request: Request) { export async function post(request: Request) {
try { try {
const output = await execShellAsync('docker builder prune -af') const output = await execShellAsync('docker builder prune -af');
return { return {
status: 200, status: 200,
body: { body: {
message: 'OK', message: 'OK',
output: output.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm,"").split('\n').pop() output: output
} .replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '')
} .split('\n')
} catch (error) { .pop()
await saveServerLog(error); }
return { };
status: 500, } catch (error) {
body: { await saveServerLog(error);
error: error.message || error return {
} status: 500,
}; body: {
} error: error.message || error
}
};
}
} }

View File

@@ -2,24 +2,26 @@ import { saveServerLog } from '$lib/api/applications/logging';
import { execShellAsync } from '$lib/api/common'; import { execShellAsync } from '$lib/api/common';
import type { Request } from '@sveltejs/kit'; import type { Request } from '@sveltejs/kit';
export async function post(request: Request) { export async function post(request: Request) {
try { try {
const output = await execShellAsync('docker container prune -f') const output = await execShellAsync('docker container prune -f');
return { return {
status: 200, status: 200,
body: { body: {
message: 'OK', message: 'OK',
output: output.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm,"").split('\n').pop() output: output
} .replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '')
} .split('\n')
} catch (error) { .pop()
await saveServerLog(error); }
return { };
status: 500, } catch (error) {
body: { await saveServerLog(error);
error: error.message || error return {
} status: 500,
}; body: {
} error: error.message || error
}
};
}
} }

View File

@@ -2,24 +2,26 @@ import { saveServerLog } from '$lib/api/applications/logging';
import { execShellAsync } from '$lib/api/common'; import { execShellAsync } from '$lib/api/common';
import type { Request } from '@sveltejs/kit'; import type { Request } from '@sveltejs/kit';
export async function post(request: Request) { export async function post(request: Request) {
try { try {
const output = await execShellAsync('docker image prune -af') const output = await execShellAsync('docker image prune -af');
return { return {
status: 200, status: 200,
body: { body: {
message: 'OK', message: 'OK',
output: output.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm,"").split('\n').pop() output: output
} .replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '')
} .split('\n')
} catch (error) { .pop()
await saveServerLog(error); }
return { };
status: 500, } catch (error) {
body: { await saveServerLog(error);
error: error.message || error return {
} status: 500,
}; body: {
} error: error.message || error
}
};
}
} }

View File

@@ -2,24 +2,26 @@ import { saveServerLog } from '$lib/api/applications/logging';
import { execShellAsync } from '$lib/api/common'; import { execShellAsync } from '$lib/api/common';
import type { Request } from '@sveltejs/kit'; import type { Request } from '@sveltejs/kit';
export async function post(request: Request) { export async function post(request: Request) {
try { try {
const output = await execShellAsync('docker volume prune -f') const output = await execShellAsync('docker volume prune -f');
return { return {
status: 200, status: 200,
body: { body: {
message: 'OK', message: 'OK',
output: output.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm,"").split('\n').pop() output: output
} .replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '')
} .split('\n')
} catch (error) { .pop()
await saveServerLog(error); }
return { };
status: 500, } catch (error) {
body: { await saveServerLog(error);
error: error.message || error return {
} status: 500,
}; body: {
} error: error.message || error
}
};
}
} }

View File

@@ -2,26 +2,26 @@ import { saveServerLog } from '$lib/api/applications/logging';
import { execShellAsync } from '$lib/api/common'; import { execShellAsync } from '$lib/api/common';
import { docker } from '$lib/api/docker'; import { docker } from '$lib/api/docker';
import type { Request } from '@sveltejs/kit'; import type { Request } from '@sveltejs/kit';
import systeminformation from 'systeminformation' import systeminformation from 'systeminformation';
export async function get(request: Request) { export async function get(request: Request) {
try { try {
const df = await execShellAsync( const df = await execShellAsync(`docker system df --format '{{ json . }}'`);
`docker system df --format '{{ json . }}'`
);
const dockerReclaimable = df const dockerReclaimable = df
.split('\n') .split('\n')
.filter((n) => n) .filter((n) => n)
.map((s) => JSON.parse(s)) .map((s) => JSON.parse(s));
return { return {
status: 200, status: 200,
body: { body: {
hostname: await (await systeminformation.osInfo()).hostname, hostname: await (await systeminformation.osInfo()).hostname,
filesystems: await (await systeminformation.fsSize()).filter(fs => !fs.fs.match('/dev/loop') || !fs.fs.match('/var/lib/docker/')), filesystems: await (
await systeminformation.fsSize()
).filter((fs) => !fs.fs.match('/dev/loop') || !fs.fs.match('/var/lib/docker/')),
dockerReclaimable dockerReclaimable
} }
} };
} catch (error) { } catch (error) {
await saveServerLog(error); await saveServerLog(error);
return { return {

View File

@@ -59,8 +59,8 @@ export async function post(request: Request) {
volumes: { volumes: {
[`${deployId}-code-server-data`]: { [`${deployId}-code-server-data`]: {
external: true external: true
}, }
}, }
}; };
await execShellAsync(`mkdir -p ${workdir}`); await execShellAsync(`mkdir -p ${workdir}`);
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack)); await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));

View File

@@ -1,6 +1,6 @@
import { execShellAsync } from '$lib/api/common'; import { execShellAsync } from '$lib/api/common';
import type { Request } from '@sveltejs/kit'; import type { Request } from '@sveltejs/kit';
import yaml from "js-yaml" import yaml from 'js-yaml';
export async function get(request: Request) { export async function get(request: Request) {
// const { POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD, POSTGRESQL_DATABASE } = JSON.parse( // const { POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD, POSTGRESQL_DATABASE } = JSON.parse(
@@ -15,10 +15,12 @@ export async function get(request: Request) {
.trim() .trim()
.split('\n'); .split('\n');
const codeServer = containers.find((container) => container.startsWith('code-server')); const codeServer = containers.find((container) => container.startsWith('code-server'));
const configYaml = yaml.load(await execShellAsync( const configYaml = yaml.load(
`docker exec ${codeServer} cat /home/coder/.config/code-server/config.yaml` await execShellAsync(
)) `docker exec ${codeServer} cat /home/coder/.config/code-server/config.yaml`
return { )
);
return {
status: 200, status: 200,
body: { message: 'OK', password: configYaml.password } body: { message: 'OK', password: configYaml.password }
}; };

View File

@@ -13,9 +13,14 @@ export async function post(request: Request) {
const workdir = '/tmp/minio'; const workdir = '/tmp/minio';
const deployId = 'minio'; const deployId = 'minio';
const secrets = [ const secrets = [
{ name: 'MINIO_ROOT_USER', value: generator.generate({ length: 12, numbers: true, strict: true }) }, {
{ name: 'MINIO_ROOT_PASSWORD', value: generator.generate({ length: 24, numbers: true, strict: true }) } name: 'MINIO_ROOT_USER',
value: generator.generate({ length: 12, numbers: true, strict: true })
},
{
name: 'MINIO_ROOT_PASSWORD',
value: generator.generate({ length: 24, numbers: true, strict: true })
}
]; ];
const generateEnvsMinIO = {}; const generateEnvsMinIO = {};
for (const secret of secrets) generateEnvsMinIO[secret.name] = secret.value; for (const secret of secrets) generateEnvsMinIO[secret.name] = secret.value;
@@ -36,18 +41,18 @@ export async function post(request: Request) {
'type=service', 'type=service',
'serviceName=minio', 'serviceName=minio',
'configuration=' + 'configuration=' +
JSON.stringify({ JSON.stringify({
baseURL, baseURL,
generateEnvsMinIO generateEnvsMinIO
}), }),
'traefik.enable=true', 'traefik.enable=true',
'traefik.http.services.' + deployId + '.loadbalancer.server.port=9000', 'traefik.http.services.' + deployId + '.loadbalancer.server.port=9000',
'traefik.http.routers.' + deployId + '.entrypoints=websecure', 'traefik.http.routers.' + deployId + '.entrypoints=websecure',
'traefik.http.routers.' + 'traefik.http.routers.' +
deployId + deployId +
'.rule=Host(`' + '.rule=Host(`' +
traefikURL + traefikURL +
'`) && PathPrefix(`/`)', '`) && PathPrefix(`/`)',
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt', 'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
'traefik.http.routers.' + deployId + '.middlewares=global-compress' 'traefik.http.routers.' + deployId + '.middlewares=global-compress'
] ]
@@ -62,8 +67,8 @@ export async function post(request: Request) {
volumes: { volumes: {
[`${deployId}-minio-data`]: { [`${deployId}-minio-data`]: {
external: true external: true
}, }
}, }
}; };
await execShellAsync(`mkdir -p ${workdir}`); await execShellAsync(`mkdir -p ${workdir}`);
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack)); await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));

View File

@@ -7,166 +7,164 @@ import { baseServiceConfiguration } from '$lib/api/applications/common';
import { cleanupTmp, execShellAsync } from '$lib/api/common'; import { cleanupTmp, execShellAsync } from '$lib/api/common';
export async function post(request: Request) { export async function post(request: Request) {
let { baseURL, remoteDB, database, wordpressExtraConfiguration } = request.body; let { baseURL, remoteDB, database, wordpressExtraConfiguration } = request.body;
const traefikURL = baseURL; const traefikURL = baseURL;
baseURL = `https://${baseURL}`; baseURL = `https://${baseURL}`;
console.log({ baseURL, remoteDB, database, wordpressExtraConfiguration }) const workdir = '/tmp/wordpress';
const deployId = `wp-${generator.generate({ length: 5, numbers: true, strict: true })}`;
const defaultDatabaseName = generator.generate({ length: 12, numbers: true, strict: true });
const defaultDatabaseHost = `${deployId}-mysql`;
const defaultDatabaseUser = generator.generate({ length: 12, numbers: true, strict: true });
const defaultDatabasePassword = generator.generate({ length: 24, numbers: true, strict: true });
const defaultDatabaseRootPassword = generator.generate({
length: 24,
numbers: true,
strict: true
});
const defaultDatabaseRootUser = generator.generate({ length: 12, numbers: true, strict: true });
let secrets = [
{ name: 'WORDPRESS_DB_HOST', value: defaultDatabaseHost },
{ name: 'WORDPRESS_DB_USER', value: defaultDatabaseUser },
{ name: 'WORDPRESS_DB_PASSWORD', value: defaultDatabasePassword },
{ name: 'WORDPRESS_DB_NAME', value: defaultDatabaseName },
{ name: 'WORDPRESS_CONFIG_EXTRA', value: wordpressExtraConfiguration }
];
const workdir = '/tmp/wordpress'; const generateEnvsMySQL = {
const deployId = `wp-${generator.generate({ length: 5, numbers: true, strict: true })}` MYSQL_ROOT_PASSWORD: defaultDatabaseRootPassword,
const defaultDatabaseName = generator.generate({ length: 12, numbers: true, strict: true }) MYSQL_ROOT_USER: defaultDatabaseRootUser,
const defaultDatabaseHost = `${deployId}-mysql` MYSQL_USER: defaultDatabaseUser,
const defaultDatabaseUser = generator.generate({ length: 12, numbers: true, strict: true }) MYSQL_PASSWORD: defaultDatabasePassword,
const defaultDatabasePassword = generator.generate({ length: 24, numbers: true, strict: true }) MYSQL_DATABASE: defaultDatabaseName
const defaultDatabaseRootPassword = generator.generate({ length: 24, numbers: true, strict: true }) };
const defaultDatabaseRootUser = generator.generate({ length: 12, numbers: true, strict: true }) const image = 'bitnami/mysql:8.0';
let secrets = [ const volume = `${deployId}-mysql-data:/bitnami/mysql/data`;
{ name: 'WORDPRESS_DB_HOST', value: defaultDatabaseHost },
{ name: 'WORDPRESS_DB_USER', value: defaultDatabaseUser },
{ name: 'WORDPRESS_DB_PASSWORD', value: defaultDatabasePassword },
{ name: 'WORDPRESS_DB_NAME', value: defaultDatabaseName },
{ name: 'WORDPRESS_CONFIG_EXTRA', value: wordpressExtraConfiguration }
];
const generateEnvsMySQL = { if (remoteDB) {
MYSQL_ROOT_PASSWORD: defaultDatabaseRootPassword, secrets = [
MYSQL_ROOT_USER: defaultDatabaseRootUser, { name: 'WORDPRESS_DB_HOST', value: database.host },
MYSQL_USER: defaultDatabaseUser, { name: 'WORDPRESS_DB_USER', value: database.user },
MYSQL_PASSWORD: defaultDatabasePassword, { name: 'WORDPRESS_DB_PASSWORD', value: database.password },
MYSQL_DATABASE: defaultDatabaseName { name: 'WORDPRESS_DB_NAME', value: database.name },
}; { name: 'WORDPRESS_TABLE_PREFIX', value: database.tablePrefix },
const image = 'bitnami/mysql:8.0'; { name: 'WORDPRESS_CONFIG_EXTRA', value: wordpressExtraConfiguration }
const volume = `${deployId}-mysql-data:/bitnami/mysql/data`; ];
}
if (remoteDB) { const generateEnvsWordpress = {};
secrets = [ for (const secret of secrets) generateEnvsWordpress[secret.name] = secret.value;
{ name: 'WORDPRESS_DB_HOST', value: database.host }, let stack = {
{ name: 'WORDPRESS_DB_USER', value: database.user }, version: '3.8',
{ name: 'WORDPRESS_DB_PASSWORD', value: database.password }, services: {
{ name: 'WORDPRESS_DB_NAME', value: database.name }, [deployId]: {
{ name: 'WORDPRESS_TABLE_PREFIX', value: database.tablePrefix }, image: 'wordpress',
{ name: 'WORDPRESS_CONFIG_EXTRA', value: wordpressExtraConfiguration } networks: [`${docker.network}`],
] environment: generateEnvsWordpress,
} volumes: [`${deployId}-wordpress-data:/var/www/html`],
deploy: {
const generateEnvsWordpress = {}; ...baseServiceConfiguration,
for (const secret of secrets) generateEnvsWordpress[secret.name] = secret.value; labels: [
let stack = { 'managedBy=coolify',
version: '3.8', 'type=service',
services: { 'serviceName=' + deployId,
[deployId]: { 'configuration=' +
image: 'wordpress', JSON.stringify({
networks: [`${docker.network}`], deployId,
environment: generateEnvsWordpress, baseURL,
volumes: [`${deployId}-wordpress-data:/var/www/html`], generateEnvsWordpress
deploy: { }),
...baseServiceConfiguration, 'traefik.enable=true',
labels: [ 'traefik.http.services.' + deployId + '.loadbalancer.server.port=80',
'managedBy=coolify', 'traefik.http.routers.' + deployId + '.entrypoints=websecure',
'type=service', 'traefik.http.routers.' +
'serviceName=' + deployId, deployId +
'configuration=' + '.rule=Host(`' +
JSON.stringify({ traefikURL +
deployId, '`) && PathPrefix(`/`)',
baseURL, 'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
generateEnvsWordpress 'traefik.http.routers.' + deployId + '.middlewares=global-compress'
}), ]
'traefik.enable=true', }
'traefik.http.services.' + deployId + '.loadbalancer.server.port=80', },
'traefik.http.routers.' + deployId + '.entrypoints=websecure', [`${deployId}-mysql`]: {
'traefik.http.routers.' + image,
deployId + networks: [`${docker.network}`],
'.rule=Host(`' + environment: generateEnvsMySQL,
traefikURL + volumes: [volume],
'`) && PathPrefix(`/`)', deploy: {
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt', ...baseServiceConfiguration,
'traefik.http.routers.' + deployId + '.middlewares=global-compress' labels: ['managedBy=coolify', 'type=service', 'serviceName=' + deployId]
] }
} }
}, },
[`${deployId}-mysql`]: { networks: {
image, [`${docker.network}`]: {
networks: [`${docker.network}`], external: true
environment: generateEnvsMySQL, }
volumes: [volume], },
deploy: { volumes: {
...baseServiceConfiguration, [`${deployId}-wordpress-data`]: {
labels: [ external: true
'managedBy=coolify', },
'type=service', [`${deployId}-mysql-data`]: {
'serviceName=' + deployId, external: true
] }
} }
} };
}, if (remoteDB) {
networks: { stack = {
[`${docker.network}`]: { version: '3.8',
external: true services: {
} [deployId]: {
}, image: 'wordpress',
volumes: { networks: [`${docker.network}`],
[`${deployId}-wordpress-data`]: { environment: generateEnvsWordpress,
external: true volumes: [`${deployId}-wordpress-data:/var/www/html`],
}, deploy: {
[`${deployId}-mysql-data`]: { ...baseServiceConfiguration,
external: true labels: [
} 'managedBy=coolify',
}, 'type=service',
}; 'serviceName=' + deployId,
if (remoteDB) { 'configuration=' +
stack = { JSON.stringify({
version: '3.8', deployId,
services: { baseURL,
[deployId]: { generateEnvsWordpress
image: 'wordpress', }),
networks: [`${docker.network}`], 'traefik.enable=true',
environment: generateEnvsWordpress, 'traefik.http.services.' + deployId + '.loadbalancer.server.port=80',
volumes: [`${deployId}-wordpress-data:/var/www/html`], 'traefik.http.routers.' + deployId + '.entrypoints=websecure',
deploy: { 'traefik.http.routers.' +
...baseServiceConfiguration, deployId +
labels: [ '.rule=Host(`' +
'managedBy=coolify', traefikURL +
'type=service', '`) && PathPrefix(`/`)',
'serviceName=' + deployId, 'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
'configuration=' + 'traefik.http.routers.' + deployId + '.middlewares=global-compress'
JSON.stringify({ ]
deployId, }
baseURL, }
generateEnvsWordpress },
}), networks: {
'traefik.enable=true', [`${docker.network}`]: {
'traefik.http.services.' + deployId + '.loadbalancer.server.port=80', external: true
'traefik.http.routers.' + deployId + '.entrypoints=websecure', }
'traefik.http.routers.' + },
deployId + volumes: {
'.rule=Host(`' + [`${deployId}-wordpress-data`]: {
traefikURL + external: true
'`) && PathPrefix(`/`)', }
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt', }
'traefik.http.routers.' + deployId + '.middlewares=global-compress' };
] }
} await execShellAsync(`mkdir -p ${workdir}`);
} await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));
}, await execShellAsync(`docker stack rm ${deployId}`);
networks: { await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`);
[`${docker.network}`]: { cleanupTmp(workdir);
external: true return {
} status: 200,
}, body: { message: 'OK' }
volumes: { };
[`${deployId}-wordpress-data`]: {
external: true
}
},
};
}
await execShellAsync(`mkdir -p ${workdir}`);
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));
await execShellAsync(`docker stack rm ${deployId}`);
await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`);
cleanupTmp(workdir);
return {
status: 200,
body: { message: 'OK' }
};
} }

View File

@@ -39,6 +39,7 @@ export async function post(request: Request) {
}; };
} }
// TODO: Monorepo support here. Find all configurations by id and update all deployments! Tough!
try { try {
const applications = await Configuration.find({ const applications = await Configuration.find({
'repository.id': request.body.repository.id 'repository.id': request.body.repository.id

View File

@@ -3,18 +3,20 @@
* @type {import('@sveltejs/kit').Load} * @type {import('@sveltejs/kit').Load}
*/ */
export async function load(session) { export async function load(session) {
if (!browser && !process.env.VITE_GITHUB_APP_CLIENTID) { if (!browser) {
return { if (!import.meta.env.VITE_GITHUB_APP_CLIENTID) {
status: 301, return {
redirect: '/dashboard/services' status: 302,
}; redirect: '/dashboard/services'
};
}
} }
return {}; return {};
} }
</script> </script>
<script> <script>
import { application, initialApplication, initConf, dashboard, prApplication } from '$store'; import { application, initialApplication, initConf, dashboard, prApplication, originalDomain } from '$store';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import Loading from '$components/Loading.svelte'; import Loading from '$components/Loading.svelte';
import Navbar from '$components/Application/Navbar.svelte'; import Navbar from '$components/Application/Navbar.svelte';
@@ -23,17 +25,12 @@
import { browser } from '$app/env'; import { browser } from '$app/env';
import { request } from '$lib/request'; import { request } from '$lib/request';
$application.repository.organization = $page.params.organization; $application.general.nickname = $page.params.nickname;
$application.repository.name = $page.params.name;
$application.repository.branch = $page.params.branch;
async function setConfiguration() { async function setConfiguration() {
try { try {
const { configuration } = await request(`/api/v1/application/config`, $session, { const { configuration } = await request(`/api/v1/application/config`, $session, {
body: { body: {
name: $application.repository.name, nickname: $application.general.nickname
organization: $application.repository.organization,
branch: $application.repository.branch
} }
}); });
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0); $prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
@@ -49,12 +46,8 @@
await setConfiguration(); await setConfiguration();
} else { } else {
const found = $dashboard.applications.deployed.find((app) => { const found = $dashboard.applications.deployed.find((app) => {
const { organization, name, branch } = app.configuration.repository; const { domain } = app.configuration.publish;
if ( if (domain === $application.publish.domain) {
organization === $application.repository.organization &&
name === $application.repository.name &&
branch === $application.repository.branch
) {
return app; return app;
} }
}); });
@@ -65,6 +58,8 @@
await setConfiguration(); await setConfiguration();
} }
} }
$originalDomain = $application.publish.domain
} else { } else {
$application = JSON.parse(JSON.stringify(initialApplication)); $application = JSON.parse(JSON.stringify(initialApplication));
} }

View File

@@ -4,11 +4,13 @@
* @type {import('@sveltejs/kit').Load} * @type {import('@sveltejs/kit').Load}
*/ */
export async function load(session) { export async function load(session) {
if (!browser && !process.env.VITE_GITHUB_APP_CLIENTID) { if (!browser) {
return { if (!import.meta.env.VITE_GITHUB_APP_CLIENTID) {
status: 301, return {
redirect: '/dashboard/services' status: 302,
}; redirect: '/dashboard/services'
};
}
} }
return { return {
props: { props: {
@@ -23,6 +25,7 @@
import { dashboard, dateOptions, settings } from '$store'; import { dashboard, dateOptions, settings } from '$store';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { browser } from '$app/env'; import { browser } from '$app/env';
import { dashify } from '$lib/common';
</script> </script>
<div <div
@@ -59,9 +62,7 @@
<div <div
class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-green-500 text-white shadow-md cursor-pointer ease-in-out hover:scale-105 duration-100 group" class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-green-500 text-white shadow-md cursor-pointer ease-in-out hover:scale-105 duration-100 group"
on:click={() => { on:click={() => {
goto( goto(`/application/${application.configuration.general.nickname}/configuration`);
`/application/${application.configuration.repository.organization}/${application.configuration.repository.name}/${application.configuration.repository.branch}/configuration`
);
}} }}
> >
<div class="flex items-center"> <div class="flex items-center">

View File

@@ -5,7 +5,7 @@
export async function load(session) { export async function load(session) {
if (!browser && !process.env.VITE_GITHUB_APP_CLIENTID) { if (!browser && !process.env.VITE_GITHUB_APP_CLIENTID) {
return { return {
status: 301, status: 302,
redirect: '/dashboard/services' redirect: '/dashboard/services'
}; };
} }

View File

@@ -8,7 +8,7 @@ import type {
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
export const settings = writable({ export const settings = writable({
clientId: null clientId: null
}) });
export const dashboard = writable<Dashboard>({ export const dashboard = writable<Dashboard>({
databases: { databases: {
deployed: [] deployed: []
@@ -32,6 +32,7 @@ export const dateOptions: DateTimeFormatOptions = {
export const githubRepositories = writable([]); export const githubRepositories = writable([]);
export const githubInstallations = writable<GithubInstallations>([]); export const githubInstallations = writable<GithubInstallations>([]);
export const originalDomain = writable(null)
export const application = writable<Application>({ export const application = writable<Application>({
github: { github: {
installation: { installation: {
@@ -81,9 +82,7 @@ export const application = writable<Application>({
} }
}); });
export const prApplication = writable([]); export const prApplication = writable([]);
export const initConf = writable({}); export const initConf = writable({});
export const initialApplication: Application = { export const initialApplication: Application = {
github: { github: {
installation: { installation: {
@@ -181,5 +180,4 @@ export const newWordpressService = writable({
}, },
wordpressExtraConfiguration: null wordpressExtraConfiguration: null
}); });
export const isPullRequestPermissionsGranted = writable(false); export const isPullRequestPermissionsGranted = writable(false);