Compare commits

...

9 Commits

Author SHA1 Message Date
Andras Bacsai
05e5d73556 v1.0.18 (#60)
* Removed nocodb image

* Quick fix for no installations
2021-06-10 11:55:02 +02:00
Andras Bacsai
53620f4b1a Update README.md 2021-06-07 23:57:31 +02:00
Andras Bacsai
9d14b03eb1 v1.0.17 (#59) 2021-06-07 23:44:36 +02:00
Andras Bacsai
04a5b1bd4f Fix for PR 2021-06-07 21:49:52 +02:00
Andras Bacsai
31b3f58b2c v1.0.16 (#51) 2021-06-07 21:33:11 +02:00
Andras Bacsai
9c173d1de0 bump version 2021-05-22 15:33:13 +02:00
Andras Bacsai
e11b6d74ed v1.0.15 (#49)
Webhook quickfix.
2021-05-22 15:28:22 +02:00
Andras Bacsai
c7efe899fa v1.0.14 (#48)
# Features
- Basic Python support

# Fixes
- Fix default start command
2021-05-22 15:18:58 +02:00
Andras Bacsai
adcd68c1ab v1.0.13 (#46) 2021-05-16 21:54:44 +02:00
82 changed files with 3828 additions and 1701 deletions

View File

@@ -1,4 +1,3 @@
# Coolify
An open-source, hassle-free, self-hostable Heroku & Netlify alternative.
@@ -7,8 +6,7 @@ An open-source, hassle-free, self-hostable Heroku & Netlify alternative.
[Small video](https://cdn.coollabs.io/assets/coolify/video/coolify.webm)
## Installation
## Installation
Installation is automated with the following command:
@@ -16,49 +14,54 @@ Installation is automated with the following command:
/bin/bash -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)"
```
## Features
You can deploy any of the following applications, databases and services easily.
- Deploy Pull Request automatically, so you can review them quickly!
- You can deploy any of the following applications, databases and services easily.
(constantly growing lists)
### Applications
With Github integration
- Static sites
- NodeJS
- VueJS
- NuxtJS
- NextJS
- React/Preact
- NextJS
- Gatsby
- Svelte
- PHP
- Rust
- Rust
- or any custom dockerfile
### Databases
- MongoDB
- MySQL
- PostgreSQL
- CouchDB
- Redis
### Services
- [Plausible Analytics](https://plausible.io)
- [Plausible Analytics](https://plausible.io)
- [NocoDB](https://nocodb.com)
## Support
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://discord.com/invite/bvS3WhR)
- Discord: [Invitation](https://discord.gg/xhBCC7eGKw)
## Roadmap
[See the Roadmap here](https://github.com/coollabsio/coolify/projects/1)
## License
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Please see the [LICENSE](/LICENSE) file in our repository for the full text.

View File

@@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
"version": "1.0.12",
"version": "1.0.18",
"license": "AGPL-3.0",
"scripts": {
"dev:docker:start": "docker-compose -f docker-compose-dev.yml up -d",
@@ -14,27 +14,27 @@
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-node": "^1.0.0-next.20",
"@sveltejs/kit": "1.0.0-next.107",
"@sveltejs/adapter-node": "^1.0.0-next.24",
"@sveltejs/kit": "1.0.0-next.113",
"@types/dockerode": "^3.2.3",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"autoprefixer": "^10.2.5",
"cssnano": "^5.0.2",
"@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1",
"autoprefixer": "^10.2.6",
"cssnano": "^5.0.5",
"dotenv-extended": "^2.9.0",
"eslint": "^7.26.0",
"eslint": "^7.28.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^3.2.0",
"postcss": "^8.2.15",
"postcss": "^8.3.0",
"postcss-load-config": "^3.0.1",
"prettier": "~2.3.0",
"prettier": "~2.3.1",
"prettier-plugin-svelte": "^2.3.0",
"svelte": "^3.38.2",
"svelte-preprocess": "^4.7.3",
"tailwindcss": "canary",
"tailwindcss": "2.2.0-canary.8",
"tslib": "^2.2.0",
"typescript": "^4.2.4",
"vite": "^2.3.2"
"typescript": "^4.3.2",
"vite": "^2.3.6"
},
"type": "module",
"dependencies": {
@@ -44,14 +44,14 @@
"compare-versions": "^3.6.0",
"cookie": "^0.4.1",
"cuid": "^2.1.8",
"dayjs": "^1.10.4",
"dayjs": "^1.10.5",
"dockerode": "^3.3.0",
"generate-password": "^1.6.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.12.9",
"mongoose": "^5.12.13",
"shelljs": "^0.8.4",
"svelte-kit-cookie-session": "^0.4.3",
"svelte-kit-cookie-session": "^1.0.6",
"svelte-select": "^3.17.0",
"unique-names-generator": "^4.5.0"
}

2991
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -92,7 +92,9 @@ body {
input {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none border border-transparent !important;
}
input:hover {
input:hover,
input:focus-visible,
input:focus {
@apply bg-warmGray-700 !important;
}
textarea {
@@ -107,6 +109,7 @@ select {
select:hover {
@apply bg-warmGray-700 !important;
}
label {
@apply text-left text-base font-bold text-warmGray-400 !important;
}
@@ -116,6 +119,11 @@ button {
.button {
@apply rounded text-sm font-bold transition-all duration-100 !important;
}
.button:focus-visible,
.button:focus {
@apply bg-warmGray-700 !important;
}
.h-271 {
min-height: 271px !important;
}

View File

@@ -1,87 +1,163 @@
<script>
import { application } from '$store';
import { VITE_GITHUB_APP_NAME } from '$lib/consts';
import { application, isPullRequestPermissionsGranted } from '$store';
import { onMount } from 'svelte';
import TooltipInfo from '$components/TooltipInfo.svelte';
import { request } from '$lib/request';
import { page, session } from '$app/stores';
import { browser } from '$app/env';
let domainInput;
let loading = {
previewDeployment: false
};
let howToActivate = false;
const buildpacks = {
static: {
port: {
active: false,
number: 80
},
build: true
build: true,
start: false
},
nodejs: {
port: {
active: true,
number: 3000
},
build: true
build: true,
start: true
},
nestjs: {
port: {
active: true,
number: 3000
},
build: true,
start: true
},
vuejs: {
port: {
active: false,
number: 80
},
build: true
build: true,
start: false
},
nuxtjs: {
port: {
active: true,
number: 3000
},
build: true
build: true,
start: true
},
react: {
port: {
active: false,
number: 80
},
build: true
build: true,
start: false
},
nextjs: {
port: {
active: true,
number: 3000
},
build: true
build: true,
start: true
},
gatsby: {
port: {
active: true,
number: 3000
},
build: true
build: true,
start: false
},
svelte: {
port: {
active: false,
number: 80
},
build: true
build: true,
start: false
},
php: {
port: {
active: false,
number: 80
},
build: false
build: false,
start: false
},
rust: {
port: {
active: true,
number: 3000
},
build: false
build: false,
start: false
},
docker: {
port: {
active: true,
number: 3000
},
build: false
build: false,
start: false
},
python: {
port: {
active: true,
number: 4000
},
build: false,
start: false,
custom: true
}
};
async function setPreviewDeployment() {
if ($application.general.isPreviewDeploymentEnabled) {
const result = window.confirm(
"Are you sure? It will delete all PR deployments - it's NOT reversible!"
);
if (result) {
loading.previewDeployment = true;
$application.general.isPreviewDeploymentEnabled =
!$application.general.isPreviewDeploymentEnabled;
if ($page.path !== '/application/new') {
const config = await request(`/api/v1/application/config/previewDeployment`, $session, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
isPreviewDeploymentEnabled: $application.general.isPreviewDeploymentEnabled
}
});
}
loading.previewDeployment = false;
}
} else {
loading.previewDeployment = true;
$application.general.isPreviewDeploymentEnabled =
!$application.general.isPreviewDeploymentEnabled;
$application.general.pullRequest = 0;
if ($page.path !== '/application/new') {
const config = await request(`/api/v1/application/config/previewDeployment`, $session, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
isPreviewDeploymentEnabled: $application.general.isPreviewDeploymentEnabled
}
});
}
loading.previewDeployment = false;
}
}
function selectBuildPack(event) {
if (event.target.innerText === 'React/Preact') {
$application.build.pack = 'react';
@@ -89,9 +165,38 @@
$application.build.pack = event.target.innerText.replace(/\./g, '').toLowerCase();
}
}
async function openGithub() {
if (browser) {
const config = await request(`https://api.github.com/apps/${VITE_GITHUB_APP_NAME}`, $session);
let url = `https://github.com/settings/apps/${VITE_GITHUB_APP_NAME}/permissions`;
if (config.owner.type === 'Organization') {
url = `https://github.com/organizations/${config.owner.login}/settings/apps/${VITE_GITHUB_APP_NAME}/permissions`;
}
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
url,
'Permission Update',
'resizable=1, scrollbars=1, fullscreen=1, height=1000, width=1220,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(async () => {
if (newWindow?.closed) {
clearInterval(timer);
location.reload();
}
}, 100);
}
}
onMount(() => {
if(!$application.publish.domain) domainInput.focus();
if (!$application.publish.domain) domainInput.focus();
});
</script>
<div>
@@ -178,6 +283,14 @@
>
Rust
</div>
<div
class={$application.build.pack === 'nestjs'
? 'buildpack bg-red-500'
: 'buildpack hover:border-red-500'}
on:click={selectBuildPack}
>
NestJS
</div>
<div
class={$application.build.pack === 'docker'
? 'buildpack bg-purple-500'
@@ -186,10 +299,139 @@
>
Docker
</div>
<div
class={$application.build.pack === 'python'
? 'buildpack bg-green-500'
: 'buildpack hover:border-green-500'}
on:click={selectBuildPack}
>
Python
</div>
</div>
</div>
<div class="text-2xl font-bold border-gradient w-52">General settings</div>
<div class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10">
<div>
<ul class="divide-y divide-warmGray-800">
<li class="pb-6 flex items-center justify-between">
<div class="flex flex-col">
<p class="text-base font-bold text-warmGray-100">Preview deployments</p>
<p class="text-sm font-medium text-warmGray-400">
PR's will be deployed so you could review them easily
</p>
</div>
{#if $isPullRequestPermissionsGranted}
<div
class="relative"
class:animate-wiggle={loading.previewDeployment}
class:opacity-25={loading.previewDeployment}
>
<button
type="button"
disabled={loading.previewDeployment}
on:click={setPreviewDeployment}
aria-pressed="false"
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-200"
class:bg-green-600={$application.general.isPreviewDeploymentEnabled}
class:bg-warmGray-700={!$application.general.isPreviewDeploymentEnabled}
class:cursor-not-allowed={loading.previewDeployment}
>
<span class="sr-only">Use setting</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200"
class:translate-x-5={$application.general.isPreviewDeploymentEnabled}
class:translate-x-0={!$application.general.isPreviewDeploymentEnabled}
>
<span
class=" ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
class:opacity-0={$application.general.isPreviewDeploymentEnabled}
class:opacity-100={!$application.general.isPreviewDeploymentEnabled}
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={$application.general.isPreviewDeploymentEnabled}
class:opacity-0={!$application.general.isPreviewDeploymentEnabled}
>
<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>
{#if loading.previewDeployment}
<div class="absolute left-0 bottom-0 -mb-4 -ml-2 text-xs font-bold">{$application.general.isPreviewDeploymentEnabled ? 'Enabling...' : 'Disabling...' }</div>
{/if}
</div>
{:else}
<div class="relative">
{#if !howToActivate}
<button
class="button py-2 px-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
on:click={() => (howToActivate = !howToActivate)}>How to active this?</button
>
{:else}
<button
class="button py-2 px-2 bg-green-600 hover:bg-green-500 text-white"
on:click={openGithub}>Open Github</button
>
{/if}
{#if howToActivate}
<div class="absolute right-0 w-64 z-10">
<div class="bg-warmGray-800 p-4 my-2 rounded text-white">
<div
class="absolute right-0 top-0 p-2 my-3 mx-1 text-xs font-bold cursor-pointer hover:bg-warmGray-700"
on:click={() => (howToActivate = false)}
>
X
</div>
<p class="text-sm font-medium text-warmGray-400">
You need to add <span class="text-white">two new permissions</span> to your GitHub
App:
</p>
<br />
<p class="text-sm font-medium text-warmGray-400">
1. In <span class="text-white">Repository permissions</span>, add
<span class="text-white">Read-only</span>
access to <span class="text-white">Pull requests</span>.
</p>
<br />
<p class="text-sm font-medium text-warmGray-400">
2. In <span class="text-white">Subscribe to events</span> section,
<span class="text-white"> check Pull request</span> field.
</p>
<br />
<p class="text-sm font-medium text-warmGray-400">
3. You <span class="text-white">receive an email</span> where you need to
<span class="text-white">accept the new permissions</span>.
</p>
</div>
</div>
{/if}
</div>
{/if}
</li>
</ul>
</div>
<div class="grid grid-flow-col gap-2 items-center pb-6">
<div class="grid grid-flow-row">
<label for="Domain" class="">Domain</label>
@@ -253,53 +495,92 @@
</div>
</div>
</div>
<div
class="text-2xl font-bold w-40"
class:border-gradient={buildpacks[$application.build.pack].build}
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>
Commands
</div>
<div class="text-2xl font-bold w-40 border-gradient">Commands</div>
<div class=" max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10 pb-32">
<div class="grid grid-flow-col gap-2 items-center">
<div class="grid grid-flow-row">
<label
for="installCommand"
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>Install Command <TooltipInfo
label="Command to run for installing dependencies. eg: yarn install."
/>
</label>
{#if $application.build.pack === 'python'}
<label for="ModulePackageName"
>Module/Package Name<TooltipInfo
label="The module/package name to start (eg: the entry filename [main], without the py extension. See gunicorn.org for more details)"
/>
</label>
<input
class="mb-6"
class:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:text-warmGray-900={!buildpacks[$application.build.pack].build}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].build}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:cursor-not-allowed={!buildpacks[$application.build.pack].build}
id="installCommand"
bind:value={$application.build.command.installation}
placeholder="eg: yarn install"
/>
<label
for="buildCommand"
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>Build Command <TooltipInfo
label="Command to run for building your application. If empty, no build phase initiated in the deploy process."
/></label
>
<input
class="mb-6"
class:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:text-warmGray-900={!buildpacks[$application.build.pack].build}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].build}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:cursor-not-allowed={!buildpacks[$application.build.pack].build}
id="buildCommand"
bind:value={$application.build.command.build}
placeholder="eg: yarn build"
/>
<input
class="mb-6"
id="ModulePackageName"
bind:value={$application.build.command.python.module}
placeholder="main"
/>
<label for="ApplicationInstance"
>Application Instance<TooltipInfo
label="The instance name (the main function name. See gunicorn.org for more details)"
/>
</label>
<input
class="mb-6"
id="ApplicationInstance"
bind:value={$application.build.command.python.instance}
placeholder="app"
/>
{:else}
<label
for="installCommand"
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>Install Command <TooltipInfo
label="Command to run for installing dependencies. eg: yarn install"
/>
</label>
<input
class="mb-6"
class:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:text-warmGray-900={!buildpacks[$application.build.pack].build}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].build}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:cursor-not-allowed={!buildpacks[$application.build.pack].build}
id="installCommand"
bind:value={$application.build.command.installation}
placeholder="eg: yarn install"
/>
<label
for="buildCommand"
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>Build Command <TooltipInfo
label="Command to run for building your application. If empty, no build phase initiated in the deploy process."
/></label
>
<input
class="mb-6"
class:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:text-warmGray-900={!buildpacks[$application.build.pack].build}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].build}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:cursor-not-allowed={!buildpacks[$application.build.pack].build}
id="buildCommand"
bind:value={$application.build.command.build}
placeholder="eg: yarn build"
/>
<label
for="startCommand"
class:text-warmGray-800={!buildpacks[$application.build.pack].start}
>Start Command <TooltipInfo
label="Command to start the application. eg: yarn start"
/></label
>
<input
class="mb-6"
class:bg-warmGray-900={!buildpacks[$application.build.pack].start}
class:text-warmGray-900={!buildpacks[$application.build.pack].start}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].start}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].start}
class:cursor-not-allowed={!buildpacks[$application.build.pack].start}
id="startcommand"
bind:value={$application.build.command.start}
placeholder="eg: yarn start"
/>
{/if}
</div>
</div>
</div>
@@ -309,4 +590,5 @@
.buildpack {
@apply px-6 py-2 mx-2 my-2 bg-warmGray-800 w-48 ease-in-out transform hover:scale-105 text-center rounded border-2 border-transparent border-dashed cursor-pointer transition duration-100;
}
</style>

