Compare commits

...

15 Commits

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

# Fixes
- Fix wrong redirects
- Logout fix for the session manager
2021-07-16 23:42:14 +02:00
Andras Bacsai
2d0f22b379 v1.0.22 (#67) 2021-06-24 23:31:08 +02:00
Andras Bacsai
a8e9668c2b Haha 2021-06-22 12:19:12 +02:00
Andras Bacsai
425feba0e2 It's working! 2021-06-22 12:12:11 +02:00
Andras Bacsai
c09b8d888f Hmm, it should work now, right? (Please... work..) 2021-06-22 12:07:51 +02:00
Andras Bacsai
748e691a58 Hmm, ok 2021-06-22 12:02:06 +02:00
Andras Bacsai
f8c81ff95f Nooo 2021-06-22 11:54:19 +02:00
Andras Bacsai
d11c4a3cd7 Fix the fix of fix 2021-06-22 11:45:46 +02:00
Andras Bacsai
3f3ea151ef Fixes are not fixing 2021-06-22 11:40:02 +02:00
Andras Bacsai
7e2f68870c Hm, soemthing is not working 2021-06-22 11:20:21 +02:00
Andras Bacsai
df41cf14da Fix login again 2021-06-22 11:12:30 +02:00
Andras Bacsai
111370c025 Fix email login 2021-06-22 10:56:38 +02:00
Andras Bacsai
bcb2ba0b1b Fix Get Started button 2021-06-22 10:44:08 +02:00
Andras Bacsai
807d526ffa v1.0.21 (#66) 2021-06-22 10:19:20 +02:00
Andras Bacsai
2ff9c5fed5 v1.0.20 (#65) 2021-06-18 21:16:05 +02:00
59 changed files with 2204 additions and 710 deletions

View File

@@ -48,6 +48,7 @@ With Github integration
### Services
- [WordPress](https://wordpress.org)
- [Plausible Analytics](https://plausible.io)
- [NocoDB](https://nocodb.com)
- [VSCode Server](https://github.com/cdr/code-server)

View File

@@ -1,12 +1,12 @@
{
"name": "coolify",
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
"version": "1.0.19",
"version": "1.0.23",
"license": "AGPL-3.0",
"scripts": {
"dev:docker:start": "docker-compose -f docker-compose-dev.yml up -d",
"dev:docker:stop": "docker-compose -f docker-compose-dev.yml down",
"dev": "NODE_ENV=development svelte-kit dev --host 0.0.0.0",
"dev": "TAILWIND_MODE=watch NODE_ENV=development svelte-kit dev --host 0.0.0.0",
"build": "NODE_ENV=production svelte-kit build",
"preview": "svelte-kit preview",
"start": "node build",
@@ -14,8 +14,8 @@
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.26",
"@sveltejs/kit": "1.0.0-next.115",
"@sveltejs/adapter-node": "1.0.0-next.33",
"@sveltejs/kit": "1.0.0-next.125",
"@types/dockerode": "^3.2.3",
"@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1",
@@ -30,29 +30,32 @@
"prettier-plugin-svelte": "^2.3.0",
"svelte": "^3.38.2",
"svelte-preprocess": "^4.7.3",
"tailwindcss": "2.2.0",
"tailwindcss": "2.2.4",
"tslib": "^2.2.0",
"typescript": "^4.3.2",
"vite": "^2.3.6"
},
"type": "module",
"dependencies": {
"dotenv-extended": "^2.9.0",
"@iarna/toml": "^2.2.5",
"@zerodevx/svelte-toast": "^0.3.0",
"bcrypt": "^5.0.1",
"commander": "^7.2.0",
"compare-versions": "^3.6.0",
"cookie": "^0.4.1",
"cuid": "^2.1.8",
"dayjs": "^1.10.5",
"dockerode": "^3.3.0",
"dotenv-extended": "^2.9.0",
"generate-password": "^1.6.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
"microtip": "^0.2.2",
"mongoose": "^5.12.13",
"shelljs": "^0.8.4",
"svelte-kit-cookie-session": "^1.0.6",
"svelte-select": "^3.17.0",
"systeminformation": "^5.7.7",
"unique-names-generator": "^4.5.0"
}
}

537
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@
import Tabs from '$components/Application/Tabs.svelte';
import Repositories from '$components/Application/Repositories.svelte';
import Login from '$components/Application/Login.svelte';
import { dashify } from '$lib/common';
let loading = {
github: false,
branches: false
@@ -26,15 +27,7 @@
let branches = [];
let relogin = false;
let permissions = {};
function dashify(str: string, options?: any) {
if (typeof str !== 'string') return str;
return str
.trim()
.replace(/\W/g, (m) => (/[À-ž]/.test(m) ? m : '-'))
.replace(/^-+|-+$/g, '')
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase();
}
async function getGithubRepos(id, page) {
return await request(
`https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`,
@@ -174,7 +167,6 @@
}, 100);
}
}
</script>
<div in:fade={{ duration: 100 }}>

View File

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

View File

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

View File

@@ -1,80 +1,83 @@
<script>
export let github = false;
export let githubLoadingText = 'Loading GitHub...';
export let fullscreen = true;
</script>
{#if fullscreen}
{#if github}
<div class="fixed left-0 top-0 flex flex-wrap content-center h-full w-full">
<div class="main flex justify-center items-center">
<div class="w-64">
<svg
class=" w-28 animate-bounce mx-auto"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
/></svg
>
<div class="text-xl font-bold text-center">
{githubLoadingText}
</div>
</div>
</div>
</div>
{:else}
<div class="main fixed left-0 top-0 flex flex-wrap content-center h-full">
<span class="loader" />
</div>
{/if}
{:else}
<div class="main h-64 py-24 left-0 top-0 flex flex-wrap content-center mx-auto">
<span class="loader" />
</div>
{/if}
<style lang="postcss">
.loader {
width: 8px;
height: 40px;
border-radius: 4px;
display: block;
margin: 20px auto;
position: relative;
background: currentColor;
color: #fff;
box-sizing: border-box;
animation: animloader 0.3s 0.3s linear infinite alternate;
}
.loader::after,
.loader::before {
content: "";
width: 8px;
height: 40px;
border-radius: 4px;
background: currentColor;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 20px;
box-sizing: border-box;
animation: animloader 0.3s 0.45s linear infinite alternate;
}
.loader::before {
left: -20px;
animation-delay: 0s;
}
@keyframes animloader {
0% {
height: 48px;
}
100% {
height: 4px;
}
}
</style>
<script>
export let github = false;
export let githubLoadingText = "Loading GitHub...";
export let fullscreen = true;
</script>
{#if fullscreen}
{#if github}
<div class="fixed left-0 top-0 flex flex-wrap content-center h-full w-full">
<div class="main flex justify-center items-center">
<div class="w-64">
<svg
class=" w-28 animate-bounce mx-auto"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
></path></svg
>
<div class="text-xl font-bold text-center">
{githubLoadingText}
</div>
</div>
</div>
</div>
{:else}
<div class="main fixed left-0 top-0 flex flex-wrap content-center h-full">
<span class=" loader"></span>
</div>
{/if}
{/if}
.loader {
width: 8px;
height: 40px;
border-radius: 4px;
display: block;
margin: 20px auto;
position: relative;
background: currentColor;
color: #fff;
box-sizing: border-box;
animation: animloader 0.3s 0.3s linear infinite alternate;
}
.loader::after,
.loader::before {
content: '';
width: 8px;
height: 40px;
border-radius: 4px;
background: currentColor;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 20px;
box-sizing: border-box;
animation: animloader 0.3s 0.45s linear infinite alternate;
}
.loader::before {
left: -20px;
animation-delay: 0s;
}
@keyframes animloader {
0% {
height: 48px;
}
100% {
height: 4px;
}
}
</style>

View File

@@ -1,54 +1,56 @@
<script>
export let value;
let showPassword = false;
export let value;
let showPassword = false;
export let isEditable = false;
</script>
<div class="relative w-full">
<input
type="{showPassword ? 'text' : 'password'}"
class="w-full "
{value}
disabled
/>
<div
class="absolute top-0 my-2 mx-2 right-0 cursor-pointer text-warmGray-600 hover:text-white"
on:click="{() => showPassword = !showPassword}"
>
{#if showPassword}
<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="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
</svg>
{:else}
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg>
{/if}
</div>
{#if showPassword}
<input type="text" class="w-full" bind:value disabled={!isEditable} />
{:else}
<input type="password" class="w-full" bind:value disabled={!isEditable} />
{/if}
<div
class="absolute top-0 my-2 mx-2 right-0 cursor-pointer text-warmGray-600 hover:text-white"
on:click={() => (showPassword = !showPassword)}
>
{#if showPassword}
<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="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg>
{:else}
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
{/if}
</div>
</div>

View File

@@ -98,10 +98,10 @@ export async function handle({ request, resolve }) {
try {
session = initializeSession(request.headers, {
secret: SECRETS_ENCRYPTION_KEY,
cookie: { path: '/' }
cookie: { path: '/', secure: true }
});
} catch (error) {
console.log(error)
console.log(error);
return {
status: 302,
headers: {
@@ -124,7 +124,7 @@ export async function handle({ request, resolve }) {
if (!session['set-cookie']) {
if (!session?.data?.coolToken && !publicPages.includes(request.path)) {
return {
status: 301,
status: 302,
headers: {
location: '/'
}
@@ -146,6 +146,6 @@ export function getSession(request) {
isLoggedIn: data && Object.keys(data).length !== 0 ? true : false,
expires: data.expires,
coolToken: data.coolToken,
ghToken: data.ghToken
ghToken: data.ghToken || null
};
}

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { deleteSameDeployments, purgeImagesContainers } from './cleanup';
import yaml from 'js-yaml';
import { delay, execShellAsync } from '../common';
export default async function (configuration, imageChanged) {
export default async function (configuration, nextStep) {
const generateEnvs = {};
for (const secret of configuration.publish.secrets) {
generateEnvs[secret.name] = secret.value;
@@ -56,23 +56,25 @@ export default async function (configuration, imageChanged) {
};
await saveAppLog('### Publishing.', configuration);
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack));
if (imageChanged) {
if (nextStep === 2) {
// console.log('image changed')
await execShellAsync(
`docker service update --image ${containerName}:${containerTag} ${containerName}_${containerName}`
);
} else {
// console.log('new deployment or force deployment or config changed')
await deleteSameDeployments(configuration);
// if (originalDomain !== configuration.publish.domain) {
// await deleteSameDeployments(configuration, originalDomain);
// } else {
// await deleteSameDeployments(configuration);
// }
// await deleteSameDeployments(configuration);
await execShellAsync(
`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,9 +1,10 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
const buildImageNodeDocker = (configuration, prodBuild) => {
const buildImageNodeDocker = (configuration, prodBuild, generateEnvs) => {
return [
'FROM node:lts',
...generateEnvs,
'WORKDIR /usr/src/app',
`COPY ${configuration.build.directory}/package*.json ./`,
configuration.build.command.installation && `RUN ${configuration.build.command.installation}`,
@@ -13,18 +14,29 @@ const buildImageNodeDocker = (configuration, prodBuild) => {
].join('\n');
};
export async function buildImage(configuration, cacheBuild?: boolean, prodBuild?: boolean) {
// TODO: Edit secrets
// TODO: Add secret from .env file / json
const generateEnvs = [];
const dotEnv = []
for (const secret of configuration.publish.secrets) {
dotEnv.push(`${secret.name}=${secret.value}`)
if (secret.isBuild) generateEnvs.push(`ENV ${secret.name}=${secret.value}`)
}
await fs.writeFile(
`${configuration.general.workdir}/.env`,
dotEnv.join('\n')
)
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
buildImageNodeDocker(configuration, prodBuild)
buildImageNodeDocker(configuration, prodBuild, generateEnvs)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{
t: `${configuration.build.container.name}:${
cacheBuild
? `${configuration.build.container.tag}-cache`
: configuration.build.container.tag
}`
t: `${configuration.build.container.name}:${cacheBuild
? `${configuration.build.container.tag}-cache`
: configuration.build.container.tag
}`
}
);
await streamEvents(stream, configuration);

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,9 @@
export function dashify(str: string, options?: any) {
if (typeof str !== 'string') return str;
return str
.trim()
.replace(/\W/g, (m) => (/[À-ž]/.test(m) ? m : '-'))
.replace(/^-+|-+$/g, '')
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase();
}

View File

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

View File

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

View File

@@ -4,12 +4,16 @@ export interface IUser extends Document {
email: string;
avatar?: string;
uid: string;
type: string;
password: string;
}
const UserSchema = new Schema({
email: { type: String, required: true, unique: true },
avatar: { type: String },
uid: { type: String, required: true }
uid: { type: String, required: true },
type: { type: String, required: true, default: 'github' },
password: { type: String }
});
UserSchema.set('timestamps', true);

View File

@@ -9,7 +9,7 @@
if (!publicPages.includes(path)) {
if (!session.session.isLoggedIn) {
return {
status: 301,
status: 302,
redirect: '/'
};
}
@@ -17,16 +17,16 @@
}
if (!publicPages.includes(path)) {
return {
status: 301,
status: 302,
redirect: '/'
};
}
return {};
}
</script>
<script lang="ts">
import 'microtip/microtip.css';
import '../app.postcss';
export let initDashboard;
import { onMount } from 'svelte';
@@ -37,8 +37,9 @@
import Tooltip from '$components/Tooltip.svelte';
import compareVersions from 'compare-versions';
import packageJson from '../../package.json';
import { dashboard } from '$store';
import { dashboard, settings } from '$store';
import { browser } from '$app/env';
$settings.clientId = import.meta.env.VITE_GITHUB_APP_CLIENTID !== 'null' ? import.meta.env.VITE_GITHUB_APP_CLIENTID : null
$dashboard = initDashboard;
const branch =
process.env.NODE_ENV === 'production' &&
@@ -53,12 +54,13 @@
let upgradeDisabled = false;
let upgradeDone = false;
let showAck = false;
let globalFeatureFlag = browser && localStorage.getItem('globalFeatureFlag');
const options = {
duration: 2000
};
onMount(async () => {
upgradeAvailable = await checkUpgrade();
browser && localStorage.removeItem('token')
browser && localStorage.removeItem('token');
if (!localStorage.getItem('automaticErrorReportsAck')) {
showAck = true;
if (latest?.coolify[branch]?.settings?.sendErrors) {
@@ -103,7 +105,6 @@
localStorage.setItem('automaticErrorReportsAck', 'true');
showAck = false;
}
</script>
<SvelteToast {options} />
@@ -135,69 +136,136 @@
class:border-purple-500={$page.path === '/dashboard/databases'}
>
<div class="w-10 pt-4 pb-4"><img src="/favicon.png" alt="coolLabs logo" /></div>
<Tooltip position="right" label="Applications">
<div
class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 mt-4 transition-all duration-100 cursor-pointer"
on:click={() => goto('/dashboard/applications')}
class:text-green-500={$page.path === '/dashboard/applications' ||
$page.path.startsWith('/application')}
class:bg-warmGray-700={$page.path === '/dashboard/applications' ||
$page.path.startsWith('/application')}
>
<svg
class="w-8"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><rect x="4" y="4" width="16" height="16" rx="2" ry="2" /><rect
x="9"
y="9"
width="6"
height="6"
/><line x1="9" y1="1" x2="9" y2="4" /><line x1="15" y1="1" x2="15" y2="4" /><line
x1="9"
y1="20"
x2="9"
y2="23"
/><line x1="15" y1="20" x2="15" y2="23" /><line x1="20" y1="9" x2="23" y2="9" /><line
x1="20"
y1="14"
x2="23"
y2="14"
/><line x1="1" y1="9" x2="4" y2="9" /><line x1="1" y1="14" x2="4" y2="14" /></svg
{#if $settings.clientId}
<Tooltip position="right" label="Applications">
<div
class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 mt-4 transition-all duration-100 cursor-pointer"
on:click={() => goto('/dashboard/applications')}
class:text-green-500={$page.path === '/dashboard/applications' ||
$page.path.startsWith('/application')}
class:bg-warmGray-700={$page.path === '/dashboard/applications' ||
$page.path.startsWith('/application')}
>
</div>
</Tooltip>
<Tooltip position="right" label="Databases">
<div
class="p-2 hover:bg-warmGray-700 rounded hover:text-purple-500 my-4 transition-all duration-100 cursor-pointer"
on:click={() => goto('/dashboard/databases')}
class:text-purple-500={$page.path === '/dashboard/databases' ||
$page.path.startsWith('/database')}
class:bg-warmGray-700={$page.path === '/dashboard/databases' ||
$page.path.startsWith('/database')}
>
<svg
class="w-8"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
<svg
class="w-8"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><rect x="4" y="4" width="16" height="16" rx="2" ry="2" /><rect
x="9"
y="9"
width="6"
height="6"
/><line x1="9" y1="1" x2="9" y2="4" /><line x1="15" y1="1" x2="15" y2="4" /><line
x1="9"
y1="20"
x2="9"
y2="23"
/><line x1="15" y1="20" x2="15" y2="23" /><line
x1="20"
y1="9"
x2="23"
y2="9"
/><line x1="20" y1="14" x2="23" y2="14" /><line x1="1" y1="9" x2="4" y2="9" /><line
x1="1"
y1="14"
x2="4"
y2="14"
/></svg
>
</div>
</Tooltip>
<Tooltip position="right" label="Databases">
<div
class="p-2 hover:bg-warmGray-700 rounded hover:text-purple-500 my-4 transition-all duration-100 cursor-pointer"
on:click={() => goto('/dashboard/databases')}
class:text-purple-500={$page.path === '/dashboard/databases' ||
$page.path.startsWith('/database')}
class:bg-warmGray-700={$page.path === '/dashboard/databases' ||
$page.path.startsWith('/database')}
>
<svg
class="w-8"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
/>
</svg>
</div>
</Tooltip>
{:else}
<Tooltip
position="right"
label="Applications disabled, no GitHub Integration detected"
size="large"
>
<div class="p-2 text-warmGray-700 mt-4 transition-all duration-100 cursor-pointer">
<svg
class="w-8"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
/>
</svg>
</div>
</Tooltip>
stroke-linecap="round"
stroke-linejoin="round"
><rect x="4" y="4" width="16" height="16" rx="2" ry="2" /><rect
x="9"
y="9"
width="6"
height="6"
/><line x1="9" y1="1" x2="9" y2="4" /><line x1="15" y1="1" x2="15" y2="4" /><line
x1="9"
y1="20"
x2="9"
y2="23"
/><line x1="15" y1="20" x2="15" y2="23" /><line
x1="20"
y1="9"
x2="23"
y2="9"
/><line x1="20" y1="14" x2="23" y2="14" /><line x1="1" y1="9" x2="4" y2="9" /><line
x1="1"
y1="14"
x2="4"
y2="14"
/></svg
>
</div>
</Tooltip>
<Tooltip position="right" label="Databases disabled, no GitHub Integration detected" size="large">
<div
class="p-2 text-warmGray-700 my-4 transition-all duration-100 cursor-pointer"
>
<svg
class="w-8"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
/>
</svg>
</div>
</Tooltip>
{/if}
<Tooltip position="right" label="Services">
<div
class="p-2 hover:bg-warmGray-700 rounded hover:text-blue-500 transition-all duration-100 cursor-pointer"
@@ -224,6 +292,31 @@
</div>
</Tooltip>
<div class="flex-1" />
{#if globalFeatureFlag}
<Tooltip position="right" label="Servers">
<div
class="p-2 hover:bg-warmGray-700 rounded hover:text-red-500 mb-4 transition-all duration-100 cursor-pointer"
on:click={() => goto('/servers')}
class:text-red-500={$page.path === '/servers' || $page.path.startsWith('/servers')}
class:bg-warmGray-700={$page.path === '/servers' || $page.path.startsWith('/servers')}
>
<svg
class="w-8"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"
/>
</svg>
</div>
</Tooltip>
{/if}
<Tooltip position="right" label="Settings">
<button
class="p-2 hover:bg-warmGray-700 rounded hover:text-yellow-500 transition-all duration-100 cursor-pointer"

View File

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

View File

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

View File

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

View File

@@ -4,63 +4,47 @@ import ApplicationLog from '$models/ApplicationLog';
import { delay, execShellAsync } from '$lib/api/common';
import Configuration from '$models/Configuration';
async function purgeImagesAsync(found) {
await delay(10000);
await purgeImagesContainers(found, true);
}
export async function post(request: Request) {
const { organization, name, branch, domain } = request.body;
const { nickname } = request.body;
try {
const configurationFound = await Configuration.findOne({
'repository.organization': organization,
'repository.name': name,
'repository.branch': branch,
'publish.domain': domain
'general.nickname': nickname
});
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
'publish.domain': { $regex: `.*${configurationFound.publish.domain}`, $options: 'i' },
'publish.path': configurationFound.publish.path
});
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}`);
}
const deploys = await Deployment.find({ organization, branch, name });
await Configuration.deleteMany({
'publish.domain': { $regex: `.*${configurationFound.publish.domain}`, $options: 'i' },
'publish.path': configurationFound.publish.path
});
const deploys = await Deployment.find({ nickname });
for (const deploy of deploys) {
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 });
const deploys = await Deployment.find({ nickname });
for (const deploy of deploys) {
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
await Deployment.deleteMany({ deployId: deploy.deployId });
}
purgeImagesAsync(configurationFound);
}
}
return {
status: 200,
body: {
organization,
name,
branch
}
body: {}
};
} catch (error) {
console.log(error);

View File

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

View File

@@ -33,7 +33,8 @@ export async function get(request: Request) {
_id: new mongoose.Types.ObjectId(),
email,
avatar: avatar_url,
uid
uid,
type: 'github'
});
const defaultSettings = new Settings({
_id: new mongoose.Types.ObjectId()
@@ -68,7 +69,8 @@ export async function get(request: Request) {
_id: new mongoose.Types.ObjectId(),
email,
avatar: avatar_url,
uid
uid,
type: 'github'
});
try {
await newUser.save();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,170 @@
import type { Request } from '@sveltejs/kit';
import yaml from 'js-yaml';
import generator from 'generate-password';
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, remoteDB, database, wordpressExtraConfiguration } = request.body;
const traefikURL = baseURL;
baseURL = `https://${baseURL}`;
const workdir = '/tmp/wordpress';
const deployId = `wp-${generator.generate({ length: 5, numbers: true, strict: true })}`;
const defaultDatabaseName = generator.generate({ length: 12, numbers: true, strict: true });
const defaultDatabaseHost = `${deployId}-mysql`;
const defaultDatabaseUser = generator.generate({ length: 12, numbers: true, strict: true });
const defaultDatabasePassword = generator.generate({ length: 24, numbers: true, strict: true });
const defaultDatabaseRootPassword = generator.generate({
length: 24,
numbers: true,
strict: true
});
const defaultDatabaseRootUser = generator.generate({ length: 12, numbers: true, strict: true });
let secrets = [
{ name: 'WORDPRESS_DB_HOST', value: defaultDatabaseHost },
{ name: 'WORDPRESS_DB_USER', value: defaultDatabaseUser },
{ name: 'WORDPRESS_DB_PASSWORD', value: defaultDatabasePassword },
{ name: 'WORDPRESS_DB_NAME', value: defaultDatabaseName },
{ name: 'WORDPRESS_CONFIG_EXTRA', value: wordpressExtraConfiguration }
];
const generateEnvsMySQL = {
MYSQL_ROOT_PASSWORD: defaultDatabaseRootPassword,
MYSQL_ROOT_USER: defaultDatabaseRootUser,
MYSQL_USER: defaultDatabaseUser,
MYSQL_PASSWORD: defaultDatabasePassword,
MYSQL_DATABASE: defaultDatabaseName
};
const image = 'bitnami/mysql:8.0';
const volume = `${deployId}-mysql-data:/bitnami/mysql/data`;
if (remoteDB) {
secrets = [
{ name: 'WORDPRESS_DB_HOST', value: database.host },
{ name: 'WORDPRESS_DB_USER', value: database.user },
{ name: 'WORDPRESS_DB_PASSWORD', value: database.password },
{ name: 'WORDPRESS_DB_NAME', value: database.name },
{ name: 'WORDPRESS_TABLE_PREFIX', value: database.tablePrefix },
{ name: 'WORDPRESS_CONFIG_EXTRA', value: wordpressExtraConfiguration }
];
}
const generateEnvsWordpress = {};
for (const secret of secrets) generateEnvsWordpress[secret.name] = secret.value;
let stack = {
version: '3.8',
services: {
[deployId]: {
image: 'wordpress',
networks: [`${docker.network}`],
environment: generateEnvsWordpress,
volumes: [`${deployId}-wordpress-data:/var/www/html`],
deploy: {
...baseServiceConfiguration,
labels: [
'managedBy=coolify',
'type=service',
'serviceName=' + deployId,
'configuration=' +
JSON.stringify({
deployId,
baseURL,
generateEnvsWordpress
}),
'traefik.enable=true',
'traefik.http.services.' + deployId + '.loadbalancer.server.port=80',
'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'
]
}
},
[`${deployId}-mysql`]: {
image,
networks: [`${docker.network}`],
environment: generateEnvsMySQL,
volumes: [volume],
deploy: {
...baseServiceConfiguration,
labels: ['managedBy=coolify', 'type=service', 'serviceName=' + deployId]
}
}
},
networks: {
[`${docker.network}`]: {
external: true
}
},
volumes: {
[`${deployId}-wordpress-data`]: {
external: true
},
[`${deployId}-mysql-data`]: {
external: true
}
}
};
if (remoteDB) {
stack = {
version: '3.8',
services: {
[deployId]: {
image: 'wordpress',
networks: [`${docker.network}`],
environment: generateEnvsWordpress,
volumes: [`${deployId}-wordpress-data:/var/www/html`],
deploy: {
...baseServiceConfiguration,
labels: [
'managedBy=coolify',
'type=service',
'serviceName=' + deployId,
'configuration=' +
JSON.stringify({
deployId,
baseURL,
generateEnvsWordpress
}),
'traefik.enable=true',
'traefik.http.services.' + deployId + '.loadbalancer.server.port=80',
'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
}
},
volumes: {
[`${deployId}-wordpress-data`]: {
external: true
}
}
};
}
await execShellAsync(`mkdir -p ${workdir}`);
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));
await execShellAsync(`docker stack rm ${deployId}`);
await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`);
cleanupTmp(workdir);
return {
status: 200,
body: { message: 'OK' }
};
}

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,18 @@
<script context="module" lang="ts">
/**
* @type {import('@sveltejs/kit').Load}
*/
export async function load(session) {
if (!browser && !process.env.VITE_GITHUB_APP_CLIENTID) {
return {
status: 302,
redirect: '/dashboard/services'
};
}
return {};
}
</script>
<script>
import { goto } from '$app/navigation';
import MongoDb from '$components/Database/SVGs/MongoDb.svelte';
@@ -8,7 +23,7 @@
import { dashboard } from '$store';
import { fade } from 'svelte/transition';
import Redis from '$components/Database/SVGs/Redis.svelte';
import { browser } from '$app/env';
</script>
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
@@ -59,9 +74,7 @@
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"
/>
<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"

View File

@@ -2,6 +2,14 @@
import { goto } from '$app/navigation';
import { dashboard } from '$store';
import { fade } from 'svelte/transition';
async function openConfiguration(service) {
if (service.serviceName === 'wordpress') {
goto(`/service/${service.configuration.deployId}/configuration`);
} else {
goto(`/service/${service.serviceName}/configuration`);
}
}
</script>
<div
@@ -34,7 +42,7 @@
<div
in:fade={{ duration: 200 }}
class="px-4 pb-4"
on:click={() => goto(`/service/${service.serviceName}/configuration`)}
on:click={() => openConfiguration(service)}
>
<div
class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-blue-500 text-white shadow-md cursor-pointer ease-in-out hover:scale-105 duration-100 group"
@@ -49,7 +57,7 @@
/>
<div class="text-white font-bold">Plausible Analytics</div>
</div>
{:else if service.serviceName == 'nocodb'}
{:else if service.serviceName == 'nocodb'}
<div>
<img
alt="nocodedb"
@@ -58,15 +66,27 @@
/>
<div class="text-white font-bold">NocoDB</div>
</div>
{:else if service.serviceName == 'code-server'}
{:else if service.serviceName == 'code-server'}
<div>
<svg class="w-10 absolute top-0 left-0 -m-6" viewBox="0 0 128 128">
<path d="M3.656 45.043s-3.027-2.191.61-5.113l8.468-7.594s2.426-2.559 4.989-.328l78.175 59.328v28.45s-.039 4.468-5.757 3.976zm0 0" fill="#2489ca"></path><path d="M23.809 63.379L3.656 81.742s-2.07 1.543 0 4.305l9.356 8.527s2.222 2.395 5.508-.328l21.359-16.238zm0 0" fill="#1070b3"></path><path d="M59.184 63.531l36.953-28.285-.239-28.297S94.32.773 89.055 3.99L39.879 48.851zm0 0" fill="#0877b9"></path><path d="M90.14 123.797c2.145 2.203 4.747 1.48 4.747 1.48l28.797-14.222c3.687-2.52 3.171-5.645 3.171-5.645V20.465c0-3.735-3.812-5.024-3.812-5.024L98.082 3.38c-5.453-3.379-9.027.61-9.027.61s4.593-3.317 6.843 2.96v112.317c0 .773-.164 1.53-.492 2.214-.656 1.332-2.086 2.57-5.504 2.051zm0 0" fill="#3c99d4"></path>
</svg>
<path
d="M3.656 45.043s-3.027-2.191.61-5.113l8.468-7.594s2.426-2.559 4.989-.328l78.175 59.328v28.45s-.039 4.468-5.757 3.976zm0 0"
fill="#2489ca"
/><path
d="M23.809 63.379L3.656 81.742s-2.07 1.543 0 4.305l9.356 8.527s2.222 2.395 5.508-.328l21.359-16.238zm0 0"
fill="#1070b3"
/><path
d="M59.184 63.531l36.953-28.285-.239-28.297S94.32.773 89.055 3.99L39.879 48.851zm0 0"
fill="#0877b9"
/><path
d="M90.14 123.797c2.145 2.203 4.747 1.48 4.747 1.48l28.797-14.222c3.687-2.52 3.171-5.645 3.171-5.645V20.465c0-3.735-3.812-5.024-3.812-5.024L98.082 3.38c-5.453-3.379-9.027.61-9.027.61s4.593-3.317 6.843 2.96v112.317c0 .773-.164 1.53-.492 2.214-.656 1.332-2.086 2.57-5.504 2.051zm0 0"
fill="#3c99d4"
/>
</svg>
<div class="text-white font-bold">VSCode Server</div>
</div>
{:else if service.serviceName == 'minio'}
{:else if service.serviceName == 'minio'}
<div>
<img
alt="minio"
@@ -76,6 +96,21 @@
<div class="text-white font-bold">MinIO</div>
</div>
{:else if service.serviceName.match(/wp-/)}
<svg class="w-10 absolute top-0 left-0 -m-6" viewBox="0 0 128 128">
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="white"
d="M64.094 126.224c34.275-.052 62.021-27.933 62.021-62.325 0-33.833-27.618-61.697-60.613-62.286C30.85.995 1.894 29.113 1.885 63.21c-.01 35.079 27.612 63.064 62.209 63.014zM63.993 4.63c32.907-.011 59.126 26.725 59.116 60.28-.011 31.679-26.925 58.18-59.092 58.187-32.771.007-59.125-26.563-59.124-59.608.002-32.193 26.766-58.848 59.1-58.859zM39.157 35.896c.538 1.793-.968 2.417-2.569 2.542-1.685.13-3.369.257-5.325.406 6.456 19.234 12.815 38.183 19.325 57.573.464-.759.655-.973.739-1.223 3.574-10.682 7.168-21.357 10.651-32.069.318-.977.16-2.271-.188-3.275-1.843-5.32-4.051-10.524-5.667-15.908-1.105-3.686-2.571-6.071-6.928-5.644-.742.073-1.648-1.524-2.479-2.349 1.005-.6 2.003-1.704 3.017-1.719a849.593 849.593 0 0126.618.008c1.018.017 2.016 1.15 3.021 1.765-.88.804-1.639 2.01-2.668 2.321-1.651.498-3.482.404-5.458.58l19.349 57.56c2.931-9.736 5.658-18.676 8.31-27.639 2.366-8.001.956-15.473-3.322-22.52-1.286-2.119-2.866-4.175-3.595-6.486-.828-2.629-1.516-5.622-1.077-8.259.745-4.469 4.174-6.688 8.814-7.113C74.333.881 34.431 9.317 19.728 34.922c5.66-.261 11.064-.604 16.472-.678 1.022-.013 2.717.851 2.957 1.652zm10.117 77.971c-.118.345-.125.729-.218 1.302 10.943 3.034 21.675 2.815 32.659-.886l-16.78-45.96c-5.37 15.611-10.52 30.575-15.661 45.544zm-8.456-2.078l-25.281-69.35c-11.405 22.278-2.729 56.268 25.281 69.35zm76.428-44.562c.802-10.534-2.832-25.119-5.97-27.125-.35 3.875-.106 8.186-1.218 12.114-2.617 9.255-5.817 18.349-8.899 27.468-3.35 9.912-6.832 19.779-10.257 29.666 16.092-9.539 24.935-23.618 26.344-42.123z"
/>
</svg>
<div class="text-white font-bold text-center">
Wordpress<span
class="flex text-xs items-center justify-center text-warmGray-300 group-hover:text-white"
>({service.configuration.baseURL.replace('https://', '')})</span
>
</div>
{/if}
</div>
</div>

View File

@@ -1,10 +1,15 @@
<script>
import { browser } from '$app/env';
import { goto } from '$app/navigation';
import { session } from '$app/stores';
import { toast } from '@zerodevx/svelte-toast';
import PasswordField from '$components/PasswordField.svelte';
import { request } from '$lib/request';
import { settings } from '$store';
import Loading from '$components/Loading.svelte';
let loading = false;
let email = null;
let password = null;
async function login() {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
@@ -22,11 +27,30 @@
const timer = setInterval(() => {
if (newWindow?.closed) {
clearInterval(timer);
browser && location.reload()
// WHY need to navigate to / to get cookies?!
browser && window.location.replace('/')
}
}, 100);
}
async function loginWithEmail() {
try {
loading = true;
const { message } = await request('/api/v1/login/email', $session, {
body: {
email,
password
}
});
toast.push(message);
setTimeout(() => {
// WHY need to navigate to / to get cookies?!
browser && window.location.replace('/')
}, 1000);
} catch (error) {
loading = false;
browser && toast.push(error.error || error || 'Ooops something went wrong.');
}
}
</script>
<div class="flex justify-center items-center h-screen w-full bg-warmGray-900">
@@ -37,24 +61,56 @@
>
<span class="border-gradient">Coolify</span>
</p>
<h2 class="text-2xl md:text-3xl font-extrabold text-white">
<h2 class="text-2xl md:text-3xl font-extrabold text-white py-10">
An open-source, hassle-free, self-hostable<br />
<span class="text-indigo-400">Heroku</span>
& <span class="text-green-400">Netlify</span> alternative
</h2>
<div class="text-center py-10">
{#if !$session.isLoggedIn}
<button
class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold"
on:click={login}>Login with Github</button
>
{:else}
<button
class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold"
on:click={() => goto('/dashboard/applications')}>Get Started</button
>
{/if}
</div>
{#if loading}
<Loading fullscreen={false} />
{:else}
<div class="text-center py-10 max-w-7xl">
{#if !$session.isLoggedIn}
{#if $settings.clientId}
<button
class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold"
on:click={login}>Login with GitHub</button
>
{:else}
<div>
<div class="grid grid-flow-row gap-2 items-center pb-6">
<div class="grid grid-flow-row">
<label for="Email" class="">Email address</label>
<input
class="border-2"
id="Email"
bind:value={email}
placeholder="hi@coollabs.io"
/>
</div>
<div class="grid grid-flow-row">
<label for="Password" class="">Password</label>
<PasswordField bind:value={password} isEditable />
</div>
</div>
<div class="space-x-4 pt-10">
<button
class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold"
on:click={loginWithEmail}>Login with Email</button
>
</div>
</div>
{/if}
{:else}
<button
class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold"
on:click={() =>
$settings.clientId ? goto('/dashboard/applications') : goto('/dashboard/services')}
>Get Started</button
>
{/if}
</div>
{/if}
</div>
</div>
</div>

View File

@@ -0,0 +1,101 @@
<script context="module">
/**
* @type {import('@sveltejs/kit').Load}
*/
export async function load({ fetch }) {
try {
const { hostname, filesystems,dockerReclaimable } = await (await fetch(`/api/v1/servers`)).json();
return {
props: {
hostname,
filesystems,
dockerReclaimable
}
};
} catch (error) {
return {
props: {
hostname: null,
filesystems: null,
dockerReclaimable: null
}
};
}
}
</script>
<script>
export let hostname;
export let filesystems;
export let dockerReclaimable;
import { browser } from '$app/env';
import { session } from '$app/stores';
import { request } from '$lib/request';
import { toast } from '@zerodevx/svelte-toast';
import { fade } from 'svelte/transition';
async function refetch() {
const data = await request('/api/v1/servers', $session)
filesystems = data.filesystems
dockerReclaimable = data.dockerReclaimable
}
async function cleanupVolumes() {
const { output } = await request('/api/v1/servers/cleanups/volumes', $session, {
body: {}
});
browser && toast.push(output);
await refetch()
}
async function cleanupImages() {
const { output } = await request('/api/v1/servers/cleanups/images', $session, {
body: {}
});
browser && toast.push(output);
await refetch()
}
async function cleanupBuildCache() {
const { output } = await request('/api/v1/servers/cleanups/caches', $session, {
body: {}
});
browser && toast.push(output);
await refetch()
}
async function cleanupContainers() {
const { output } = await request('/api/v1/servers/cleanups/containers', $session, {
body: {}
});
browser && toast.push(output);
await refetch()
}
</script>
<div class="min-h-full text-white" in:fade={{ duration: 100 }}>
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
<div>Servers</div>
</div>
</div>
<div in:fade={{ duration: 100 }}>
<div class="max-w-4xl mx-auto px-6 pb-4 h-64 ">
<div class="text-center font-bold text-xl">{hostname}</div>
<div class="font-bold">Filesystem Usage</div>
{#each filesystems as filesystem}
<!-- <div>{JSON.stringify(filesystem)}</div> -->
<div class="text-xs">
{filesystem.mount}: {(filesystem.available / 1024 / 1024).toFixed()}MB ({filesystem.use}%) free of {(filesystem.size /1024 /1024).toFixed()}MB
</div>
{/each}
<div class="font-bold">Docker Reclaimable</div>
{#each dockerReclaimable as reclaimable}
<div class="text-xs">
{reclaimable.Type}: {reclaimable.Reclaimable} of {reclaimable.Size}
</div>
{/each}
<button class="button hover:bg-warmGray-700 bg-warmGray-800 rounded p-2 font-bold" on:click={cleanupVolumes}>Cleanup unused volumes</button>
<button class="button hover:bg-warmGray-700 bg-warmGray-800 rounded p-2 font-bold" on:click={cleanupImages}>Cleanup unused images</button>
<button class="button hover:bg-warmGray-700 bg-warmGray-800 rounded p-2 font-bold" on:click={cleanupBuildCache}>Cleanup build caches</button>
<button class="button hover:bg-warmGray-700 bg-warmGray-800 rounded p-2 font-bold" on:click={cleanupContainers}>Cleanup containers</button>
</div>
</div>

View File

@@ -13,6 +13,7 @@
let service = {};
async function loadServiceConfig() {
if ($page.params.name) {
try {
service = await request(`/api/v1/services/${$page.params.name}`, $session);
} catch (error) {
@@ -39,6 +40,8 @@
<div>VSCode Server</div>
{:else if $page.params.name === 'minio'}
<div>MinIO</div>
{:else if $page.params.name.match(/wp-/)}
<div>Wordpress<span class="flex text-xs items-center justify-center">({service.config.baseURL.replace('https://','')})</span></div>
{/if}
<div class="px-4">
@@ -76,6 +79,15 @@
class="w-7 mx-auto"
src="https://cdn.coollabs.io/assets/coolify/services/minio/MINIO_Bird.png"
/>
{:else if $page.params.name.match(/wp-/)}
<svg class="w-8 mx-auto" viewBox="0 0 128 128">
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="white"
d="M64.094 126.224c34.275-.052 62.021-27.933 62.021-62.325 0-33.833-27.618-61.697-60.613-62.286C30.85.995 1.894 29.113 1.885 63.21c-.01 35.079 27.612 63.064 62.209 63.014zM63.993 4.63c32.907-.011 59.126 26.725 59.116 60.28-.011 31.679-26.925 58.18-59.092 58.187-32.771.007-59.125-26.563-59.124-59.608.002-32.193 26.766-58.848 59.1-58.859zM39.157 35.896c.538 1.793-.968 2.417-2.569 2.542-1.685.13-3.369.257-5.325.406 6.456 19.234 12.815 38.183 19.325 57.573.464-.759.655-.973.739-1.223 3.574-10.682 7.168-21.357 10.651-32.069.318-.977.16-2.271-.188-3.275-1.843-5.32-4.051-10.524-5.667-15.908-1.105-3.686-2.571-6.071-6.928-5.644-.742.073-1.648-1.524-2.479-2.349 1.005-.6 2.003-1.704 3.017-1.719a849.593 849.593 0 0126.618.008c1.018.017 2.016 1.15 3.021 1.765-.88.804-1.639 2.01-2.668 2.321-1.651.498-3.482.404-5.458.58l19.349 57.56c2.931-9.736 5.658-18.676 8.31-27.639 2.366-8.001.956-15.473-3.322-22.52-1.286-2.119-2.866-4.175-3.595-6.486-.828-2.629-1.516-5.622-1.077-8.259.745-4.469 4.174-6.688 8.814-7.113C74.333.881 34.431 9.317 19.728 34.922c5.66-.261 11.064-.604 16.472-.678 1.022-.013 2.717.851 2.957 1.652zm10.117 77.971c-.118.345-.125.729-.218 1.302 10.943 3.034 21.675 2.815 32.659-.886l-16.78-45.96c-5.37 15.611-10.52 30.575-15.661 45.544zm-8.456-2.078l-25.281-69.35c-11.405 22.278-2.729 56.268 25.281 69.35zm76.428-44.562c.802-10.534-2.832-25.119-5.97-27.125-.35 3.875-.106 8.186-1.218 12.114-2.617 9.255-5.817 18.349-8.899 27.468-3.35 9.912-6.832 19.779-10.257 29.666 16.092-9.539 24.935-23.618 26.344-42.123z"
/>
</svg>
{/if}
</div>
<a target="_blank" class="icon mx-2" href={service.config.baseURL}>
@@ -106,6 +118,8 @@
<CodeServer {service} />
{:else if $page.params.name === 'minio'}
<MinIo {service} />
{:else if $page.params.name.match(/wp-/)}
<div class="font-bold">Nothing to show here. Enjoy using WordPress!</div>
{/if}
</div>
</div>

View File

@@ -2,7 +2,7 @@
import { fade } from 'svelte/transition';
import { toast } from '@zerodevx/svelte-toast';
import { newService } from '$store';
import { newService, newWordpressService } from '$store';
import { page, session } from '$app/stores';
import { request } from '$lib/request';
import { goto } from '$app/navigation';
@@ -25,7 +25,7 @@
$: deployableNocoDB = $newService.baseURL === '' || $newService.baseURL === null;
$: deployableCodeServer = $newService.baseURL === '' || $newService.baseURL === null;
$: deployableMinIO = $newService.baseURL === '' || $newService.baseURL === null;
$: deployableWordpress = false
let loading = false;
async function deployPlausible() {
try {
@@ -115,6 +115,28 @@
loading = false;
}
}
async function deployWordpress() {
try {
loading = true;
await request(`/api/v1/services/deploy/${$page.params.type}`, $session, {
body: {
...$newWordpressService
}
});
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>
@@ -129,13 +151,15 @@
<span class="text-blue-500 px-2 capitalize">VSCode Server</span>
{:else if $page.params.type === 'minio'}
<span class="text-blue-500 px-2 capitalize">MinIO</span>
{:else if $page.params.type === 'wordpress'}
<span class="text-blue-500 px-2 capitalize">Wordpress</span>
{/if}
</div>
</div>
{#if loading}
<Loading />
{: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="space-y-2 max-w-xl mx-auto px-6 flex-col text-center" in:fade={{ duration: 100 }}>
<div class="grid grid-flow-row">
<label for="Domain"
>Domain <TooltipInfo
@@ -298,4 +322,188 @@
Deploy
</button>
</div>
{:else if $page.params.type === 'wordpress'}
<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 Wordpress instance here.`}
/></label
>
<input
id="Domain"
class:border-red-500={$newWordpressService.baseURL == null || $newWordpressService.baseURL == ''}
bind:value={$newWordpressService.baseURL}
placeholder="wordpress.coollabs.io"
/>
</div>
<div class="">
<div class="px-4 sm:px-6">
<ul class="divide-y divide-warmGray-800">
<li class="py-4 flex items-center justify-between text-left">
<div class="flex flex-col">
<p class="text-base font-bold text-warmGray-100">Use remote MySQL database?</p>
<p class="text-sm font-medium text-warmGray-400">
If not, Coolify will create a local database for you.
</p>
</div>
<button
type="button"
on:click={() => ($newWordpressService.remoteDB = !$newWordpressService.remoteDB)}
aria-pressed="true"
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={$newWordpressService.remoteDB}
class:bg-warmGray-700={!$newWordpressService.remoteDB}
>
<span class="sr-only">Use setting</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transition ease-in-out duration-200 transform"
class:translate-x-5={$newWordpressService.remoteDB}
class:translate-x-0={!$newWordpressService.remoteDB}
>
<span
class=" ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
class:opacity-0={$newWordpressService.remoteDB}
class:opacity-100={!$newWordpressService.remoteDB}
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={$newWordpressService.remoteDB}
class:opacity-0={!$newWordpressService.remoteDB}
>
<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>
</li>
</ul>
</div>
{#if $newWordpressService.remoteDB}
<div class="grid grid-flow-row pb-5">
<label for="database.host"
>DB Host <TooltipInfo
position="right"
label={`IP address of a remote Mysql instance.`}
/></label
>
<input
id="database.host"
class:border-red-500={$newWordpressService.database.host == null ||
$newWordpressService.database.host == ''}
bind:value={$newWordpressService.database.host}
placeholder="10.10.10.10:3306"
/>
</div>
<div class="grid grid-flow-row pb-5">
<label for="database.user"
>DB User <TooltipInfo position="right" label={`Database user.`} /></label
>
<input
id="database.user"
class:border-red-500={$newWordpressService.database.user == null ||
$newWordpressService.database.user == ''}
bind:value={$newWordpressService.database.user}
placeholder="wordpressuser"
/>
</div>
<div class="grid grid-flow-row pb-5">
<label for="database.password"
>DB Password <TooltipInfo
position="right"
label={`Database password for the database user.`}
/></label
>
<input
id="database.password"
class:border-red-500={$newWordpressService.database.password == null ||
$newWordpressService.database.password == ''}
bind:value={$newWordpressService.database.password}
placeholder="supersecretuserpasswordforwordpress"
/>
</div>
<div class="grid grid-flow-row pb-5">
<label for="database.name"
>DB Name<TooltipInfo
position="right"
label={`Database name`}
/></label
>
<input
id="database.name"
class:border-red-500={$newWordpressService.database.name == null ||
$newWordpressService.database.name == ''}
bind:value={$newWordpressService.database.name}
placeholder="wordpress"
/>
</div>
<div class="grid grid-flow-row pb-5">
<label for="database.tablePrefix"
>DB Table Prefix <TooltipInfo
position="right"
label={`Table prefix for wordpress`}
/></label
>
<input
id="database.tablePrefix"
class:border-red-500={$newWordpressService.database.tablePrefix == null ||
$newWordpressService.database.tablePrefix == ''}
bind:value={$newWordpressService.database.tablePrefix}
placeholder="wordpress"
/>
</div>
{/if}
<div class="grid grid-flow-row py-5">
<label for="wordpressExtraConfiguration"
>Wordpress Configuration Extra <TooltipInfo
position="right"
label={`Database password for the database user.`}
/></label
>
<textarea
class="h-32"
id="wordpressExtraConfiguration"
bind:value={$newWordpressService.wordpressExtraConfiguration}
placeholder="// Example Extra Configuration
define('WP_ALLOW_MULTISITE', true );
define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', false);"
/>
</div>
</div>
<button
disabled={deployableWordpress}
class:cursor-not-allowed={deployableWordpress}
class:bg-blue-500={!deployableWordpress}
class:hover:bg-blue-400={!deployableWordpress}
class:hover:bg-transparent={deployableWordpress}
class:text-warmGray-700={deployableWordpress}
class:text-white={!deployableWordpress}
class="button p-2 w-64 bg-blue-500 hover:bg-blue-400 text-white"
on:click={deployWordpress}
>
Deploy
</button>
</div>
{/if}

View File

@@ -10,11 +10,11 @@
Select a service
</div>
</div>
<div class="text-center space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
<div class="text-center space-y-2 max-w-7xl mx-auto px-6" in:fade={{ duration: 100 }}>
{#if $page.path === '/service/new'}
<div class="flex justify-center space-x-4 font-bold pb-6">
<div class="flex justify-center font-bold pb-6 flex-wrap">
<div
class="text-center flex-col items-center cursor-pointer ease-in-out hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 rounded bg-warmGray-800 w-48"
class="text-center flex-col items-center cursor-pointer ease-in-out hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 m-2 rounded bg-warmGray-800 w-48"
on:click={() => goto('/service/new/plausible')}
>
<img
@@ -25,7 +25,7 @@
<div class="text-white">Plausible Analytics</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-white p-2 rounded bg-warmGray-800 w-48"
class="text-center flex-col items-center cursor-pointer ease-in-out hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 m-2 rounded bg-warmGray-800 w-48"
on:click={() => goto('/service/new/nocodb')}
>
<img
@@ -37,7 +37,7 @@
<div class="text-white">NocoDB</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-green-500 p-2 rounded bg-warmGray-800 w-48"
class="text-center flex-col items-center cursor-pointer ease-in-out hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 m-2 rounded bg-warmGray-800 w-48"
on:click={() => goto('/service/new/code-server')}
>
<svg class="w-14 mx-auto pb-2" viewBox="0 0 128 128">
@@ -59,7 +59,7 @@
<div class="text-white">VSCode Server</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-red-500 p-2 rounded bg-warmGray-800 w-48"
class="text-center flex-col items-center cursor-pointer ease-in-out hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 m-2 rounded bg-warmGray-800 w-48"
on:click={() => goto('/service/new/minio')}
>
<img
@@ -70,6 +70,21 @@
<div class="flex-1" />
<div class="text-white">MinIO</div>
</div>
<div
class="text-center flex-col items-center cursor-pointer ease-in-out hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 m-2 rounded bg-warmGray-800 w-48"
on:click={() => goto('/service/new/wordpress')}
>
<svg class="w-14 mx-auto pb-2" viewBox="0 0 128 128">
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="white"
d="M64.094 126.224c34.275-.052 62.021-27.933 62.021-62.325 0-33.833-27.618-61.697-60.613-62.286C30.85.995 1.894 29.113 1.885 63.21c-.01 35.079 27.612 63.064 62.209 63.014zM63.993 4.63c32.907-.011 59.126 26.725 59.116 60.28-.011 31.679-26.925 58.18-59.092 58.187-32.771.007-59.125-26.563-59.124-59.608.002-32.193 26.766-58.848 59.1-58.859zM39.157 35.896c.538 1.793-.968 2.417-2.569 2.542-1.685.13-3.369.257-5.325.406 6.456 19.234 12.815 38.183 19.325 57.573.464-.759.655-.973.739-1.223 3.574-10.682 7.168-21.357 10.651-32.069.318-.977.16-2.271-.188-3.275-1.843-5.32-4.051-10.524-5.667-15.908-1.105-3.686-2.571-6.071-6.928-5.644-.742.073-1.648-1.524-2.479-2.349 1.005-.6 2.003-1.704 3.017-1.719a849.593 849.593 0 0126.618.008c1.018.017 2.016 1.15 3.021 1.765-.88.804-1.639 2.01-2.668 2.321-1.651.498-3.482.404-5.458.58l19.349 57.56c2.931-9.736 5.658-18.676 8.31-27.639 2.366-8.001.956-15.473-3.322-22.52-1.286-2.119-2.866-4.175-3.595-6.486-.828-2.629-1.516-5.622-1.077-8.259.745-4.469 4.174-6.688 8.814-7.113C74.333.881 34.431 9.317 19.728 34.922c5.66-.261 11.064-.604 16.472-.678 1.022-.013 2.717.851 2.957 1.652zm10.117 77.971c-.118.345-.125.729-.218 1.302 10.943 3.034 21.675 2.815 32.659-.886l-16.78-45.96c-5.37 15.611-10.52 30.575-15.661 45.544zm-8.456-2.078l-25.281-69.35c-11.405 22.278-2.729 56.268 25.281 69.35zm76.428-44.562c.802-10.534-2.832-25.119-5.97-27.125-.35 3.875-.106 8.186-1.218 12.114-2.617 9.255-5.817 18.349-8.899 27.468-3.35 9.912-6.832 19.779-10.257 29.666 16.092-9.539 24.935-23.618 26.344-42.123z"
/>
</svg>
<div class="flex-1" />
<div class="text-white">Wordpress</div>
</div>
</div>
{/if}
</div>

View File

@@ -83,7 +83,7 @@
>
<span class="sr-only">Use setting</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transition ease-in-out duration-200"
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transition ease-in-out duration-200 transform"
class:translate-x-5={settings?.allowRegistration}
class:translate-x-0={!settings?.allowRegistration}
>
@@ -143,7 +143,7 @@
>
<span class="sr-only">Use setting</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transition ease-in-out duration-200"
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transition ease-in-out duration-200 transform"
class:translate-x-5={settings?.sendErrors}
class:translate-x-0={!settings?.sendErrors}
>

View File

@@ -6,7 +6,9 @@ import type {
GithubInstallations
} from 'src/global';
import { writable } from 'svelte/store';
export const settings = writable({
clientId: null
});
export const dashboard = writable<Dashboard>({
databases: {
deployed: []
@@ -30,6 +32,7 @@ export const dateOptions: DateTimeFormatOptions = {
export const githubRepositories = writable([]);
export const githubInstallations = writable<GithubInstallations>([]);
export const originalDomain = writable(null)
export const application = writable<Application>({
github: {
installation: {
@@ -79,9 +82,7 @@ export const application = writable<Application>({
}
});
export const prApplication = writable([]);
export const initConf = writable({});
export const initialApplication: Application = {
github: {
installation: {
@@ -167,5 +168,16 @@ export const initialNewService = {
userPasswordAgain: null,
baseURL: null
};
export const newWordpressService = writable({
baseURL: null,
remoteDB: false,
database: {
host: null,
name: 'wordpress',
user: null,
password: null,
tablePrefix: 'wordpress'
},
wordpressExtraConfiguration: null
});
export const isPullRequestPermissionsGranted = writable(false);