wip: trpc
This commit is contained in:
parent
568ab24fd9
commit
97313e4180
@ -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 {
|
||||||
|
305
apps/client/src/routes/databases/[id]/+layout.svelte
Normal file
305
apps/client/src/routes/databases/[id]/+layout.svelte
Normal 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 />
|
48
apps/client/src/routes/databases/[id]/+layout.ts
Normal file
48
apps/client/src/routes/databases/[id]/+layout.ts
Normal 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.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
62
apps/client/src/routes/databases/[id]/+page.svelte
Normal file
62
apps/client/src/routes/databases/[id]/+page.svelte
Normal 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} />
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
37
apps/client/src/routes/databases/[id]/utils.ts
Normal file
37
apps/client/src/routes/databases/[id]/utils.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {};
|
|
||||||
})
|
|
||||||
});
|
|
379
apps/server/src/trpc/routers/databases/index.ts
Normal file
379
apps/server/src/trpc/routers/databases/index.ts
Normal 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 {};
|
||||||
|
})
|
||||||
|
});
|
283
apps/server/src/trpc/routers/databases/lib.ts
Normal file
283
apps/server/src/trpc/routers/databases/lib.ts
Normal 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
|
||||||
|
})
|
||||||
|
)}`
|
||||||
|
];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user