View File

@@ -0,0 +1,110 @@
<script>
import { browser } from '$app/env';
import { goto } from '$app/navigation';
import { onDestroy, onMount } from 'svelte';
import { session } from '$app/stores';
import { request } from '$lib/request';
import { toast } from '@zerodevx/svelte-toast';
import { application, prApplication } from '$store';
let loadPRDeployments = null;
onMount(async () => {
await getPRDeployments();
loadPRDeployments = setInterval(async () => {
await getPRDeployments();
}, 1000);
});
onDestroy(() => {
clearInterval(loadPRDeployments);
});
async function getPRDeployments() {
const { configuration } = await request(`/api/v1/application/config`, $session, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch
}
});
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
}
async function removePR(prConfiguration) {
const result = window.confirm("Are you sure? It's NOT reversible!");
if (result) {
await request(`/api/v1/application/remove`, $session, {
body: {
organization: prConfiguration.repository.organization,
name: prConfiguration.repository.name,
branch: prConfiguration.repository.branch,
domain: prConfiguration.publish.domain
}
});
browser && toast.push('PR deployment removed.');
const { configuration } = await request(`/api/v1/application/config`, $session, {
body: {
name: prConfiguration.repository.name,
organization: prConfiguration.repository.organization,
branch: prConfiguration.repository.branch
}
});
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
}
}
</script>
<div class="text-2xl font-bold border-gradient w-48">Pull Requests</div>
<div class="text-center pt-4">
{#if $prApplication.length > 0}
<div class="py-4 ">
{#each $prApplication as pr}
<div class="flex space-x-4 justify-center items-center">
<div class="text-left font-bold tracking-tight ">
{pr.publish.domain}
</div>
<a
target="_blank"
class="icon mx-2 "
href={'https://' + pr.publish.domain + pr.publish.path}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg></a
>
<!-- <div class="flex-1" /> -->
<button
class="icon hover:text-red-500 hover:bg-warmGray-800"
on:click={() => removePR(pr)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
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>
{:else}
<div class="font-bold text-center">No PR deployments found</div>
{/if}
</div>

View File

@@ -1,8 +1,15 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { session } from '$app/stores';
import { githubRepositories, application, githubInstallations } from '$store';
import {
githubRepositories,
application,
githubInstallations,
prApplication,
initConf,
isPullRequestPermissionsGranted
} from '$store';
import { fade } from 'svelte/transition';
import Loading from '$components/Loading.svelte';
@@ -18,6 +25,7 @@
};
let branches = [];
let relogin = false;
let permissions = {};
function dashify(str: string, options?: any) {
if (typeof str !== 'string') return str;
return str
@@ -60,7 +68,7 @@
return false;
}
if (installations.length === 0) {
relogin = true;
loading.github = false;
return false;
}
$application.github.installation.id = installations[0].id;
@@ -110,8 +118,17 @@
$session
);
loading.branches = false;
await loadPermissions();
}
async function loadPermissions() {
const config = await request(
`https://api.github.com/apps/${import.meta.env.VITE_GITHUB_APP_NAME}`,
$session
);
if (config.permissions['pull_requests'] && config.events.includes('pull_request')) {
$isPullRequestPermissionsGranted = true;
}
}
async function modifyGithubAppConfig() {
if (browser) {
const left = screen.width / 2 - 1020 / 2;
@@ -128,20 +145,23 @@
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(async () => {
if (newWindow.closed) {
if (newWindow?.closed) {
clearInterval(timer);
loading.github = true;
if ($application.repository.name) {
try {
const config = await request(`/api/v1/application/config`, $session, {
const { configuration } = await request(`/api/v1/application/config`, $session, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch
}
});
$application = { ...config };
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
$application = configuration.find((c) => c.general.pullRequest === 0);
$initConf = JSON.parse(JSON.stringify($application));
} catch (error) {
browser && goto('/dashboard/applications', { replaceState: true });
}

View File

@@ -14,7 +14,7 @@
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow.closed) {
if (newWindow?.closed) {
clearInterval(timer);
location.reload()
}

View File

@@ -3,22 +3,28 @@
import { onDestroy } from 'svelte';
import { toast } from '@zerodevx/svelte-toast';
import Tooltip from '$components/Tooltip.svelte';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { page, session } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/env';
async function removeApplication() {
await request(`/api/v1/application/remove`, $session, {
body: {
organization: $application.repository.organization,
name: $application.repository.name,
branch: $application.repository.branch
}
});
const result = window.confirm(
"Are you sure? It will delete all deployments, including PR's - it's NOT reversible!"
);
if (result) {
await request(`/api/v1/application/remove`, $session, {
body: {
organization: $application.repository.organization,
name: $application.repository.name,
branch: $application.repository.branch,
domain: $application.publish.domain
}
});
browser && toast.push('Application removed.');
$application = JSON.parse(JSON.stringify(initialApplication));
browser && goto(`/dashboard/applications`, { replaceState: true });
browser && toast.push('Application removed.');
$application = JSON.parse(JSON.stringify(initialApplication));
browser && goto(`/dashboard/applications`, { replaceState: true });
}
}
onDestroy(() => {
@@ -50,6 +56,7 @@
// toast.push(error.error || error || 'Ooops something went wrong.');
}
}
</script>
<nav class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4 z-50">

View File

@@ -1,25 +1,26 @@
<script>
import { toast } from '@zerodevx/svelte-toast';
import templates from '$lib/api/applications/templates';
import { application, dashboard } from '$store';
import templates from '$lib/api/applications/packs/templates';
import { application, dashboard, initConf, prApplication } from '$store';
import General from '$components/Application/ActiveTab/General.svelte';
import Secrets from '$components/Application/ActiveTab/Secrets.svelte';
import Loading from '$components/Loading.svelte';
import { goto } from '$app/navigation';
import { page, session } from '$app/stores';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { browser } from '$app/env';
import PullRequests from './ActiveTab/PullRequests.svelte';
let activeTab = {
general: true,
buildStep: false,
secrets: false
secrets: false,
pullRequests: false
};
function activateTab(tab) {
if (activeTab.hasOwnProperty(tab)) {
activeTab = {
general: false,
buildStep: false,
pullRequests: false,
secrets: false
};
activeTab[tab] = true;
@@ -48,16 +49,7 @@
}
return;
}
if ($page.path !== '/application/new') {
const config = await request(`/api/v1/application/config`, $session, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch
}
});
$application = { ...config };
} else {
if ($page.path === '/application/new') {
try {
const dir = await request(
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/contents/?ref=${$application.repository.branch}`,
@@ -66,7 +58,7 @@
const packageJson = dir.find((f) => f.type === 'file' && f.name === 'package.json');
const Dockerfile = dir.find((f) => f.type === 'file' && f.name === 'Dockerfile');
const CargoToml = dir.find((f) => f.type === 'file' && f.name === 'Cargo.toml');
const requirementsTXT = dir.find((f) => f.type === 'file' && f.name === 'requirements.txt');
if (packageJson) {
const { content } = await request(packageJson.git_url, $session);
const packageJsonContent = JSON.parse(atob(content));
@@ -80,10 +72,18 @@
if (checkPackageJSONContents(dep)) {
const config = templates[dep];
$application.build.pack = config.pack;
if (config.installation)
if (config.installation) {
$application.build.command.installation = config.installation;
if (config.port) $application.publish.port = config.port;
if (config.directory) $application.publish.directory = config.directory;
}
if (config.start) {
$application.build.command.start = config.start;
}
if (config.port) {
$application.publish.port = config.port;
}
if (config.directory) {
$application.publish.directory = config.directory;
}
if (packageJsonContent.scripts.hasOwnProperty('build') && config.build) {
$application.build.command.build = config.build;
@@ -94,6 +94,9 @@
} else if (CargoToml) {
$application.build.pack = 'rust';
browser && toast.push(`Rust language detected. Default values set.`);
} else if (requirementsTXT) {
$application.build.pack = 'python';
browser && toast.push('Python language detected. Default values set.');
} else if (Dockerfile) {
$application.build.pack = 'docker';
browser && toast.push('Custom Dockerfile found. Build pack set to docker.');
@@ -103,6 +106,7 @@
}
}
}
</script>
{#await load()}
@@ -124,6 +128,15 @@
>
Secrets
</div>
{#if $application.general.isPreviewDeploymentEnabled}
<div
on:click={() => activateTab('pullRequests')}
class:text-green-500={activeTab.pullRequests}
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
>
Pull Requests
</div>
{/if}
</nav>
</div>
<div class="max-w-4xl mx-auto">
@@ -132,6 +145,8 @@
<General />
{:else if activeTab.secrets}
<Secrets />
{:else if activeTab.pullRequests && $page.path !== '/application/new'}
<PullRequests />
{/if}
</div>
</div>

View File

@@ -5,16 +5,19 @@
import Postgresql from './SVGs/Postgresql.svelte';
import Mysql from './SVGs/Mysql.svelte';
import CouchDb from './SVGs/CouchDb.svelte';
import Redis from './SVGs/Redis.svelte';
import { page, session } from '$app/stores';
import { goto } from '$app/navigation';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { browser } from '$app/env';
import Loading from '$components/Loading.svelte';
let type;
let defaultDatabaseName;
let loading = false;
async function deploy() {
try {
loading = true;
await request(`/api/v1/databases/deploy`, $session, {
body: {
type,
@@ -28,82 +31,106 @@
}
} catch (error) {
console.log(error);
} finally {
loading = false;
}
}
</script>
<div class="text-center space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
{#if $page.path === '/database/new'}
<div class="flex justify-center space-x-4 font-bold pb-6">
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-green-600 p-2 rounded bg-warmGray-800 w-32"
class:border-green-600={type === 'mongodb'}
on:click={() => (type = 'mongodb')}
>
<div class="flex items-center justify-center my-2">
<MongoDb customClass="w-6" />
{#if loading}
<Loading />
{:else}
<div class="text-center space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
{#if $page.path === '/database/new'}
<div class="flex justify-center space-x-4 font-bold pb-6">
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-green-600 p-2 rounded bg-warmGray-800 w-32"
class:border-green-600={type === 'mongodb'}
on:click={() => (type = 'mongodb')}
>
<div class="flex items-center justify-center my-2">
<MongoDb customClass="w-6" />
</div>
<div class="text-white">MongoDB</div>
</div>
<div class="text-white">MongoDB</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-red-600 p-2 rounded bg-warmGray-800 w-32"
class:border-red-600={type === 'couchdb'}
on:click={() => (type = 'couchdb')}
>
<div class="flex items-center justify-center my-2">
<CouchDb customClass="w-12 text-red-600 fill-current" />
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-red-600 p-2 rounded bg-warmGray-800 w-32"
class:border-red-600={type === 'couchdb'}
on:click={() => (type = 'couchdb')}
>
<div class="flex items-center justify-center my-2">
<CouchDb customClass="w-12 text-red-600 fill-current" />
</div>
<div class="text-white">Couchdb</div>
</div>
<div class="text-white">Couchdb</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-600 p-2 rounded bg-warmGray-800 w-32"
class:border-blue-600={type === 'postgresql'}
on:click={() => (type = 'postgresql')}
>
<div class="flex items-center justify-center my-2">
<Postgresql customClass="w-12" />
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-600 p-2 rounded bg-warmGray-800 w-32"
class:border-blue-600={type === 'postgresql'}
on:click={() => (type = 'postgresql')}
>
<div class="flex items-center justify-center my-2">
<Postgresql customClass="w-12" />
</div>
<div class="text-white">PostgreSQL</div>
</div>
<div class="text-white">PostgreSQL</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-orange-600 p-2 rounded bg-warmGray-800 w-32"
class:border-orange-600={type === 'mysql'}
on:click={() => (type = 'mysql')}
>
<div class="flex items-center justify-center">
<Mysql customClass="w-10" />
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-orange-600 p-2 rounded bg-warmGray-800 w-32"
class:border-orange-600={type === 'mysql'}
on:click={() => (type = 'mysql')}
>
<div class="flex items-center justify-center">
<Mysql customClass="w-10" />
</div>
<div class="text-white">MySQL</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-red-600 p-2 rounded bg-warmGray-800 w-32"
class:border-red-600={type === 'redis'}
on:click={() => (type = 'redis')}
>
<div class="flex items-center justify-center">
<Redis customClass="w-12" />
</div>
<div class="text-white">Redis</div>
</div>
<div class="text-white">MySQL</div>
</div>
<!-- <button
<!-- <button
class="button bg-gray-500 p-2 text-white hover:bg-yellow-500 cursor-pointer w-32"
on:click="{() => (type = 'clickhouse')}"
class:bg-yellow-500="{type === 'clickhouse'}"
>
Clickhouse
</button> -->
</div>
{#if type}
<div class="flex justify-center space-x-4 items-center">
<label for="defaultDB">Default database</label>
<input id="defaultDB" class="w-64" placeholder="random" bind:value={defaultDatabaseName} />
<button
class:bg-green-600={type === 'mongodb'}
class:hover:bg-green-500={type === 'mongodb'}
class:bg-blue-600={type === 'postgresql'}
class:hover:bg-blue-500={type === 'postgresql'}
class:bg-orange-600={type === 'mysql'}
class:hover:bg-orange-500={type === 'mysql'}
class:bg-red-600={type === 'couchdb'}
class:hover:bg-red-500={type === 'couchdb'}
class:bg-yellow-500={type === 'clickhouse'}
class:hover:bg-yellow-400={type === 'clickhouse'}
class="button p-2 w-32 text-white"
on:click={deploy}>Deploy</button
>
</div>
{#if type}
<div class="flex justify-center space-x-4 items-center">
{#if type !== 'redis'}
<label for="defaultDB">Default database</label>
<input
id="defaultDB"
class="w-64"
placeholder="random"
bind:value={defaultDatabaseName}
/>
{/if}
<button
class:bg-green-600={type === 'mongodb'}
class:hover:bg-green-500={type === 'mongodb'}
class:bg-blue-600={type === 'postgresql'}
class:hover:bg-blue-500={type === 'postgresql'}
class:bg-orange-600={type === 'mysql'}
class:hover:bg-orange-500={type === 'mysql'}
class:bg-red-600={type === 'couchdb' || type === 'redis'}
class:hover:bg-red-500={type === 'couchdb' || type === 'redis'}
class:bg-yellow-500={type === 'clickhouse'}
class:hover:bg-yellow-400={type === 'clickhouse'}
class="button p-2 w-32 text-white"
on:click={deploy}>Deploy</button
>
</div>
{/if}
{/if}
{/if}
</div>
</div>
{/if}

View File

@@ -0,0 +1,37 @@
<script>
export let customClass;
</script>
<svg
class={customClass}
height="64"
viewBox="0 0 32 32"
width="64"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
><defs
><path
id="a"
d="m45.536 38.764c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.813s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z"
/><path
id="b"
d="m45.536 28.733c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.935c2.332-.837 3.14-.867 5.126-.14s12.35 4.853 14.312 5.57 2.037 1.31.024 2.36z"
/></defs
><g transform="matrix(.848327 0 0 .848327 -7.883573 -9.449691)"
><use fill="#a41e11" xlink:href="#a" /><path
d="m45.536 34.95c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.936c2.332-.836 3.14-.867 5.126-.14s12.35 4.852 14.31 5.582 2.037 1.31.024 2.36z"
fill="#d82c20"
/><use fill="#a41e11" xlink:href="#a" y="-6.218" /><use fill="#d82c20" xlink:href="#b" /><path
d="m45.536 26.098c-2.013 1.05-12.44 5.337-14.66 6.495s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.815s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z"
fill="#a41e11"
/><use fill="#d82c20" xlink:href="#b" y="-6.449" /><g fill="#fff"
><path
d="m29.096 20.712-1.182-1.965-3.774-.34 2.816-1.016-.845-1.56 2.636 1.03 2.486-.814-.672 1.612 2.534.95-3.268.34zm-6.296 3.912 8.74-1.342-2.64 3.872z"
/><ellipse cx="20.444" cy="21.402" rx="4.672" ry="1.811" /></g
><path d="m42.132 21.138-5.17 2.042-.004-4.087z" fill="#7a0c00" /><path
d="m36.963 23.18-.56.22-5.166-2.042 5.723-2.264z"
fill="#ad2115"
/></g
></svg
>

View File

@@ -3,7 +3,7 @@
import { toast } from '@zerodevx/svelte-toast';
import Loading from '../Loading.svelte';
import Tooltip from '$components/Tooltip.svelte';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { page, session } from '$app/stores';
import PasswordField from '$components/PasswordField.svelte';
import { browser } from '$app/env';

7
src/global.d.ts vendored
View File

@@ -36,6 +36,8 @@ export type Application = {
deployId: string;
nickname: string;
workdir: string;
isPreviewDeploymentEnabled: boolean;
pullRequest: number;
};
build: {
pack: string;
@@ -43,6 +45,11 @@ export type Application = {
command: {
build: string | null;
installation: string;
start: string;
python: {
module?: string;
instance?: string;
};
};
container: {
name: string;

View File

@@ -4,6 +4,9 @@ import { publicPages } from '$lib/consts';
import mongoose from 'mongoose';
import { verifyUserId } from '$lib/api/common';
import { initializeSession } from 'svelte-kit-cookie-session';
import { cleanupStuckedDeploymentsInDB } from '$lib/api/applications/cleanup';
import { docker } from '$lib/api/docker';
import Configuration from '$models/Configuration';
process.on('SIGINT', function () {
mongoose.connection.close(function () {
@@ -13,6 +16,7 @@ process.on('SIGINT', function () {
});
async function connectMongoDB() {
// TODO: Save configurations on start?
const { MONGODB_USER, MONGODB_PASSWORD, MONGODB_HOST, MONGODB_PORT, MONGODB_DB } = process.env;
try {
if (process.env.NODE_ENV === 'production') {
@@ -32,14 +36,80 @@ async function connectMongoDB() {
}
}
if (mongoose.connection.readyState !== 1) connectMongoDB();
(async () => {
if (mongoose.connection.readyState !== 1) await connectMongoDB();
try {
await mongoose.connection.db.dropCollection('logs-servers');
} catch (error) {
//
}
try {
await cleanupStuckedDeploymentsInDB();
} catch (error) {
console.log(error);
}
try {
const dockerServices = await docker.engine.listServices();
let applications: any = dockerServices.filter(
(r) =>
r.Spec.Labels.managedBy === 'coolify' &&
r.Spec.Labels.type === 'application' &&
r.Spec.Labels.configuration
);
applications = applications.map((r) => {
if (JSON.parse(r.Spec.Labels.configuration)) {
return {
configuration: JSON.parse(r.Spec.Labels.configuration),
UpdatedAt: r.UpdatedAt
};
}
return {};
});
applications = [
...new Map(
applications.map((item) => [
item.configuration.publish.domain + item.configuration.publish.path,
item
])
).values()
];
for (const application of applications) {
await Configuration.findOneAndUpdate(
{
'repository.name': application.configuration.repository.name,
'repository.organization': application.configuration.repository.organization,
'repository.branch': application.configuration.repository.branch,
'publish.domain': application.configuration.publish.domain
},
{
...application.configuration
},
{ upsert: true, new: true }
);
}
} catch (error) {
console.log(error);
}
})();
export async function handle({ request, render }) {
export async function handle({ request, resolve }) {
const { SECRETS_ENCRYPTION_KEY } = process.env;
const session = initializeSession(request.headers, {
secret: SECRETS_ENCRYPTION_KEY,
cookie: { path: '/' }
});
let session;
try {
session = initializeSession(request.headers, {
secret: SECRETS_ENCRYPTION_KEY,
cookie: { path: '/' }
});
} catch (error) {
return {
status: 302,
headers: {
'set-cookie': 'kit.session=deleted;path=/;expires=Wed, 21 Oct 2015 07:28:00 GMT',
location: '/'
}
};
}
request.locals.session = session;
if (session?.data?.coolToken) {
try {
@@ -49,7 +119,7 @@ export async function handle({ request, render }) {
request.locals.session.destroy = true;
}
}
const response = await render(request);
const response = await resolve(request);
if (!session['set-cookie']) {
if (!session?.data?.coolToken && !publicPages.includes(request.path)) {
return {

View File

@@ -1,4 +1,4 @@
import Deployment from '$models/Logs/Deployment';
import Deployment from '$models/Deployment';
import { saveAppLog } from './logging';
import * as packs from './packs';

View File

@@ -1,4 +1,5 @@
import { docker } from '$lib/api/docker';
import Deployment from '$models/Deployment';
import { execShellAsync } from '../common';
export async function deleteSameDeployments(configuration) {
@@ -10,36 +11,56 @@ export async function deleteSameDeployments(configuration) {
const running = JSON.parse(s.Spec.Labels.configuration);
if (
running.repository.id === configuration.repository.id &&
running.repository.branch === configuration.repository.branch
running.repository.branch === configuration.repository.branch &&
running.publish.domain === configuration.publish.domain
) {
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`);
}
});
}
export async function cleanupStuckedDeploymentsInDB() {
// Cleanup stucked deployments.
await Deployment.updateMany(
{ progress: { $in: ['queued', 'inprogress'] } },
{ progress: 'failed' }
);
}
export async function purgeImagesContainers(configuration, deleteAll = false) {
const { name, tag } = configuration.build.container;
await execShellAsync('docker container prune -f');
if (deleteAll) {
const IDsToDelete = (
await execShellAsync(`docker images ls --filter=reference='${name}' --format '{{json .ID }}'`)
)
.trim()
.replace(/"/g, '')
.split('\n');
if (IDsToDelete.length > 0)
await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`);
} else {
const IDsToDelete = (
await execShellAsync(
`docker images ls --filter=reference='${name}' --filter=before='${name}:${tag}' --format '{{json .ID }}'`
)
)
.trim()
.replace(/"/g, '')
.split('\n');
if (IDsToDelete.length > 1)
await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`);
try {
await execShellAsync('docker container prune -f');
} catch (error) {
//
}
try {
if (deleteAll) {
const IDsToDelete = (
await execShellAsync(
`docker images ls --filter=reference='${name}' --format '{{json .ID }}'`
)
)
.trim()
.replace(/"/g, '')
.split('\n');
if (IDsToDelete.length > 0) await execShellAsync(`docker rmi -f ${IDsToDelete.join(' ')}`);
} else {
const IDsToDelete = (
await execShellAsync(
`docker images ls --filter=reference='${name}' --filter=before='${name}:${tag}' --format '{{json .ID }}'`
)
)
.trim()
.replace(/"/g, '')
.split('\n');
if (IDsToDelete.length > 1) await execShellAsync(`docker rmi -f ${IDsToDelete.join(' ')}`);
}
} catch (error) {
console.log(error);
}
try {
await execShellAsync('docker image prune -f');
} catch (error) {
//
}
await execShellAsync('docker image prune -f');
}

View File

@@ -4,7 +4,7 @@ import { execShellAsync } from '../common';
export default async function (configuration) {
try {
const { GITHUB_APP_PRIVATE_KEY } = process.env;
const { workdir } = configuration.general;
const { workdir, isPreviewDeploymentEnabled, pullRequest } = configuration.general;
const { organization, name, branch } = configuration.repository;
const github = configuration.github;
if (!github.installation.id || !github.app.id) {
@@ -37,8 +37,14 @@ export default async function (configuration) {
await execShellAsync(
`mkdir -p ${workdir} && git clone -q -b ${branch} https://x-access-token:${token}@github.com/${organization}/${name}.git ${workdir}/`
);
if (isPreviewDeploymentEnabled && pullRequest && pullRequest !== 0) {
await execShellAsync(
`cd ${workdir} && git fetch origin pull/${pullRequest}/head:pull_${pullRequest} && git checkout pull_${pullRequest}`
);
}
configuration.build.container.tag = (
await execShellAsync(`cd ${configuration.general.workdir}/ && git rev-parse HEAD`)
await execShellAsync(`cd ${workdir}/ && git rev-parse HEAD`)
)
.replace('\n', '')
.slice(0, 7);

View File

@@ -10,9 +10,8 @@ function getUniq() {
}
export function setDefaultConfiguration(configuration) {
const nickname = getUniq();
const nickname = configuration.general.nickname || getUniq();
const deployId = cuid();
const shaBase = JSON.stringify({ repository: configuration.repository });
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex');
@@ -21,17 +20,25 @@ export function setDefaultConfiguration(configuration) {
configuration.general.nickname = nickname;
configuration.general.deployId = deployId;
configuration.general.workdir = `/tmp/${deployId}`;
if (configuration.general.isPreviewDeploymentEnabled && configuration.general.pullRequest !== 0) {
configuration.build.container.name = `pr${configuration.general.pullRequest}-${sha256.slice(
0,
8
)}`;
configuration.publish.domain = `pr${configuration.general.pullRequest}.${configuration.publish.domain}`;
}
if (!configuration.publish.path) configuration.publish.path = '/';
if (!configuration.publish.port) {
if (
configuration.build.pack === 'nodejs' ||
configuration.build.pack === 'vuejs' ||
configuration.build.pack === 'nuxtjs' ||
configuration.build.pack === 'rust' ||
configuration.build.pack === 'nextjs'
configuration.build.pack === 'nextjs' ||
configuration.build.pack === 'nestjs'
) {
configuration.publish.port = 3000;
} else if (configuration.build.pack === 'python') {
configuration.publish.port = 4000;
} else {
configuration.publish.port = 80;
}
@@ -48,6 +55,21 @@ export function setDefaultConfiguration(configuration) {
if (!configuration.build.command.installation)
configuration.build.command.installation = 'yarn install';
}
if (
configuration.build.pack === 'nodejs' ||
configuration.build.pack === 'vuejs' ||
configuration.build.pack === 'nuxtjs' ||
configuration.build.pack === 'nextjs' ||
configuration.build.pack === 'nestjs'
) {
if (!configuration.build.command.start) configuration.build.command.start = 'yarn start';
}
if (configuration.build.pack === 'python') {
if (!configuration.build.command.python.module)
configuration.build.command.python.module = 'main';
if (!configuration.build.command.python.instance)
configuration.build.command.python.instance = 'app';
}
configuration.build.container.baseSHA = crypto
.createHash('sha256')
@@ -58,13 +80,17 @@ export function setDefaultConfiguration(configuration) {
return configuration;
}
export async function precheckDeployment({ services, configuration }) {
export async function precheckDeployment(configuration) {
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 configChanged = false;
let imageChanged = false;
let forceUpdate = false;
for (const service of services) {
const running = JSON.parse(service.Spec.Labels.configuration);
if (running) {
@@ -72,6 +98,7 @@ export async function precheckDeployment({ services, configuration }) {
running.repository.id === configuration.repository.id &&
running.repository.branch === configuration.repository.branch
) {
foundService = true;
// Base service configuration changed
if (
!running.build.container.baseSHA ||
@@ -91,8 +118,32 @@ export async function precheckDeployment({ services, configuration }) {
(n) =>
n.DesiredState !== 'Running' && n.Image.split(':')[1] === running.build.container.tag
);
if (isError.length > 0) forceUpdate = true;
foundService = true;
if (isError.length > 0) {
forceUpdate = true;
}
const 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;
};
const runningWithoutContainer = JSON.parse(JSON.stringify(running));
delete runningWithoutContainer.build.container;
@@ -102,16 +153,23 @@ export async function precheckDeployment({ services, configuration }) {
// If only the configuration changed
if (
JSON.stringify(runningWithoutContainer.build) !==
JSON.stringify(configurationWithoutContainer.build) ||
JSON.stringify(runningWithoutContainer.publish) !==
JSON.stringify(configurationWithoutContainer.publish)
)
!compareObjects(runningWithoutContainer.build, configurationWithoutContainer.build) ||
!compareObjects(runningWithoutContainer.publish, configurationWithoutContainer.publish) ||
runningWithoutContainer.general.isPreviewDeploymentEnabled !==
configurationWithoutContainer.general.isPreviewDeploymentEnabled
) {
configChanged = true;
}
// If only the image changed
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true;
// If build pack changed, forceUpdate the service
if (running.build.pack !== configuration.build.pack) forceUpdate = true;
if (
configuration.general.isPreviewDeploymentEnabled &&
configuration.general.pullRequest !== 0
)
forceUpdate = true;
}
}
}
@@ -128,29 +186,9 @@ export async function precheckDeployment({ services, configuration }) {
}
export async function updateServiceLabels(configuration) {
// In case of any failure during deployment, still update the current configuration.
const services = (await docker.engine.listServices()).filter(
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
return await execShellAsync(
`docker service update --label-add configuration='${JSON.stringify(configuration)}' ${
configuration.build.container.name
}_${configuration.build.container.name}`
);
const found = services.find((s) => {
const config = JSON.parse(s.Spec.Labels.configuration);
if (
config.repository.id === configuration.repository.id &&
config.repository.branch === configuration.repository.branch
) {
return config;
}
return null;
});
if (found) {
const { ID } = found;
const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration };
await execShellAsync(
`docker service update --label-add configuration='${JSON.stringify(
Labels
)}' --label-add com.docker.stack.image='${configuration.build.container.name}:${
configuration.build.container.tag
}' ${ID}`
);
}
}

View File

@@ -1,9 +1,9 @@
import { docker } from '$lib/api/docker';
import { saveAppLog } from './logging';
import { promises as fs } from 'fs';
import { deleteSameDeployments } from './cleanup';
import { deleteSameDeployments, purgeImagesContainers } from './cleanup';
import yaml from 'js-yaml';
import { execShellAsync } from '../common';
import { delay, execShellAsync } from '../common';
export default async function (configuration, imageChanged) {
const generateEnvs = {};
@@ -11,6 +11,7 @@ export default async function (configuration, imageChanged) {
generateEnvs[secret.name] = secret.value;
}
const containerName = configuration.build.container.name;
const containerTag = configuration.build.container.tag;
// Only save SHA256 of it in the configuration label
const baseServiceConfiguration = configuration.baseServiceConfiguration;
@@ -20,7 +21,7 @@ export default async function (configuration, imageChanged) {
version: '3.8',
services: {
[containerName]: {
image: `${configuration.build.container.name}:${configuration.build.container.tag}`,
image: `${containerName}:${containerTag}`,
networks: [`${docker.network}`],
environment: generateEnvs,
deploy: {
@@ -31,22 +32,18 @@ export default async function (configuration, imageChanged) {
'configuration=' + JSON.stringify(configuration),
'traefik.enable=true',
'traefik.http.services.' +
configuration.build.container.name +
containerName +
`.loadbalancer.server.port=${configuration.publish.port}`,
'traefik.http.routers.' + configuration.build.container.name + '.entrypoints=websecure',
'traefik.http.routers.' + containerName + '.entrypoints=websecure',
'traefik.http.routers.' +
configuration.build.container.name +
containerName +
'.rule=Host(`' +
configuration.publish.domain +
'`) && PathPrefix(`' +
configuration.publish.path +
'`)',
'traefik.http.routers.' +
configuration.build.container.name +
'.tls.certresolver=letsencrypt',
'traefik.http.routers.' +
configuration.build.container.name +
'.middlewares=global-compress'
'traefik.http.routers.' + containerName + '.tls.certresolver=letsencrypt',
'traefik.http.routers.' + containerName + '.middlewares=global-compress'
]
}
}
@@ -62,7 +59,7 @@ export default async function (configuration, imageChanged) {
if (imageChanged) {
// console.log('image changed')
await execShellAsync(
`docker service update --image ${configuration.build.container.name}:${configuration.build.container.tag} ${configuration.build.container.name}_${configuration.build.container.name}`
`docker service update --image ${containerName}:${containerTag} ${containerName}_${containerName}`
);
} else {
// console.log('new deployment or force deployment or config changed')
@@ -71,6 +68,11 @@ export default async function (configuration, imageChanged) {
`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);
}

View File

@@ -1,6 +1,6 @@
import Settings from '$models/Settings';
import ServerLog from '$models/Logs/Server';
import ApplicationLog from '$models/Logs/Application';
import ServerLog from '$models/ServerLog';
import ApplicationLog from '$models/ApplicationLog';
import dayjs from 'dayjs';
import { version } from '../../../../package.json';

View File

@@ -1,20 +1,21 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
const buildImageNodeDocker = (configuration) => {
const buildImageNodeDocker = (configuration, prodBuild) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
`COPY ${configuration.build.directory}/package*.json ./`,
configuration.build.command.installation && `RUN ${configuration.build.command.installation}`,
`COPY ./${configuration.build.directory} ./`,
`RUN ${configuration.build.command.build}`
`RUN ${configuration.build.command.build}`,
prodBuild && `RUN rm -fr node_modules && ${configuration.build.command.installation} --prod`
].join('\n');
};
export async function buildImage(configuration, cacheBuild?: boolean) {
export async function buildImage(configuration, cacheBuild?: boolean, prodBuild?: boolean) {
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
buildImageNodeDocker(configuration)
buildImageNodeDocker(configuration, prodBuild)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },

View File

@@ -7,8 +7,10 @@ import php from './php';
import nuxtjs from './nuxtjs';
import nodejs from './nodejs';
import nextjs from './nextjs';
import nestjs from './nestjs';
import gatsby from './gatsby';
import docker from './docker';
import python from './python';
export {
vuejs,
@@ -20,6 +22,8 @@ export {
nuxtjs,
nodejs,
nextjs,
nestjs,
gatsby,
docker
docker,
python
};

View File

@@ -0,0 +1,31 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
const publishNodejsDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
: `
COPY ${configuration.build.directory}/package*.json ./
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
`CMD ${configuration.build.command.start}`
].join('\n');
};
export default async function (configuration) {
if (configuration.build.command.build) await buildImage(configuration, false, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishNodejsDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -13,7 +13,7 @@ const publishNodejsDocker = (configuration) => {
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
`CMD ${configuration.build.command.start}`
].join('\n');
};
export default async function (configuration) {

View File

@@ -13,7 +13,7 @@ const publishNodejsDocker = (configuration) => {
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
`CMD ${configuration.build.command.start}`
].join('\n');
};

View File

@@ -13,7 +13,7 @@ const publishNodejsDocker = (configuration) => {
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
`CMD ${configuration.build.command.start}`
].join('\n');
};

View File

@@ -8,7 +8,7 @@ const publishPHPDocker = (configuration) => {
'WORKDIR /usr/src/app',
`COPY ./${configuration.build.directory} /var/www/html`,
'EXPOSE 80',
' CMD ["apache2-foreground"]'
'CMD ["apache2-foreground"]'
].join('\n');
};

View File

@@ -0,0 +1,24 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
const publishPython = (configuration) => {
return [
'FROM python:3-alpine',
'WORKDIR /usr/src/app',
'RUN pip install gunicorn',
`COPY ./${configuration.build.directory}/requirements.txt ./`,
`RUN pip install --no-cache-dir -r ./${configuration.build.directory}/requirements.txt`,
`COPY ./${configuration.build.directory}/ .`,
`EXPOSE ${configuration.publish.port}`,
`CMD gunicorn -w=4 ${configuration.build.command.python.module}:${configuration.build.command.python.instance}`
].join('\n');
};
export default async function (configuration) {
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishPython(configuration));
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -1,6 +1,7 @@
const defaultBuildAndDeploy = {
installation: 'yarn install',
build: 'yarn build'
build: 'yarn build',
start: 'yarn start'
};
const templates = {
@@ -10,6 +11,13 @@ const templates = {
directory: 'public',
name: 'Svelte'
},
'@nestjs/core': {
pack: 'nestjs',
...defaultBuildAndDeploy,
start: 'yarn start:prod',
port: 3000,
name: 'NestJS'
},
next: {
pack: 'nextjs',
...defaultBuildAndDeploy,
@@ -38,6 +46,7 @@ const templates = {
pack: 'vuejs',
...defaultBuildAndDeploy,
directory: 'dist',
port: 80,
name: 'Vue'
},
gatsby: {

View File

@@ -1,4 +1,4 @@
import Deployment from '$models/Logs/Deployment';
import Deployment from '$models/Deployment';
import dayjs from 'dayjs';
import buildContainer from './buildContainer';
import { updateServiceLabels } from './configuration';
@@ -9,23 +9,22 @@ import { saveAppLog } from './logging';
export default async function (configuration, imageChanged) {
const { id, organization, name, branch } = configuration.repository;
const { domain } = configuration.publish;
const { deployId, nickname } = configuration.general;
await new Deployment({
repoId: id,
branch,
deployId,
domain,
organization,
name,
nickname
}).save();
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration);
await copyFiles(configuration);
await buildContainer(configuration);
await deploy(configuration, imageChanged);
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' }
);
await updateServiceLabels(configuration);
const { deployId } = configuration.general;
try {
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration);
await copyFiles(configuration);
await buildContainer(configuration);
await deploy(configuration, imageChanged);
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' }
);
await updateServiceLabels(configuration);
} catch (error) {
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' }
);
}
}

View File

@@ -1,13 +1,17 @@
import shell from 'shelljs';
import User from '$models/User';
import jsonwebtoken from 'jsonwebtoken';
import { saveServerLog } from './applications/logging';
export function execShellAsync(cmd, opts = {}) {
try {
return new Promise(function (resolve, reject) {
shell.config.silent = true;
shell.exec(cmd, opts, function (code, stdout, stderr) {
if (code !== 0) return reject(new Error(stderr));
shell.exec(cmd, opts, async function (code, stdout, stderr) {
if (code !== 0) {
await saveServerLog({ message: JSON.stringify({ cmd, opts, code, stdout, stderr }) });
return reject(new Error(stderr));
}
return resolve(stdout);
});
});

23
src/lib/api/github.ts Normal file
View File

@@ -0,0 +1,23 @@
import type { Request } from '@sveltejs/kit';
export async function githubAPI(
request: Request,
resource: string,
token?: string,
data?: Record<string, unknown>
) {
const base = 'https://api.github.com';
const res = await fetch(`${base}${resource}`, {
method: request.method,
headers: {
'content-type': 'application/json',
accept: 'application/json',
authorization: token ? `token ${token}` : ''
},
body: data && JSON.stringify(data)
});
return {
status: res.status,
body: await res.json()
};
}

View File

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

View File

@@ -61,10 +61,8 @@ export async function request(
} else if (response.headers.get('content-type').match(/multipart\/form-data/)) {
return await response.formData();
} else {
console.log(response);
if (response.headers.get('content-disposition')) {
const blob = await response.blob();
console.log(blob);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = response.headers.get('content-disposition').split('=')[1] || 'backup.gz';
@@ -86,10 +84,10 @@ export async function request(
});
} else if (response.status >= 500) {
const error = (await response.json()).error;
browser && toast.push(error);
browser && toast.push(error.message || error);
return Promise.reject({
status: response.status,
error: error || 'Oops, something is not okay. Are you okay?'
error: error.message || error || 'Oops, something is not okay. Are you okay?'
});
} else {
browser && toast.push(response.statusText);

View File

@@ -8,4 +8,5 @@ const ApplicationLogsSchema = new Schema({
ApplicationLogsSchema.set('timestamps', true);
export default mongoose.model('logs-application', ApplicationLogsSchema);
export default mongoose.models['logs-application'] ||
mongoose.model('logs-application', ApplicationLogsSchema);

View File

@@ -0,0 +1,55 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const ConfigurationSchema = new Schema({
github: {
installation: {
id: { type: Number, required: true }
},
app: {
id: { type: Number, required: true }
}
},
repository: {
id: { type: Number, required: true },
organization: { type: String, required: true },
name: { type: String, required: true },
branch: { type: String, required: true }
},
general: {
deployId: { type: String, required: true },
nickname: { type: String, required: true },
workdir: { type: String, required: true },
isPreviewDeploymentEnabled: { type: Boolean, required: true, default: false },
pullRequest: { type: Number, required: true, default: 0 }
},
build: {
pack: { type: String, required: true },
directory: { type: String },
command: {
build: { type: String },
installation: { type: String },
start: { type: String },
python: {
module: { type: String },
instance: { type: String }
}
},
container: {
name: { type: String, required: true },
tag: { type: String, required: true },
baseSHA: { type: String, required: true }
}
},
publish: {
directory: { type: String },
domain: { type: String, required: true },
path: { type: String },
port: { type: Number },
secrets: { type: Array }
}
});
ConfigurationSchema.set('timestamps', true);
export default mongoose.models['configuration'] ||
mongoose.model('configuration', ConfigurationSchema);

View File

@@ -14,4 +14,4 @@ const DeploymentSchema = new Schema({
DeploymentSchema.set('timestamps', true);
export default mongoose.model('deployment', DeploymentSchema);
export default mongoose.models['deployment'] || mongoose.model('deployment', DeploymentSchema);

View File

@@ -1,5 +1,5 @@
import mongoose from 'mongoose';
import { version } from '../../../package.json';
import { version } from '../../package.json';
const { Schema, Document } = mongoose;
// export interface ILogsServer extends Document {
@@ -20,4 +20,4 @@ const LogsServerSchema = new Schema({
LogsServerSchema.set('timestamps', { createdAt: 'createdAt', updatedAt: false });
export default mongoose.model('logs-server', LogsServerSchema);
export default mongoose.models['logs-server'] || mongoose.model('logs-server', LogsServerSchema);

View File

@@ -14,4 +14,4 @@ const SettingsSchema = new Schema({
SettingsSchema.set('timestamps', true);
export default mongoose.model('settings', SettingsSchema);
export default mongoose.models['settings'] || mongoose.model('settings', SettingsSchema);

View File

@@ -14,4 +14,4 @@ const UserSchema = new Schema({
UserSchema.set('timestamps', true);
export default mongoose.model('user', UserSchema);
export default mongoose.models['user'] || mongoose.model('user', UserSchema);

View File

@@ -1,6 +1,6 @@
<script context="module" lang="ts">
import { publicPages } from '$lib/consts';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
/**
* @type {import('@sveltejs/kit').Load}
*/
@@ -306,7 +306,7 @@
>
{:else}
<button
class="opacity-50 tracking-tight font-bold text-xs rounded px-2 cursor-not-allowed"
class="opacity-50 tracking-tight font-bold text-xs rounded px-2 cursor-not-allowed"
disabled={upgradeDisabled}>Upgrading. It could take a while, please wait...</button
>
{/if}

View File

@@ -1,42 +0,0 @@
import type { Request } from '@sveltejs/kit';
// export async function api(request: Request, resource: string, data?: {}) {
// const base = 'https://github.com/';
// if (!request.context.isLoggedIn) {
// return { status: 401, body: 'Unauthorized' };
// }
// const res = await fetch(`${base}${resource}`, {
// method: request.method,
// headers: {
// 'content-type': 'application/json'
// },
// body: data && JSON.stringify(data)
// });
// return {
// status: res.status,
// body: await res.json()
// };
// }
export async function githubAPI(
request: Request,
resource: string,
token?: string,
data?: Record<string, unknown>
) {
const base = 'https://api.github.com';
const res = await fetch(`${base}${resource}`, {
method: request.method,
headers: {
'content-type': 'application/json',
accept: 'application/json',
authorization: token ? `token ${token}` : ''
},
body: data && JSON.stringify(data)
});
return {
status: res.status,
body: await res.json()
};
}

View File

@@ -1,32 +1,18 @@
import { setDefaultConfiguration } from '$lib/api/applications/configuration';
import { saveServerLog } from '$lib/api/applications/logging';
import { docker } from '$lib/api/docker';
import Configuration from '$models/Configuration';
import type { Request } from '@sveltejs/kit';
export async function post(request: Request) {
try {
const { DOMAIN } = process.env;
const configuration = setDefaultConfiguration(request.body);
const services = (await docker.engine.listServices()).filter(
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
);
let foundDomain = false;
for (const service of services) {
const running = JSON.parse(service.Spec.Labels.configuration);
if (running) {
if (
running.publish.domain === configuration.publish.domain &&
running.repository.id !== configuration.repository.id &&
running.publish.path === configuration.publish.path
) {
foundDomain = true;
}
}
}
if (DOMAIN === configuration.publish.domain) foundDomain = true;
if (foundDomain) {
const configurationFound = await Configuration.find({
'repository.id': { $ne: configuration.repository.id },
'publish.domain': configuration.publish.domain
}).select('-_id -__v -createdAt -updatedAt');
if (configurationFound.length > 0 || configuration.publish.domain === DOMAIN) {
return {
status: 200,
body: {
@@ -44,7 +30,7 @@ export async function post(request: Request) {
return {
status: 500,
body: {
error
error: error.message || error
}
};
}

View File

@@ -1,9 +1,25 @@
import { docker } from '$lib/api/docker';
import Configuration from '$models/Configuration';
import type { Request } from '@sveltejs/kit';
export async function post(request: Request) {
const { name, organization, branch }: any = request.body || {};
if (name && organization && branch) {
const configurationFound = await Configuration.find({
'repository.name': name,
'repository.organization': organization,
'repository.branch': branch
}).select('-_id -__v -createdAt -updatedAt');
if (configurationFound) {
return {
status: 200,
body: {
configuration: [...configurationFound]
}
};
}
const services = await docker.engine.listServices();
const applications = services.filter(
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
@@ -30,6 +46,7 @@ export async function post(request: Request) {
}
return null;
});
if (found) {
return {
status: 200,
@@ -38,13 +55,12 @@ export async function post(request: Request) {
...JSON.parse(found.Spec.Labels.configuration)
}
};
} else {
return {
status: 500,
body: {
error: 'No configuration found.'
}
};
}
return {
status: 500,
body: {
error: 'No configuration found.'
}
};
}
}

View File

@@ -0,0 +1,75 @@
import { updateServiceLabels } from '$lib/api/applications/configuration';
import { execShellAsync } from '$lib/api/common';
import { docker } from '$lib/api/docker';
import ApplicationLog from '$models/ApplicationLog';
import Configuration from '$models/Configuration';
import Deployment from '$models/Deployment';
import type { Request } from '@sveltejs/kit';
export async function post(request: Request) {
const { name, organization, branch, isPreviewDeploymentEnabled }: any = request.body || {};
if (name && organization && branch) {
const configuration = await Configuration.findOneAndUpdate(
{
'repository.name': name,
'repository.organization': organization,
'repository.branch': branch
},
{
$set: {
'general.isPreviewDeploymentEnabled': isPreviewDeploymentEnabled,
'general.pullRequest': 0
}
},
{ new: true }
).select('-_id -__v -createdAt -updatedAt');
if (!isPreviewDeploymentEnabled) {
const found = await Configuration.find({
'repository.name': name,
'repository.organization': organization,
'repository.branch': branch,
'general.pullRequest': { $ne: 0 }
});
for (const prDeployment of found) {
await Configuration.findOneAndRemove({
'repository.name': name,
'repository.organization': organization,
'repository.branch': branch,
'publish.domain': prDeployment.publish.domain
});
const deploys = await Deployment.find({
organization,
branch,
name,
domain: prDeployment.publish.domain
});
for (const deploy of deploys) {
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
await Deployment.deleteMany({ deployId: deploy.deployId });
}
await execShellAsync(`docker stack rm ${prDeployment.build.container.name}`);
}
return {
status: 200,
body: {
organization,
name,
branch
}
};
}
updateServiceLabels(configuration);
return {
status: 200,
body: {
success: true
}
};
}
return {
status: 500,
body: {
error: 'Cannot save.'
}
};
}

View File

@@ -1,31 +1,26 @@
import type { Request } from '@sveltejs/kit';
import Deployment from '$models/Logs/Deployment';
import { docker } from '$lib/api/docker';
import Deployment from '$models/Deployment';
import { precheckDeployment, setDefaultConfiguration } from '$lib/api/applications/configuration';
import cloneRepository from '$lib/api/applications/cloneRepository';
import { cleanupTmp } from '$lib/api/common';
import queueAndBuild from '$lib/api/applications/queueAndBuild';
export async function post(request: Request) {
let configuration;
try {
const services = (await docker.engine.listServices()).filter(
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
);
configuration = setDefaultConfiguration(request.body);
import Configuration from '$models/Configuration';
if (!configuration) {
return {
status: 500,
body: {
error: 'Whaaat?'
}
};
}
export async function post(request: Request) {
const configuration = setDefaultConfiguration(request.body);
if (!configuration) {
return {
status: 500,
body: {
error: 'Whaaat?'
}
};
}
try {
await cloneRepository(configuration);
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({
services,
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment(
configuration
});
);
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
cleanupTmp(configuration.general.workdir);
return {
@@ -53,9 +48,35 @@ export async function post(request: Request) {
}
};
}
const { id, organization, name, branch } = configuration.repository;
const { domain } = configuration.publish;
const { deployId, nickname, pullRequest } = configuration.general;
await new Deployment({
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 {
status: 200,
status: 201,
body: {
message: 'Deployment queued.',
nickname: configuration.general.nickname,
@@ -64,26 +85,28 @@ export async function post(request: Request) {
}
};
} catch (error) {
console.log(error);
await Deployment.findOneAndUpdate(
{
repoId: configuration.repository.id,
branch: configuration.repository.branch,
organization: configuration.repository.organization,
name: configuration.repository.name,
domain: configuration.publish.domain,
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'
domain: configuration.publish.domain,
progress: 'failed'
}
);
return {
status: 500,
body: {
error
error: error.message || error
}
};
}

View File

@@ -1,6 +1,6 @@
import type { Request } from '@sveltejs/kit';
import ApplicationLog from '$models/Logs/Application';
import Deployment from '$models/Logs/Deployment';
import ApplicationLog from '$models/ApplicationLog';
import Deployment from '$models/Deployment';
import dayjs from 'dayjs';
export async function get(request: Request) {
@@ -10,10 +10,7 @@ export async function get(request: Request) {
.select('-_id -__v')
.sort({ createdAt: 'asc' });
const deploy: any = await Deployment.findOne({ deployId })
.select('-_id -__v')
.sort({ createdAt: 'desc' });
const deploy: any = await Deployment.findOne({ deployId }).select('-_id -__v');
const finalLogs: any = {};
finalLogs.progress = deploy.progress;
finalLogs.events = logs.map((log) => log.event);
@@ -24,11 +21,11 @@ export async function get(request: Request) {
...finalLogs
}
};
} catch (e) {
} catch (error) {
return {
status: 500,
body: {
error: e
error: error.message || error
}
};
}

View File

@@ -2,7 +2,7 @@ import type { Request } from '@sveltejs/kit';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import relativeTime from 'dayjs/plugin/relativeTime.js';
import Deployment from '$models/Logs/Deployment';
import Deployment from '$models/Deployment';
dayjs.extend(utc);
dayjs.extend(relativeTime);
export async function get(request: Request) {
@@ -10,22 +10,18 @@ export async function get(request: Request) {
const repoId = request.query.get('repoId');
const branch = request.query.get('branch');
const page = request.query.get('page');
const onePage = 5;
const show = Number(page) * onePage || 5;
const deploy: any = await Deployment.find({ repoId, branch })
.select('-_id -__v -repoId')
.sort({ createdAt: 'desc' })
.limit(show);
const finalLogs = deploy.map((d) => {
const finalLogs = { ...d._doc };
const updatedAt = dayjs(d.updatedAt).utc();
finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000;
finalLogs.since = updatedAt.fromNow();
finalLogs.isPr = d.domain.startsWith('pr');
return finalLogs;
});
return {
@@ -36,11 +32,10 @@ export async function get(request: Request) {
}
};
} catch (error) {
console.log(error);
return {
status: 500,
body: {
error
error: error.message || error
}
};
}

View File

@@ -16,11 +16,12 @@ export async function get(request: Request) {
body: { success: true, logs }
};
} catch (error) {
console.log(error);
await saveServerLog(error);
return {
status: 500,
body: {
error
error: 'No such service. Is it under deployment?'
}
};
}

View File

@@ -1,55 +1,69 @@
import { purgeImagesContainers } from '$lib/api/applications/cleanup';
import { docker } from '$lib/api/docker';
import Deployment from '$models/Logs/Deployment';
import ApplicationLog from '$models/Logs/Application';
import Deployment from '$models/Deployment';
import ApplicationLog from '$models/ApplicationLog';
import { delay, execShellAsync } from '$lib/api/common';
import Configuration from '$models/Configuration';
async function call(found) {
async function purgeImagesAsync(found) {
await delay(10000);
await purgeImagesContainers(found, true);
}
export async function post(request: Request) {
const { organization, name, branch } = request.body;
let found = false;
const { organization, name, branch, domain } = request.body;
try {
(await docker.engine.listServices())
.filter((r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
.map((s) => {
const running = JSON.parse(s.Spec.Labels.configuration);
if (
running.repository.organization === organization &&
running.repository.name === name &&
running.repository.branch === branch
) {
found = running;
const configurationFound = await Configuration.findOne({
'repository.organization': organization,
'repository.name': name,
'repository.branch': branch,
'publish.domain': domain
});
if (configurationFound) {
const id = configurationFound._id;
if (configurationFound?.general?.pullRequest === 0) {
// Main deployment deletion request; deleting main + PRs
const allConfiguration = await Configuration.find({
'repository.name': name,
'repository.organization': organization,
'repository.branch': branch
});
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}`);
}
return null;
});
if (found) {
const deploys = await Deployment.find({ organization, branch, name });
for (const deploy of deploys) {
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
await Deployment.deleteMany({ deployId: deploy.deployId });
const deploys = await Deployment.find({ organization, branch, name });
for (const deploy of deploys) {
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
await Deployment.deleteMany({ deployId: deploy.deployId });
}
purgeImagesAsync(configurationFound);
} else {
// Delete only PRs
await Configuration.findByIdAndRemove(id);
await execShellAsync(`docker stack rm ${configurationFound.build.container.name}`);
const deploys = await Deployment.find({ organization, branch, name, domain });
for (const deploy of deploys) {
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
await Deployment.deleteMany({ deployId: deploy.deployId });
}
purgeImagesAsync(configurationFound);
}
await execShellAsync(`docker stack rm ${found.build.container.name}`);
call(found);
return {
status: 200,
body: {
organization,
name,
branch
}
};
} else {
return {
status: 500,
error: {
message: 'Nothing to do.'
}
};
}
return {
status: 200,
body: {
organization,
name,
branch
}
};
} catch (error) {
console.log(error);
return {
status: 500,
error: {

View File

@@ -1,16 +1,9 @@
import { docker } from '$lib/api/docker';
import LogsServer from '$models/Logs/Server';
import type { Request } from '@sveltejs/kit';
import Configuration from '$models/Configuration';
export async function get(request: Request) {
const serverLogs = await LogsServer.find();
// Should update this to get data from mongodb and update db with the currently running services on start!
const dockerServices = await docker.engine.listServices();
let applications: any = dockerServices.filter(
(r) =>
r.Spec.Labels.managedBy === 'coolify' &&
r.Spec.Labels.type === 'application' &&
r.Spec.Labels.configuration
);
let databases: any = dockerServices.filter(
(r) =>
r.Spec.Labels.managedBy === 'coolify' &&
@@ -23,15 +16,6 @@ export async function get(request: Request) {
r.Spec.Labels.type === 'service' &&
r.Spec.Labels.configuration
);
applications = applications.map((r) => {
if (JSON.parse(r.Spec.Labels.configuration)) {
return {
configuration: JSON.parse(r.Spec.Labels.configuration),
UpdatedAt: r.UpdatedAt
};
}
return {};
});
databases = databases.map((r) => {
if (JSON.parse(r.Spec.Labels.configuration)) {
return {
@@ -49,19 +33,27 @@ export async function get(request: Request) {
}
return {};
});
applications = [
...new Map(
applications.map((item) => [
item.configuration.publish.domain + item.configuration.publish.path,
item
])
).values()
];
const configurations = await Configuration.find({
'general.pullRequest': { $in: [null, 0] }
}).select('-_id -__v -createdAt');
const applications = [];
for (const configuration of configurations) {
const foundPRDeployments = await Configuration.find({
'repository.id': configuration.repository.id,
'repository.branch': configuration.repository.branch,
'general.pullRequest': { $ne: 0 }
}).select('-_id -__v -createdAt');
const payload = {
configuration,
UpdatedAt: configuration.updatedAt,
prBuilds: foundPRDeployments.length > 0 ? true : false
};
applications.push(payload);
}
return {
status: 200,
body: {
success: true,
serverLogs,
applications: {
deployed: applications
},

View File

@@ -100,6 +100,31 @@ export async function post(request: Request) {
body: fs.readFileSync(`${fullfilename}`)
};
}
} else if (type === 'redis') {
if (databaseService) {
const password = configuration.database.passwords[0];
const databaseName = configuration.database.defaultDatabaseName;
const filename = `${databaseName}_${now.getTime()}.rdb`;
const fullfilename = `${tmpdir}/${filename}`;
await execShellAsync(
`docker exec -i ${containerID} /bin/bash -c "redis-cli --pass ${password} save"`
);
await execShellAsync(
`docker cp ${containerID}:/bitnami/redis/data/dump.rdb ${fullfilename}`
);
await execShellAsync(
`docker exec -i ${containerID} /bin/bash -c "rm -f /bitnami/redis/data/dump.rdb"`
);
return {
status: 200,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Transfer-Encoding': 'binary',
'Content-Disposition': `attachment; filename=${filename}`
},
body: fs.readFileSync(`${fullfilename}`)
};
}
}
return {
status: 501,
@@ -108,12 +133,11 @@ export async function post(request: Request) {
}
};
} catch (error) {
console.log(error);
await saveServerLog(error);
return {
status: 500,
body: {
error
error: error.message || error
}
};
} finally {

View File

@@ -96,6 +96,12 @@ export async function post(request: Request) {
hard: 262144
}
};
} else if (type === 'redis') {
image = 'bitnami/redis';
volume = `${configuration.general.deployId}-${type}-data:/bitnami/redis/data`;
generateEnvs = {
REDIS_PASSWORD: passwords[0]
};
}
const stack = {

View File

@@ -1,10 +1,10 @@
import { githubAPI } from '$api';
import type { Request } from '@sveltejs/kit';
import mongoose from 'mongoose';
import User from '$models/User';
import Settings from '$models/Settings';
import cuid from 'cuid';
import jsonwebtoken from 'jsonwebtoken';
import { githubAPI } from '$lib/api/github';
export async function get(request: Request) {
const code = request.query.get('code');
@@ -17,7 +17,7 @@ export async function get(request: Request) {
{ headers: { accept: 'application/json' } }
)
).json();
const { avatar_url, id } = await (await githubAPI(request, '/user', access_token)).body;
const { avatar_url } = await (await githubAPI(request, '/user', access_token)).body;
const email = (await githubAPI(request, '/user/emails', access_token)).body.filter(
(e) => e.primary
)[0].email;
@@ -41,11 +41,10 @@ export async function get(request: Request) {
try {
await newUser.save();
await defaultSettings.save();
} catch (e) {
console.log(e);
} catch (error) {
return {
status: 500,
body: e
error: error.message || error
};
}
} else {
@@ -73,12 +72,11 @@ export async function get(request: Request) {
});
try {
await newUser.save();
} catch (e) {
console.log(e);
} catch (error) {
return {
status: 500,
body: {
error: e
error: error.message || error
}
};
}
@@ -103,8 +101,6 @@ export async function get(request: Request) {
}
};
} catch (error) {
console.log('error happened');
console.log(error);
return { status: 500, body: { ...error } };
return { status: 500, body: { error: error.message || error } };
}
}

View File

@@ -4,6 +4,7 @@ import type { Request } from '@sveltejs/kit';
export async function get(request: Request) {
const { serviceName } = request.params;
try {
const service = (await docker.engine.listServices()).find(
(r) =>
@@ -34,12 +35,11 @@ export async function get(request: Request) {
};
}
} catch (error) {
console.log(error);
return {
status: 500,
body: {
success: false,
error
error: error.message || error
}
};
}

View File

@@ -0,0 +1,59 @@
import type { Request } from '@sveltejs/kit';
import yaml from 'js-yaml';
import { promises as fs } from 'fs';
import { docker } from '$lib/api/docker';
import { baseServiceConfiguration } from '$lib/api/applications/common';
import { cleanupTmp, execShellAsync } from '$lib/api/common';
export async function post(request: Request) {
let { baseURL } = request.body;
const traefikURL = baseURL;
baseURL = `https://${baseURL}`;
const workdir = '/tmp/nocodb';
const deployId = 'nocodb';
const stack = {
version: '3.8',
services: {
[deployId]: {
image: 'nocodb/nocodb',
networks: [`${docker.network}`],
deploy: {
...baseServiceConfiguration,
labels: [
'managedBy=coolify',
'type=service',
'serviceName=nocodb',
'configuration=' +
JSON.stringify({
baseURL
}),
'traefik.enable=true',
'traefik.http.services.' + deployId + '.loadbalancer.server.port=8080',
'traefik.http.routers.' + deployId + '.entrypoints=websecure',
'traefik.http.routers.' +
deployId +
'.rule=Host(`' +
traefikURL +
'`) && PathPrefix(`/`)',
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
'traefik.http.routers.' + deployId + '.middlewares=global-compress'
]
}
}
},
networks: {
[`${docker.network}`]: {
external: true
}
}
};
await execShellAsync(`mkdir -p ${workdir}`);
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));
await execShellAsync('docker stack rm nocodb');
await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`);
cleanupTmp(workdir);
return {
status: 200,
body: { message: 'OK' }
};
}

View File

@@ -22,7 +22,7 @@ export async function get(request: Request) {
return {
status: 500,
body: {
error
error: error.message || error
}
};
}
@@ -45,7 +45,7 @@ export async function post(request: Request) {
return {
status: 500,
body: {
error
error: error.message || error
}
};
}

View File

@@ -10,7 +10,6 @@ export async function get(request: Request) {
execShellAsync(
'docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -u root coolify bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p2.sh)"'
);
// saveServerLog({ message: upgradeP2, type: 'UPGRADE-P-2' })
return {
status: 200,
body: {

View File

@@ -1,24 +0,0 @@
// import { deleteCookies } from '$lib/api/common';
// import { verifyUserId } from '$lib/api/common';
// import type { Request } from '@sveltejs/kit';
// import * as cookie from 'cookie';
// export async function post(request: Request) {
// const { coolToken } = cookie.parse(request.headers.cookie || '');
// try {
// await verifyUserId(coolToken);
// return {
// status: 200,
// body: { success: true }
// };
// } catch (error) {
// return {
// status: 301,
// headers: {
// location: '/',
// 'set-cookie': [...deleteCookies]
// },
// body: { error: 'Unauthorized' }
// };
// }
// }

View File

@@ -1,13 +1,19 @@
import type { Request } from '@sveltejs/kit';
import crypto from 'crypto';
import Deployment from '$models/Logs/Deployment';
import Deployment from '$models/Deployment';
import { docker } from '$lib/api/docker';
import { precheckDeployment, setDefaultConfiguration } from '$lib/api/applications/configuration';
import cloneRepository from '$lib/api/applications/cloneRepository';
import { cleanupTmp } from '$lib/api/common';
import { cleanupTmp, execShellAsync } from '$lib/api/common';
import queueAndBuild from '$lib/api/applications/queueAndBuild';
import Configuration from '$models/Configuration';
import ApplicationLog from '$models/ApplicationLog';
import { cleanupStuckedDeploymentsInDB } from '$lib/api/applications/cleanup';
export async function post(request: Request) {
let configuration;
const allowedGithubEvents = ['push', 'pull_request'];
const allowedPRActions = ['opened', 'reopened', 'synchronize', 'closed'];
const githubEvent = request.headers['x-github-event'];
const { GITHUP_APP_WEBHOOK_SECRET } = process.env;
const hmac = crypto.createHmac('sha256', GITHUP_APP_WEBHOOK_SECRET);
const digest = Buffer.from(
@@ -19,52 +25,96 @@ export async function post(request: Request) {
return {
status: 500,
body: {
error: 'Invalid request'
error: 'Invalid request.'
}
};
}
if (request.headers['x-github-event'] !== 'push') {
if (!allowedGithubEvents.includes(githubEvent)) {
return {
status: 500,
body: {
error: 'Not a push event.'
error: 'Event not allowed.'
}
};
}
try {
const services = (await docker.engine.listServices()).filter(
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
);
configuration = services.find((r) => {
if (request.body.ref.startsWith('refs')) {
const branch = request.body.ref.split('/')[2];
if (
JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id &&
JSON.parse(r.Spec.Labels.configuration).repository.branch === branch
) {
return r;
const applications = await Configuration.find({
'repository.id': request.body.repository.id
}).select('-_id -__v -createdAt -updatedAt');
if (githubEvent === 'push') {
configuration = applications.find((r) => {
if (request.body.ref.startsWith('refs')) {
if (r.repository.branch === request.body.ref.split('/')[2]) {
return r;
}
}
return null;
});
} else if (githubEvent === 'pull_request') {
if (!allowedPRActions.includes(request.body.action)) {
return {
status: 500,
body: {
error: 'PR action is not allowed.'
}
};
}
return null;
});
configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration));
configuration = applications.find(
(r) => r.repository.branch === request.body['pull_request'].base.ref
);
if (configuration) {
if (!configuration.general.isPreviewDeploymentEnabled) {
return {
status: 500,
body: {
error: 'PR deployments are not enabled.'
}
};
}
configuration.general.pullRequest = request.body.number;
}
}
if (!configuration) {
return {
status: 500,
body: {
error: 'Whaaat?'
error: 'No configuration found.'
}
};
}
configuration = setDefaultConfiguration(configuration);
const { id, organization, name, branch } = configuration.repository;
const { domain } = configuration.publish;
const { deployId, nickname, pullRequest } = configuration.general;
if (request.body.action === 'closed') {
const deploys = await Deployment.find({ organization, branch, name, domain });
for (const deploy of deploys) {
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
await Deployment.deleteMany({ deployId: deploy.deployId });
}
await Configuration.findOneAndRemove({
'repository.id': id,
'repository.organization': organization,
'repository.name': name,
'repository.branch': branch,
'general.pullRequest': pullRequest
});
await execShellAsync(`docker stack rm ${configuration.build.container.name}`);
return {
status: 200,
body: {
success: true,
message: 'Removed'
}
};
}
await cloneRepository(configuration);
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({
services,
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment(
configuration
});
);
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
cleanupTmp(configuration.general.workdir);
return {
@@ -76,11 +126,11 @@ export async function post(request: Request) {
};
}
const alreadyQueued = await Deployment.find({
repoId: configuration.repository.id,
branch: configuration.repository.branch,
organization: configuration.repository.organization,
name: configuration.repository.name,
domain: configuration.publish.domain,
repoId: id,
branch: branch,
organization: organization,
name: name,
domain: domain,
progress: { $in: ['queued', 'inprogress'] }
});
if (alreadyQueued.length > 0) {
@@ -92,6 +142,43 @@ export async function post(request: Request) {
}
};
}
await new Deployment({
repoId: id,
branch,
deployId,
domain,
organization,
name,
nickname
}).save();
if (githubEvent === 'pull_request') {
await Configuration.findOneAndUpdate(
{
'repository.id': id,
'repository.organization': organization,
'repository.name': name,
'repository.branch': branch,
'general.pullRequest': pullRequest
},
{ ...configuration },
{ upsert: true, new: true }
);
} else {
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 {
status: 201,
@@ -103,11 +190,40 @@ export async function post(request: Request) {
}
};
} catch (error) {
console.log(error);
// console.log(configuration)
if (configuration) {
cleanupTmp(configuration.general.workdir);
await Deployment.findOneAndUpdate(
{
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 {
status: 500,
body: {
error
error: error.message || error
}
};
} finally {
try {
await cleanupStuckedDeploymentsInDB();
} catch (error) {
console.log(error);
}
}
}

View File

@@ -2,7 +2,7 @@
import { onDestroy, onMount } from 'svelte';
import { fade } from 'svelte/transition';
import Loading from '$components/Loading.svelte';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { page, session } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/env';

View File

@@ -5,7 +5,7 @@
import { onDestroy, onMount } from 'svelte';
import Loading from '$components/Loading.svelte';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { session } from '$app/stores';
import { goto } from '$app/navigation';
@@ -84,13 +84,15 @@
deployment.progress !== 'failed'}
class:bg-warmGray-800={deployment.progress !== 'done' &&
deployment.progress !== 'failed'}
class:hover:bg-red-200={deployment.progress === 'failed'}
class:hover:border-red-500={deployment.progress === 'failed'}
on:click={() => goto(`./logs/${deployment.deployId}`)}
>
<div class="font-bold text-sm px-3 flex justify-center items-center">
{deployment.branch}
<div class="flex space-x-2 px-2">
<div class="font-bold text-sm flex justify-center items-center">
{deployment.branch}
</div>
<div class="font-bold text-xs flex justify-center items-center text-warmGray-500">{deployment.isPr ? 'PR' : ''}</div>
</div>
<div class="flex-1" />
<div class="px-3 w-48">
<div

View File

@@ -1,12 +1,12 @@
<script>
import { application, initialApplication, initConf, dashboard } from '$store';
import { application, initialApplication, initConf, dashboard, prApplication } from '$store';
import { onDestroy } from 'svelte';
import Loading from '$components/Loading.svelte';
import Navbar from '$components/Application/Navbar.svelte';
import { page, session } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/env';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
$application.repository.organization = $page.params.organization;
$application.repository.name = $page.params.name;
@@ -14,15 +14,16 @@
async function setConfiguration() {
try {
const config = await request(`/api/v1/application/config`, $session, {
const { configuration } = await request(`/api/v1/application/config`, $session, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch
}
});
$application = { ...config };
$initConf = JSON.parse(JSON.stringify($application));
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
$application = configuration.find((c) => c.general.pullRequest === 0);
if (!$application) browser && goto('/dashboard/applications');
} catch (error) {
browser && goto('/dashboard/applications');
}

View File

@@ -1,5 +1,5 @@
<script context="module" lang="ts">
import { request } from '$lib/api/request';
import { request } from '$lib/request';
/**
* @type {import('@sveltejs/kit').Load}
*/

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,8 @@
import Mysql from '$components/Database/SVGs/Mysql.svelte';
import { dashboard } from '$store';
import { fade } from 'svelte/transition';
import Redis from '$components/Database/SVGs/Redis.svelte';
</script>
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
@@ -56,6 +58,10 @@
<CouchDb
customClass="w-10 h-10 fill-current text-red-600 absolute top-0 left-0 -m-4"
/>
{:else if database.configuration.general.type == 'redis'}
<Redis
customClass="w-10 h-10 absolute top-0 left-0 -m-4"
/>
{:else if database.configuration.general.type == 'clickhouse'}
<Clickhouse
customClass="w-10 h-10 fill-current text-red-600 absolute top-0 left-0 -m-4"
@@ -79,4 +85,3 @@
<div class="text-2xl font-bold text-center">No databases found</div>
{/if}
</div>

View File

@@ -49,6 +49,15 @@
/>
<div class="text-white font-bold">Plausible Analytics</div>
</div>
{:else if service.serviceName == 'nocodb'}
<div>
<img
alt="nocodedb"
class="w-10 absolute top-0 left-0 -m-6"
src="https://cdn.coollabs.io/assets/coolify/services/nocodb/nocodb.png"
/>
<div class="text-white font-bold">NocoDB</div>
</div>
{/if}
</div>
</div>

View File

@@ -1,7 +1,7 @@
<script>
import { database } from '$store';
import { page, session } from '$app/stores';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { fade } from 'svelte/transition';
import { goto } from '$app/navigation';
import MongoDb from '$components/Database/SVGs/MongoDb.svelte';
@@ -12,10 +12,11 @@
import PasswordField from '$components/PasswordField.svelte';
import { browser } from '$app/env';
import { toast } from '@zerodevx/svelte-toast';
import Redis from '$components/Database/SVGs/Redis.svelte';
async function backup() {
try {
await request(`/api/v1/databases/${$page.params.name}/backup`, $session, {body: {}});
await request(`/api/v1/databases/${$page.params.name}/backup`, $session, { body: {} });
browser && toast.push(`Successfully created backup.`);
} catch (error) {
@@ -56,6 +57,8 @@
<Mysql customClass="w-8 h-8" />
{:else if $database.config.general.type === 'couchdb'}
<CouchDb customClass="w-8 h-8 fill-current text-red-600" />
{:else if $database.config.general.type === 'redis'}
<Redis customClass="w-8 h-8" />
{/if}
</div>
</div>
@@ -81,6 +84,10 @@
<PasswordField
value={`http://${$database.envs.COUCHDB_USER}:${$database.envs.COUCHDB_PASSWORD}@${$database.config.general.deployId}:5984`}
/>
{:else if $database.config.general.type === 'redis'}
<PasswordField
value={`redis://${$database.envs.REDIS_PASSWORD}@${$database.config.general.deployId}:6379`}
/>
{:else if $database.config.general.type === 'clickhouse'}
<!-- {JSON.stringify($database)} -->
<!-- <textarea
@@ -97,6 +104,12 @@
<PasswordField value={$database.envs.MONGODB_ROOT_PASSWORD} />
</div>
{/if}
{#if $database.config.general.type === 'redis'}
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Redis password</div>
<PasswordField value={$database.envs.REDIS_PASSWORD} />
</div>
{/if}
<div class="pb-2 pt-5 space-y-4">
<div class="text-2xl font-bold border-gradient w-32">Backup</div>
<div class="pt-4">

View File

@@ -4,7 +4,7 @@
import { goto } from '$app/navigation';
import { page, session } from '$app/stores';
import Tooltip from '$components/Tooltip.svelte';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { database, initialDatabase } from '$store';
import { toast } from '@zerodevx/svelte-toast';
import { onDestroy } from 'svelte';

View File

@@ -3,7 +3,7 @@
import { goto } from '$app/navigation';
import { session } from '$app/stores';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
async function login() {
const left = screen.width / 2 - 1020 / 2;
@@ -20,7 +20,7 @@
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow.closed) {
if (newWindow?.closed) {
clearInterval(timer);
browser && location.reload()
}

View File

@@ -5,7 +5,7 @@
import { page, session } from '$app/stores';
import Tooltip from '$components/Tooltip.svelte';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { toast } from '@zerodevx/svelte-toast';

View File

@@ -3,7 +3,7 @@
import { toast } from '@zerodevx/svelte-toast';
import { page, session } from '$app/stores';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { goto } from '$app/navigation';
import Loading from '$components/Loading.svelte';
import Plausible from '$components/Service/Plausible.svelte';
@@ -14,23 +14,14 @@
try {
service = await request(`/api/v1/services/${$page.params.name}`, $session);
} catch (error) {
browser && toast.push(`Cannot find service ${$page.params.name}?!`);
goto(`/dashboard/services`, { replaceState: true });
if (browser) {
toast.push(`Cannot find service ${$page.params.name}?!`);
goto(`/dashboard/services`, { replaceState: true });
}
}
}
}
async function activate() {
try {
await request(`/api/v1/services/deploy/${$page.params.name}/activate`, $session, {
method: 'PATCH',
body: {}
});
browser && toast.push(`All users are activated for Plausible.`);
} catch (error) {
console.log(error);
browser && toast.push(`Ooops, there was an error activating users for Plausible?!`);
}
}
</script>
{#await loadServiceConfig()}
@@ -38,7 +29,12 @@
{:then}
<div class="min-h-full text-white">
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
<div>{$page.params.name === 'plausible' ? 'Plausible Analytics' : $page.params.name}</div>
{#if $page.params.name === 'plausible'}
<div>Plausible Analytics</div>
{:else if $page.params.name === 'nocodb'}
<div>NocoDB</div>
{/if}
<div class="px-4">
{#if $page.params.name === 'plausible'}
<img
@@ -46,37 +42,38 @@
class="w-6 mx-auto"
src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png"
/>
{:else if $page.params.name === 'nocodb'}
<img
alt="nocodb logo"
class="w-8 mx-auto"
src="https://cdn.coollabs.io/assets/coolify/services/nocodb/nocodb.png"
/>
{/if}
</div>
<a
target="_blank"
class="icon mx-2"
href={service.config.baseURL}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
<a target="_blank" class="icon mx-2" href={service.config.baseURL}>
<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 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg></a
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg></a
>
</div>
</div>
<div class="space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
<div class="block text-center py-4">
{#if $page.params.name === 'plausible'}
<Plausible {service} />
{:else if $page.params.name === 'nocodb'}
<div class="font-bold">Nothing to show here. Enjoy using NocoDB!</div>
{/if}
</div>
</div>

View File

@@ -5,7 +5,7 @@
import { page, session } from '$app/stores';
import Loading from '$components/Loading.svelte';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { initialNewService, newService } from '$store';
import { toast } from '@zerodevx/svelte-toast';
@@ -14,13 +14,11 @@
async function checkService() {
try {
const data = await request(`/api/v1/services/${$page.params.type}`, $session);
if (!data?.success) {
if (data?.success) {
if (browser) {
goto(`/dashboard/services`, { replaceState: true });
goto(`/service/${$page.params.type}/configuration`, { replaceState: true });
toast.push(
`${
$page.params.type === 'plausible' ? 'Plausible Analytics' : $page.params.type
} already deployed.`
`Service already deployed.`
);
}
}

View File

@@ -4,13 +4,13 @@
import { toast } from '@zerodevx/svelte-toast';
import { newService } from '$store';
import { page, session } from '$app/stores';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { goto } from '$app/navigation';
import Loading from '$components/Loading.svelte';
import TooltipInfo from '$components/TooltipInfo.svelte';
import { browser } from '$app/env';
$: deployable =
$: deployablePlausible =
$newService.baseURL === '' ||
$newService.baseURL === null ||
$newService.email === '' ||
@@ -22,7 +22,7 @@
$newService.userPassword.length <= 6 ||
$newService.userPassword !== $newService.userPasswordAgain;
let loading = false;
async function deploy() {
async function deployPlausible() {
try {
loading = true;
const payload = $newService;
@@ -44,6 +44,30 @@
loading = false;
}
}
async function deployNocodb() {
try {
loading = true;
await request(`/api/v1/services/deploy/${$page.params.type}`, $session, {
body: {
baseURL: $newService.baseURL
}
});
if (browser) {
toast.push(
'Service deployment queued.<br><br><br>It could take 2-5 minutes to be ready, be patient and grab a coffee/tea!',
{ duration: 4000 }
);
goto(`/dashboard/services`, { replaceState: true });
}
} catch (error) {
console.log(error);
browser && toast.push('Oops something went wrong. See console.log.');
} finally {
loading = false;
}
}
</script>
<div class="min-h-full text-white">
@@ -51,18 +75,20 @@
Deploy new
{#if $page.params.type === 'plausible'}
<span class="text-blue-500 px-2 capitalize">Plausible Analytics</span>
{:else if $page.params.type === 'nocodb'}
<span class="text-blue-500 px-2 capitalize">NocoDB</span>
{/if}
</div>
</div>
{#if loading}
<Loading />
{:else}
{:else if $page.params.type === 'plausible'}
<div class="space-y-2 max-w-4xl mx-auto px-6 flex-col text-center" in:fade={{ duration: 100 }}>
<div class="grid grid-flow-row">
<label for="Domain"
>Domain <TooltipInfo
position="right"
label={`You will have your Plausible instance at here.`}
label={`You could reach your Plausible Analytics instance here.`}
/></label
>
<input
@@ -114,15 +140,39 @@
/>
</div>
<button
disabled={deployable}
class:cursor-not-allowed={deployable}
class:bg-blue-500={!deployable}
class:hover:bg-blue-400={!deployable}
class:hover:bg-transparent={deployable}
class:text-warmGray-700={deployable}
class:text-white={!deployable}
disabled={deployablePlausible}
class:cursor-not-allowed={deployablePlausible}
class:bg-blue-500={!deployablePlausible}
class:hover:bg-blue-400={!deployablePlausible}
class:hover:bg-transparent={deployablePlausible}
class:text-warmGray-700={deployablePlausible}
class:text-white={!deployablePlausible}
class="button p-2"
on:click={deploy}
on:click={deployPlausible}
>
Deploy
</button>
</div>
{:else if $page.params.type === 'nocodb'}
<div class="space-y-2 max-w-xl mx-auto px-6 flex-col text-center" in:fade={{ duration: 100 }}>
<div class="grid grid-flow-row pb-5">
<label for="Domain"
>Domain <TooltipInfo
position="right"
label={`You could reach your NocoDB instance here.`}
/></label
>
<input
id="Domain"
class:border-red-500={$newService.baseURL == null || $newService.baseURL == ''}
bind:value={$newService.baseURL}
placeholder="nocodb.coollabs.io"
/>
</div>
<button
class="button p-2 w-64 bg-blue-500 hover:bg-blue-400 text-white"
on:click={deployNocodb}
>
Deploy
</button>

View File

@@ -1,7 +1,6 @@
<script>
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { fade } from 'svelte/transition';
</script>
@@ -14,7 +13,7 @@
{#if $page.path === '/service/new'}
<div class="flex justify-center space-x-4 font-bold pb-6">
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 rounded bg-warmGray-800"
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 rounded bg-warmGray-800 w-48"
on:click={() => goto('/service/new/plausible')}
>
<img
@@ -24,6 +23,18 @@
/>
<div class="text-white">Plausible Analytics</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-white p-2 rounded bg-warmGray-800 w-48"
on:click={() => goto('/service/new/nocodb')}
>
<img
alt="nocodb logo"
class="w-14 mx-auto pb-2"
src="https://cdn.coollabs.io/assets/coolify/services/nocodb/nocodb.png"
/>
<div class="flex-1" />
<div class="text-white">NocoDB</div>
</div>
</div>
{/if}
</div>

View File

@@ -26,10 +26,10 @@
<script>
export let allowRegistration;
export let sendErrors;
import { browser } from '$app/env';
import { session } from '$app/stores';
import { request } from '$lib/api/request';
import { request } from '$lib/request';
import { toast } from '@zerodevx/svelte-toast';
import { fade } from 'svelte/transition';
let settings = {

View File

@@ -5,7 +5,7 @@ import type {
DateTimeFormatOptions,
GithubInstallations
} from 'src/global';
import { writable, derived, readable, Writable } from 'svelte/store';
import { writable } from 'svelte/store';
export const dashboard = writable<Dashboard>({
databases: {
@@ -48,14 +48,21 @@ export const application = writable<Application>({
general: {
deployId: null,
nickname: null,
workdir: null
workdir: null,
isPreviewDeploymentEnabled: false,
pullRequest: 0
},
build: {
pack: 'static',
directory: null,
command: {
build: null,
installation: null
installation: null,
start: null,
python: {
module: null,
instance: null
}
},
container: {
name: null,
@@ -71,6 +78,7 @@ export const application = writable<Application>({
secrets: []
}
});
export const prApplication = writable([]);
export const initConf = writable({});
@@ -92,14 +100,21 @@ export const initialApplication: Application = {
general: {
deployId: null,
nickname: null,
workdir: null
workdir: null,
isPreviewDeploymentEnabled: false,
pullRequest: 0
},
build: {
pack: 'static',
directory: null,
command: {
build: null,
installation: null
installation: null,
start: null,
python: {
module: null,
instance: null
}
},
container: {
name: null,
@@ -152,3 +167,5 @@ export const initialNewService = {
userPasswordAgain: null,
baseURL: null
};
export const isPullRequestPermissionsGranted = writable(false);

View File

@@ -30,7 +30,7 @@ export default {
alias: {
$components: path.resolve('./src/components/'),
$store: path.resolve('./src/store/index.ts'),
$api: path.resolve('./src/routes/api/_index.ts'),
$api: path.resolve('./src/routes/api/'),
$models: path.resolve('./src/models/')
}
}

View File

@@ -8,30 +8,6 @@ const svelteClassColonExtractor = (content) => {
module.exports = {
mode: 'jit',
purge: ['./**/*.html', './src/**/*.{js,jsx,ts,tsx,svelte}'],
// purge: {
// enabled: process.env.NODE_ENV === 'production',
// content: ['./src/**/*.svelte', './src/**/*.html', './src/**/*.css', './index.html'],
// preserveHtmlElements: true,
// options: {
// safelist: [
// /svelte-/,
// 'border-green-500',
// 'border-yellow-300',
// 'border-red-500',
// 'hover:border-green-500',
// 'hover:border-red-200',
// 'hover:bg-red-200',
// 'hover:bg-warmGray-900',
// 'hover:bg-transparent'
// ],
// defaultExtractor: (content) => {
// // WARNING: tailwindExtractor is internal tailwind api
// // if this breaks after a tailwind update, report to svite repo
// return [...tailwindExtractor(content), ...svelteClassColonExtractor(content)];
// },
// keyframes: false
// }
// },
important: true,
theme: {
extend: {

View File

@@ -26,7 +26,7 @@
"paths": {
"$lib/*": ["src/lib/*"],
"$store": ["src/store/index.ts"],
"$api": ["src/routes/api/_index.ts"],
"$api/*": ["src/routes/api/*"],
"$models/*": ["src/models/*"],
"$components/*": ["src/components/*"]
}