wip: trpc
This commit is contained in:
parent
568ab24fd9
commit
97313e4180
@ -42,6 +42,7 @@ interface AppSession {
|
||||
gitlab: string | null;
|
||||
};
|
||||
pendingInvitations: Array<any>;
|
||||
isARM: boolean
|
||||
}
|
||||
|
||||
export const appSession: Writable<AppSession> = writable({
|
||||
@ -61,7 +62,8 @@ export const appSession: Writable<AppSession> = writable({
|
||||
github: null,
|
||||
gitlab: null
|
||||
},
|
||||
pendingInvitations: []
|
||||
pendingInvitations: [],
|
||||
isARM: false
|
||||
});
|
||||
|
||||
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 { executeCommand } from './executeCommand';
|
||||
import { saveBuildLog } from './logging';
|
||||
import { checkContainer } from './docker';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
const customConfig: Config = {
|
||||
dictionaries: [adjectives, colors, animals],
|
||||
@ -22,6 +24,37 @@ export const isDev = env.NODE_ENV === 'development';
|
||||
export const version = '3.13.0';
|
||||
export const sentryDSN =
|
||||
'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> {
|
||||
return await prisma.setting.findUnique({ where: { id: '0' } });
|
||||
@ -707,4 +740,86 @@ export function makeLabelForServices(type) {
|
||||
];
|
||||
}
|
||||
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