wip: trpc

This commit is contained in:
Andras Bacsai 2023-01-13 14:54:21 +01:00
parent 568ab24fd9
commit 97313e4180
18 changed files with 1951 additions and 86 deletions

View File

@ -42,6 +42,7 @@ interface AppSession {
gitlab: string | null; gitlab: string | null;
}; };
pendingInvitations: Array<any>; pendingInvitations: Array<any>;
isARM: boolean
} }
export const appSession: Writable<AppSession> = writable({ export const appSession: Writable<AppSession> = writable({
@ -61,7 +62,8 @@ export const appSession: Writable<AppSession> = writable({
github: null, github: null,
gitlab: null gitlab: null
}, },
pendingInvitations: [] pendingInvitations: [],
isARM: false
}); });
interface AddToast { interface AddToast {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,8 @@ import { env } from '../env';
import { day } from './dayjs'; import { day } from './dayjs';
import { executeCommand } from './executeCommand'; import { executeCommand } from './executeCommand';
import { saveBuildLog } from './logging'; import { saveBuildLog } from './logging';
import { checkContainer } from './docker';
import yaml from 'js-yaml';
const customConfig: Config = { const customConfig: Config = {
dictionaries: [adjectives, colors, animals], dictionaries: [adjectives, colors, animals],
@ -22,6 +24,37 @@ export const isDev = env.NODE_ENV === 'development';
export const version = '3.13.0'; export const version = '3.13.0';
export const sentryDSN = export const sentryDSN =
'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216'; 'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
export const defaultTraefikImage = `traefik:v2.8`;
export function getAPIUrl() {
if (process.env.GITPOD_WORKSPACE_URL) {
const { href } = new URL(process.env.GITPOD_WORKSPACE_URL);
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
return newURL;
}
if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
}
return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000';
}
export function getUIUrl() {
if (process.env.GITPOD_WORKSPACE_URL) {
const { href } = new URL(process.env.GITPOD_WORKSPACE_URL);
const newURL = href.replace('https://', 'https://3000-').replace(/\/$/, '');
return newURL;
}
if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3000')}`;
}
return 'http://localhost:3000';
}
const mainTraefikEndpoint = isDev
? `${getAPIUrl()}/webhooks/traefik/main.json`
: 'http://coolify:3000/webhooks/traefik/main.json';
const otherTraefikEndpoint = isDev
? `${getAPIUrl()}/webhooks/traefik/other.json`
: 'http://coolify:3000/webhooks/traefik/other.json';
export async function listSettings(): Promise<Setting | null> { export async function listSettings(): Promise<Setting | null> {
return await prisma.setting.findUnique({ where: { id: '0' } }); return await prisma.setting.findUnique({ where: { id: '0' } });
@ -708,3 +741,85 @@ export function makeLabelForServices(type) {
} }
export const asyncSleep = (delay: number): Promise<unknown> => export const asyncSleep = (delay: number): Promise<unknown> =>
new Promise((resolve) => setTimeout(resolve, delay)); new Promise((resolve) => setTimeout(resolve, delay));
export async function startTraefikTCPProxy(
destinationDocker: any,
id: string,
publicPort: number,
privatePort: number,
type?: string
): Promise<{ stdout: string; stderr: string }> {
const { network, id: dockerId, remoteEngine } = destinationDocker;
const container = `${id}-${publicPort}`;
const { found } = await checkContainer({ dockerId, container, remove: true });
const { ipv4, ipv6 } = await listSettings();
let dependentId = id;
if (type === 'wordpressftp') dependentId = `${id}-ftp`;
const { found: foundDependentContainer } = await checkContainer({
dockerId,
container: dependentId,
remove: true
});
if (foundDependentContainer && !found) {
const { stdout: Config } = await executeCommand({
dockerId,
command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'`
});
const ip = JSON.parse(Config)[0].Gateway;
let traefikUrl = otherTraefikEndpoint;
if (remoteEngine) {
let ip = null;
if (isDev) {
ip = getAPIUrl();
} else {
ip = `http://${ipv4 || ipv6}:3000`;
}
traefikUrl = `${ip}/webhooks/traefik/other.json`;
}
const tcpProxy = {
version: '3.8',
services: {
[`${id}-${publicPort}`]: {
container_name: container,
image: defaultTraefikImage,
command: [
`--entrypoints.tcp.address=:${publicPort}`,
`--entryPoints.tcp.forwardedHeaders.insecure=true`,
`--providers.http.endpoint=${traefikUrl}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`,
'--providers.http.pollTimeout=10s',
'--log.level=error'
],
ports: [`${publicPort}:${publicPort}`],
extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal: ${ip}`],
volumes: ['/var/run/docker.sock:/var/run/docker.sock'],
networks: ['coolify-infra', network]
}
},
networks: {
[network]: {
external: false,
name: network
},
'coolify-infra': {
external: false,
name: 'coolify-infra'
}
}
};
await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy));
await executeCommand({
dockerId,
command: `docker compose -f /tmp/docker-compose-${id}.yaml up -d`
});
await fs.rm(`/tmp/docker-compose-${id}.yaml`);
}
if (!foundDependentContainer && found) {
await executeCommand({
dockerId,
command: `docker stop -t 0 ${container} && docker rm ${container}`,
shell: true
});
}
}

View File

@ -1,84 +0,0 @@
import { z } from 'zod';
import { privateProcedure, router } from '../trpc';
import { decrypt } from '../../lib/common';
import { prisma } from '../../prisma';
import { executeCommand } from '../../lib/executeCommand';
import { stopDatabaseContainer, stopTcpHttpProxy } from '../../lib/docker';
export const databasesRouter = router({
status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
const id = input.id;
const teamId = ctx.user?.teamId;
let isRunning = false;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database) {
const { destinationDockerId, destinationDocker } = database;
if (destinationDockerId) {
try {
const { stdout } = await executeCommand({
dockerId: destinationDocker.id,
command: `docker inspect --format '{{json .State}}' ${id}`
});
if (JSON.parse(stdout).Running) {
isRunning = true;
}
} catch (error) {
//
}
}
}
return {
isRunning
};
}),
cleanup: privateProcedure.query(async ({ ctx }) => {
const teamId = ctx.user?.teamId;
let databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true }
});
for (const database of databases) {
if (!database?.version) {
const { id } = database;
if (database.destinationDockerId) {
const everStarted = await stopDatabaseContainer(database);
if (everStarted)
await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
}
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } });
}
}
return {};
}),
delete: privateProcedure
.input(z.object({ id: z.string(), force: z.boolean() }))
.mutation(async ({ ctx, input }) => {
const { id, force } = input;
const teamId = ctx.user?.teamId;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (!force) {
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword)
database.rootUserPassword = decrypt(database.rootUserPassword);
if (database.destinationDockerId) {
const everStarted = await stopDatabaseContainer(database);
if (everStarted)
await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
}
}
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } });
return {};
})
});

View File

@ -0,0 +1,379 @@
import { z } from 'zod';
import fs from 'fs/promises';
import { privateProcedure, router } from '../../trpc';
import {
createDirectories,
decrypt,
encrypt,
getContainerUsage,
listSettings,
startTraefikTCPProxy
} from '../../../lib/common';
import { prisma } from '../../../prisma';
import { executeCommand } from '../../../lib/executeCommand';
import {
defaultComposeConfiguration,
stopDatabaseContainer,
stopTcpHttpProxy
} from '../../../lib/docker';
import {
generateDatabaseConfiguration,
getDatabaseVersions,
makeLabelForStandaloneDatabase,
updatePasswordInDb
} from './lib';
import yaml from 'js-yaml';
import { getFreePublicPort } from '../services/lib';
export const databasesRouter = router({
usage: privateProcedure
.input(
z.object({
id: z.string()
})
)
.query(async ({ ctx, input }) => {
const teamId = ctx.user?.teamId;
const { id } = input;
let usage = {};
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
if (database.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(database.destinationDocker.id, id)]);
}
return {
success: true,
data: {
usage
}
};
}),
save: privateProcedure
.input(
z.object({
id: z.string()
})
)
.mutation(async ({ ctx, input }) => {
const teamId = ctx.user?.teamId;
const {
id,
name,
defaultDatabase,
dbUser,
dbUserPassword,
rootUser,
rootUserPassword,
version,
isRunning
} = input;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
if (isRunning) {
if (database.dbUserPassword !== dbUserPassword) {
await updatePasswordInDb(database, dbUser, dbUserPassword, false);
} else if (database.rootUserPassword !== rootUserPassword) {
await updatePasswordInDb(database, rootUser, rootUserPassword, true);
}
}
const encryptedDbUserPassword = dbUserPassword && encrypt(dbUserPassword);
const encryptedRootUserPassword = rootUserPassword && encrypt(rootUserPassword);
await prisma.database.update({
where: { id },
data: {
name,
defaultDatabase,
dbUser,
dbUserPassword: encryptedDbUserPassword,
rootUser,
rootUserPassword: encryptedRootUserPassword,
version
}
});
}),
saveSettings: privateProcedure
.input(
z.object({
id: z.string(),
isPublic: z.boolean(),
appendOnly: z.boolean().default(true)
})
)
.mutation(async ({ ctx, input }) => {
const teamId = ctx.user?.teamId;
const { id, isPublic, appendOnly = true } = input;
let publicPort = null;
const {
destinationDocker: { remoteEngine, engine, remoteIpAddress }
} = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } });
if (isPublic) {
publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
}
await prisma.database.update({
where: { id },
data: {
settings: {
upsert: { update: { isPublic, appendOnly }, create: { isPublic, appendOnly } }
}
}
});
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
const { arch } = await listSettings();
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database;
const { privatePort } = generateDatabaseConfiguration(database, arch);
if (destinationDockerId) {
if (isPublic) {
await prisma.database.update({ where: { id }, data: { publicPort } });
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
} else {
await prisma.database.update({ where: { id }, data: { publicPort: null } });
await stopTcpHttpProxy(id, destinationDocker, oldPublicPort);
}
}
return { publicPort };
}),
saveSecret: privateProcedure
.input(
z.object({
id: z.string(),
name: z.string(),
value: z.string(),
isNew: z.boolean().default(true)
})
)
.mutation(async ({ ctx, input }) => {
let { id, name, value, isNew } = input;
if (isNew) {
const found = await prisma.databaseSecret.findFirst({ where: { name, databaseId: id } });
if (found) {
throw `Secret ${name} already exists.`;
} else {
value = encrypt(value.trim());
await prisma.databaseSecret.create({
data: { name, value, database: { connect: { id } } }
});
}
} else {
value = encrypt(value.trim());
const found = await prisma.databaseSecret.findFirst({ where: { databaseId: id, name } });
if (found) {
await prisma.databaseSecret.updateMany({
where: { databaseId: id, name },
data: { value }
});
} else {
await prisma.databaseSecret.create({
data: { name, value, database: { connect: { id } } }
});
}
}
}),
start: privateProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
const { id } = input;
const teamId = ctx.user?.teamId;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true, databaseSecret: true }
});
const { arch } = await listSettings();
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const {
type,
destinationDockerId,
destinationDocker,
publicPort,
settings: { isPublic },
databaseSecret
} = database;
const { privatePort, command, environmentVariables, image, volume, ulimits } =
generateDatabaseConfiguration(database, arch);
const network = destinationDockerId && destinationDocker.network;
const volumeName = volume.split(':')[0];
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
const { workdir } = await createDirectories({ repository: type, buildId: id });
if (databaseSecret.length > 0) {
databaseSecret.forEach((secret) => {
environmentVariables[secret.name] = decrypt(secret.value);
});
}
const composeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image,
command,
environment: environmentVariables,
volumes: [volume],
ulimits,
labels,
...defaultComposeConfiguration(network)
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[volumeName]: {
name: volumeName
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await executeCommand({
dockerId: destinationDocker.id,
command: `docker compose -f ${composeFileDestination} up -d`
});
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}),
stop: privateProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
const { id } = input;
const teamId = ctx.user?.teamId;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const everStarted = await stopDatabaseContainer(database);
if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
await prisma.database.update({
where: { id },
data: {
settings: { upsert: { update: { isPublic: false }, create: { isPublic: false } } }
}
});
await prisma.database.update({ where: { id }, data: { publicPort: null } });
}),
getDatabaseById: privateProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const { id } = input;
const teamId = ctx.user?.teamId;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (!database) {
throw { status: 404, message: 'Database not found.' };
}
const settings = await listSettings();
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const configuration = generateDatabaseConfiguration(database, settings.arch);
return {
success: true,
data: {
privatePort: configuration?.privatePort,
database,
versions: await getDatabaseVersions(database.type, settings.arch),
settings
}
};
}),
status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
const id = input.id;
const teamId = ctx.user?.teamId;
let isRunning = false;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database) {
const { destinationDockerId, destinationDocker } = database;
if (destinationDockerId) {
try {
const { stdout } = await executeCommand({
dockerId: destinationDocker.id,
command: `docker inspect --format '{{json .State}}' ${id}`
});
if (JSON.parse(stdout).Running) {
isRunning = true;
}
} catch (error) {
//
}
}
}
return {
success: true,
data: {
isRunning
}
};
}),
cleanup: privateProcedure.query(async ({ ctx }) => {
const teamId = ctx.user?.teamId;
let databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true }
});
for (const database of databases) {
if (!database?.version) {
const { id } = database;
if (database.destinationDockerId) {
const everStarted = await stopDatabaseContainer(database);
if (everStarted)
await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
}
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } });
}
}
return {};
}),
delete: privateProcedure
.input(z.object({ id: z.string(), force: z.boolean().default(false) }))
.mutation(async ({ ctx, input }) => {
const { id, force } = input;
const teamId = ctx.user?.teamId;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (!force) {
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword)
database.rootUserPassword = decrypt(database.rootUserPassword);
if (database.destinationDockerId) {
const everStarted = await stopDatabaseContainer(database);
if (everStarted)
await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
}
}
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } });
return {};
})
});

View File

@ -0,0 +1,283 @@
import { base64Encode, isARM, version } from "../../../lib/common";
import { executeCommand } from "../../../lib/executeCommand";
import { prisma } from "../../../prisma";
export const supportedDatabaseTypesAndVersions = [
{
name: 'mongodb',
fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb',
baseImageARM: 'mongo',
versions: ['5.0', '4.4', '4.2'],
versionsARM: ['5.0', '4.4', '4.2']
},
{
name: 'mysql',
fancyName: 'MySQL',
baseImage: 'bitnami/mysql',
baseImageARM: 'mysql',
versions: ['8.0', '5.7'],
versionsARM: ['8.0', '5.7']
},
{
name: 'mariadb',
fancyName: 'MariaDB',
baseImage: 'bitnami/mariadb',
baseImageARM: 'mariadb',
versions: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'],
versionsARM: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2']
},
{
name: 'postgresql',
fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql',
baseImageARM: 'postgres',
versions: ['14.5.0', '13.8.0', '12.12.0', '11.17.0', '10.22.0'],
versionsARM: ['14.5', '13.8', '12.12', '11.17', '10.22']
},
{
name: 'redis',
fancyName: 'Redis',
baseImage: 'bitnami/redis',
baseImageARM: 'redis',
versions: ['7.0', '6.2', '6.0', '5.0'],
versionsARM: ['7.0', '6.2', '6.0', '5.0']
},
{
name: 'couchdb',
fancyName: 'CouchDB',
baseImage: 'bitnami/couchdb',
baseImageARM: 'couchdb',
versions: ['3.2.2', '3.1.2', '2.3.1'],
versionsARM: ['3.2.2', '3.1.2', '2.3.1']
},
{
name: 'edgedb',
fancyName: 'EdgeDB',
baseImage: 'edgedb/edgedb',
versions: ['latest', '2.1', '2.0', '1.4']
}
];
export function getDatabaseImage(type: string, arch: string): string {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) {
if (isARM(arch)) {
return found.baseImageARM || found.baseImage;
}
return found.baseImage;
}
return '';
}
export function generateDatabaseConfiguration(database: any, arch: string) {
const { id, dbUser, dbUserPassword, rootUser, rootUserPassword, defaultDatabase, version, type } =
database;
const baseImage = getDatabaseImage(type, arch);
if (type === 'mysql') {
const configuration = {
privatePort: 3306,
environmentVariables: {
MYSQL_USER: dbUser,
MYSQL_PASSWORD: dbUserPassword,
MYSQL_ROOT_PASSWORD: rootUserPassword,
MYSQL_ROOT_USER: rootUser,
MYSQL_DATABASE: defaultDatabase
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/mysql/data`,
ulimits: {}
};
if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
}
return configuration;
} else if (type === 'mariadb') {
const configuration = {
privatePort: 3306,
environmentVariables: {
MARIADB_ROOT_USER: rootUser,
MARIADB_ROOT_PASSWORD: rootUserPassword,
MARIADB_USER: dbUser,
MARIADB_PASSWORD: dbUserPassword,
MARIADB_DATABASE: defaultDatabase
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/mariadb`,
ulimits: {}
};
if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
}
return configuration;
} else if (type === 'mongodb') {
const configuration = {
privatePort: 27017,
environmentVariables: {
MONGODB_ROOT_USER: rootUser,
MONGODB_ROOT_PASSWORD: rootUserPassword
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/mongodb`,
ulimits: {}
};
if (isARM(arch)) {
configuration.environmentVariables = {
MONGO_INITDB_ROOT_USERNAME: rootUser,
MONGO_INITDB_ROOT_PASSWORD: rootUserPassword
};
configuration.volume = `${id}-${type}-data:/data/db`;
}
return configuration;
} else if (type === 'postgresql') {
const configuration = {
privatePort: 5432,
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
POSTGRESQL_PASSWORD: dbUserPassword,
POSTGRESQL_USERNAME: dbUser,
POSTGRESQL_DATABASE: defaultDatabase
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/postgresql`,
ulimits: {}
};
if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/var/lib/postgresql`;
configuration.environmentVariables = {
POSTGRES_PASSWORD: dbUserPassword,
POSTGRES_USER: dbUser,
POSTGRES_DB: defaultDatabase
};
}
return configuration;
} else if (type === 'redis') {
const {
settings: { appendOnly }
} = database;
const configuration = {
privatePort: 6379,
command: undefined,
environmentVariables: {
REDIS_PASSWORD: dbUserPassword,
REDIS_AOF_ENABLED: appendOnly ? 'yes' : 'no'
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/redis/data`,
ulimits: {}
};
if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/data`;
configuration.command = `/usr/local/bin/redis-server --appendonly ${
appendOnly ? 'yes' : 'no'
} --requirepass ${dbUserPassword}`;
}
return configuration;
} else if (type === 'couchdb') {
const configuration = {
privatePort: 5984,
environmentVariables: {
COUCHDB_PASSWORD: dbUserPassword,
COUCHDB_USER: dbUser
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/couchdb`,
ulimits: {}
};
if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/opt/couchdb/data`;
}
return configuration;
} else if (type === 'edgedb') {
const configuration = {
privatePort: 5656,
environmentVariables: {
EDGEDB_SERVER_PASSWORD: rootUserPassword,
EDGEDB_SERVER_USER: rootUser,
EDGEDB_SERVER_DATABASE: defaultDatabase,
EDGEDB_SERVER_TLS_CERT_MODE: 'generate_self_signed'
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/var/lib/edgedb/data`,
ulimits: {}
};
return configuration;
}
return null;
}
export function getDatabaseVersions(type: string, arch: string): string[] {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) {
if (isARM(arch)) {
return found.versionsARM || found.versions;
}
return found.versions;
}
return [];
}
export async function updatePasswordInDb(database, user, newPassword, isRoot) {
const {
id,
type,
rootUser,
rootUserPassword,
dbUser,
dbUserPassword,
defaultDatabase,
destinationDockerId,
destinationDocker: { id: dockerId }
} = database;
if (destinationDockerId) {
if (type === 'mysql') {
await executeCommand({
dockerId,
command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"`
});
} else if (type === 'mariadb') {
await executeCommand({
dockerId,
command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"`
});
} else if (type === 'postgresql') {
if (isRoot) {
await executeCommand({
dockerId,
command: `docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"`
});
} else {
await executeCommand({
dockerId,
command: `docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"`
});
}
} else if (type === 'mongodb') {
await executeCommand({
dockerId,
command: `docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"`
});
} else if (type === 'redis') {
await executeCommand({
dockerId,
command: `docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}`
});
}
}
}
export async function makeLabelForStandaloneDatabase({ id, image, volume }) {
const database = await prisma.database.findFirst({ where: { id } });
delete database.destinationDockerId;
delete database.createdAt;
delete database.updatedAt;
return [
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.type=standalone-database`,
`coolify.name=${database.name}`,
`coolify.configuration=${base64Encode(
JSON.stringify({
version,
image,
volume,
...database
})
)}`
];
}