mirror of
https://github.com/ershisan99/coolify.git
synced 2026-01-01 12:33:45 +00:00
48
src/routes/services/[id]/_Services/_MinIO.svelte
Normal file
48
src/routes/services/[id]/_Services/_MinIO.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
|
||||
export let service;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MinIO Server</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUser">Root User</label>
|
||||
<div class="col-span-2 ">
|
||||
<input
|
||||
name="rootUser"
|
||||
id="rootUser"
|
||||
placeholder="User to login"
|
||||
value={service.minio.rootUser}
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUserPassword">Root's Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
id="rootUserPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="rootUserPassword"
|
||||
value={service.minio.rootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="publicPort">API Port</label>
|
||||
<div class="col-span-2 ">
|
||||
<input
|
||||
name="publicPort"
|
||||
id="publicPort"
|
||||
value={service.minio.publicPort}
|
||||
disabled
|
||||
readonly
|
||||
placeholder="Generated automatically after start"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
103
src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte
Normal file
103
src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte
Normal file
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
export let service;
|
||||
export let readOnly;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Plausible Analytics</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="email">Email Address</label>
|
||||
<div class="col-span-2">
|
||||
<input
|
||||
name="email"
|
||||
id="email"
|
||||
disabled={readOnly}
|
||||
readonly={readOnly}
|
||||
placeholder="Email address"
|
||||
bind:value={service.plausibleAnalytics.email}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="username">Username</label>
|
||||
<div class="col-span-2">
|
||||
<CopyPasswordField
|
||||
name="username"
|
||||
id="username"
|
||||
disabled={readOnly}
|
||||
readonly={readOnly}
|
||||
placeholder="User to login"
|
||||
bind:value={service.plausibleAnalytics.username}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="password">Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
id="password"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="password"
|
||||
value={service.plausibleAnalytics.password}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="postgresqlUser">Username</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.plausibleAnalytics.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="postgresqlPassword">Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.plausibleAnalytics.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="postgresqlDatabase">Database</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.plausibleAnalytics.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="grid grid-cols-3 items-center">
|
||||
<label for="postgresqlPublicPort">Public Port</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
placeholder="Generated automatically after start"
|
||||
readonly
|
||||
disabled
|
||||
id="postgresqlPublicPort"
|
||||
name="postgresqlPublicPort"
|
||||
value={service.plausibleAnalytics.postgresqlPublicPort}
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
157
src/routes/services/[id]/_Services/_Services.svelte
Normal file
157
src/routes/services/[id]/_Services/_Services.svelte
Normal file
@@ -0,0 +1,157 @@
|
||||
<script lang="ts">
|
||||
export let service;
|
||||
export let isRunning;
|
||||
export let readOnly;
|
||||
|
||||
import { page, session } from '$app/stores';
|
||||
import { post } from '$lib/api';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import MinIo from './_MinIO.svelte';
|
||||
import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
|
||||
import VsCodeServer from './_VSCodeServer.svelte';
|
||||
import Wordpress from './_Wordpress.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let loading = false;
|
||||
let loadingVerification = false;
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/services/${id}/check.json`, { fqdn: service.fqdn });
|
||||
await post(`/services/${id}/${service.type}.json`, { ...service });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function setEmailsToVerified() {
|
||||
loadingVerification = true;
|
||||
try {
|
||||
await post(`/services/${id}/${service.type}/activate.json`, { id: service.id });
|
||||
toast.push('All email verified. You can login now.');
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loadingVerification = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">General</div>
|
||||
{#if $session.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-pink-600={!loading}
|
||||
class:hover:bg-pink-500={!loading}
|
||||
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
|
||||
>
|
||||
{/if}
|
||||
{#if service.type === 'plausibleanalytics' && isRunning}
|
||||
<button
|
||||
on:click|preventDefault={setEmailsToVerified}
|
||||
class:bg-pink-600={!loadingVerification}
|
||||
class:hover:bg-pink-500={!loadingVerification}
|
||||
disabled={loadingVerification}
|
||||
>{loadingVerification ? 'Verifying' : 'Verify emails without SMTP'}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="mt-2 grid grid-cols-3 items-center">
|
||||
<label for="name">Name</label>
|
||||
<div class="col-span-2 ">
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="name"
|
||||
id="name"
|
||||
bind:value={service.name}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="destination">Destination</label>
|
||||
<div class="col-span-2">
|
||||
{#if service.destinationDockerId}
|
||||
<div class="no-underline">
|
||||
<input
|
||||
value={service.destinationDocker.name}
|
||||
id="destination"
|
||||
disabled
|
||||
class="bg-transparent "
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3">
|
||||
<label for="fqdn" class="pt-2">Domain (FQDN)</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://analytics.coollabs.io"
|
||||
readonly={!$session.isAdmin && !isRunning}
|
||||
disabled={!$session.isAdmin || isRunning}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
bind:value={service.fqdn}
|
||||
required
|
||||
/>
|
||||
<Explainer
|
||||
text="If you specify <span class='text-green-600 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you."
|
||||
/>
|
||||
</div>
|
||||
<!-- {:else}
|
||||
<label for="fqdn" class="pt-2">Domain (FQDN)</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://analytics.coollabs.io"
|
||||
readonly={!$session.isAdmin}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
bind:value={service.fqdn}
|
||||
required
|
||||
/>
|
||||
<Explainer
|
||||
text="If you specify <span class='text-green-600'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you."
|
||||
/>
|
||||
</div>
|
||||
{/if} -->
|
||||
</div>
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
<PlausibleAnalytics bind:service {readOnly} />
|
||||
{:else if service.type === 'minio'}
|
||||
<MinIo {service} />
|
||||
{:else if service.type === 'vscodeserver'}
|
||||
<VsCodeServer {service} />
|
||||
{:else if service.type === 'wordpress'}
|
||||
<Wordpress bind:service {isRunning} {readOnly} />
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
<!-- <div class="font-bold flex space-x-1 pb-5">
|
||||
<div class="text-xl tracking-tight mr-4">Features</div>
|
||||
</div>
|
||||
<div class="px-4 sm:px-6 pb-10">
|
||||
<ul class="mt-2 divide-y divide-stone-800">
|
||||
<Setting
|
||||
bind:setting={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!"
|
||||
/>
|
||||
</ul>
|
||||
</div> -->
|
||||
</div>
|
||||
22
src/routes/services/[id]/_Services/_VSCodeServer.svelte
Normal file
22
src/routes/services/[id]/_Services/_VSCodeServer.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
|
||||
export let service;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">VSCode Server</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="password">Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
id="password"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="password"
|
||||
value={service.vscodeserver.password}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
94
src/routes/services/[id]/_Services/_Wordpress.svelte
Normal file
94
src/routes/services/[id]/_Services/_Wordpress.svelte
Normal file
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
|
||||
export let service;
|
||||
export let isRunning;
|
||||
export let readOnly;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Wordpress</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="extraConfig">Extra Config</label>
|
||||
<div class="col-span-2 ">
|
||||
<textarea
|
||||
disabled={isRunning}
|
||||
readonly={isRunning}
|
||||
class:resize-none={isRunning}
|
||||
rows={isRunning ? 1 : 5}
|
||||
name="extraConfig"
|
||||
id="extraConfig"
|
||||
placeholder={!isRunning
|
||||
? `eg:
|
||||
|
||||
define('WP_ALLOW_MULTISITE', true);
|
||||
define('MULTISITE', true);
|
||||
define('SUBDOMAIN_INSTALL', false);`
|
||||
: null}>{service.wordpress.extraConfig}</textarea
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MySQL</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="mysqlDatabase">Database</label>
|
||||
<div class="col-span-2 ">
|
||||
<input
|
||||
name="mysqlDatabase"
|
||||
id="mysqlDatabase"
|
||||
required
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.wordpress.mysqlDatabase}
|
||||
placeholder="eg: wordpress_db"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="mysqlRootUser">Root User</label>
|
||||
<div class="col-span-2 ">
|
||||
<input
|
||||
name="mysqlRootUser"
|
||||
id="mysqlRootUser"
|
||||
placeholder="MySQL Root User"
|
||||
value={service.wordpress.mysqlRootUser}
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="mysqlRootUserPassword">Root's Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
id="mysqlRootUserPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="mysqlRootUserPassword"
|
||||
value={service.wordpress.mysqlRootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="mysqlUser">User</label>
|
||||
<div class="col-span-2 ">
|
||||
<input name="mysqlUser" id="mysqlUser" value={service.wordpress.mysqlUser} disabled readonly />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="mysqlPassword">Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
id="mysqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="mysqlPassword"
|
||||
value={service.wordpress.mysqlPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
202
src/routes/services/[id]/__layout.svelte
Normal file
202
src/routes/services/[id]/__layout.svelte
Normal file
@@ -0,0 +1,202 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
function checkConfiguration(service): string {
|
||||
let configurationPhase = null;
|
||||
if (!service.type) {
|
||||
configurationPhase = 'type';
|
||||
} else if (!service.version) {
|
||||
configurationPhase = 'version';
|
||||
} else if (!service.destinationDockerId) {
|
||||
configurationPhase = 'destination';
|
||||
}
|
||||
return configurationPhase;
|
||||
}
|
||||
export const load: Load = async ({ fetch, params, url }) => {
|
||||
let readOnly = false;
|
||||
const endpoint = `/services/${params.id}.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
const { service, isRunning } = await res.json();
|
||||
if (!service || Object.entries(service).length === 0) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/databases'
|
||||
};
|
||||
}
|
||||
const configurationPhase = checkConfiguration(service);
|
||||
if (
|
||||
configurationPhase &&
|
||||
url.pathname !== `/services/${params.id}/configuration/${configurationPhase}`
|
||||
) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}/configuration/${configurationPhase}`
|
||||
};
|
||||
}
|
||||
if (service.plausibleAnalytics?.email && service.plausibleAnalytics.username) readOnly = true;
|
||||
if (service.wordpress?.mysqlDatabase) readOnly = true;
|
||||
|
||||
return {
|
||||
props: {
|
||||
service,
|
||||
isRunning
|
||||
},
|
||||
stuff: {
|
||||
service,
|
||||
isRunning,
|
||||
readOnly
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/services'
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { session } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import { del, post } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let service;
|
||||
export let isRunning;
|
||||
|
||||
let loading = false;
|
||||
|
||||
async function deleteService() {
|
||||
const sure = confirm(`Are you sure you would like to delete '${service.name}'?`);
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
if (service.type) await post(`/services/${service.id}/${service.type}/stop.json`, {});
|
||||
await del(`/services/${service.id}/delete.json`, { id: service.id });
|
||||
return await goto(`/services`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopService() {
|
||||
const sure = confirm(`Are you sure you would like to stop '${service.name}'?`);
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/services/${service.id}/${service.type}/stop.json`, {});
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function startService() {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/services/${service.id}/${service.type}/start.json`, {});
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
if (
|
||||
service.type &&
|
||||
service.destinationDockerId &&
|
||||
service.version &&
|
||||
service.fqdn &&
|
||||
!isRunning
|
||||
) {
|
||||
try {
|
||||
await post(`/services/${service.id}/${service.type}/stop.json`, {});
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav class="nav-side">
|
||||
{#if loading}
|
||||
<Loading fullscreen cover />
|
||||
{:else}
|
||||
{#if service.type && service.destinationDockerId && service.version && service.fqdn}
|
||||
{#if isRunning}
|
||||
<button
|
||||
on:click={stopService}
|
||||
title="Stop Service"
|
||||
type="submit"
|
||||
disabled={!$session.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-pink-600 hover:text-white"
|
||||
data-tooltip={$session.isAdmin
|
||||
? 'Stop Service'
|
||||
: 'You do not have permission to stop the service.'}
|
||||
>
|
||||
<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>
|
||||
{:else}
|
||||
<button
|
||||
on:click={startService}
|
||||
title="Start Service"
|
||||
type="submit"
|
||||
disabled={!$session.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-pink-600 hover:text-white"
|
||||
data-tooltip={$session.isAdmin
|
||||
? 'Start Service'
|
||||
: 'You do not have permission to start the service.'}
|
||||
><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>
|
||||
{/if}
|
||||
{/if}
|
||||
<button
|
||||
on:click={deleteService}
|
||||
title="Delete Service"
|
||||
type="submit"
|
||||
disabled={!$session.isAdmin}
|
||||
class:hover:text-red-500={$session.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$session.isAdmin
|
||||
? 'Delete Service'
|
||||
: 'You do not have permission to delete a service.'}><DeleteIcon /></button
|
||||
>
|
||||
{/if}
|
||||
</nav>
|
||||
<slot />
|
||||
26
src/routes/services/[id]/check.json.ts
Normal file
26
src/routes/services/[id]/check.json.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
let { fqdn } = await event.request.json();
|
||||
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
const found = await db.isDomainConfigured({ id, fqdn });
|
||||
return {
|
||||
status: found ? 500 : 200,
|
||||
body: {
|
||||
error: found && `Domain ${getDomain(fqdn)} is already configured`
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
19
src/routes/services/[id]/configuration/destination.json.ts
Normal file
19
src/routes/services/[id]/configuration/destination.json.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
const { id } = event.params;
|
||||
|
||||
const { destinationId } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.configureDestinationForService({ id, destinationId });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
91
src/routes/services/[id]/configuration/destination.svelte
Normal file
91
src/routes/services/[id]/configuration/destination.svelte
Normal file
@@ -0,0 +1,91 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
const { service } = stuff;
|
||||
if (service?.destinationDockerId && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/service/${params.id}`
|
||||
};
|
||||
}
|
||||
|
||||
const endpoint = `/destinations.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type Prisma from '@prisma/client';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { enhance, errorNotification } from '$lib/form';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let destinations: Prisma.DestinationDocker[];
|
||||
async function handleSubmit(destinationId) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/destination.json`, { destinationId });
|
||||
return await goto(from || `/services/${id}`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
{#if !destinations || destinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">No configurable Destination found</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each destinations as destination}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
||||
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
|
||||
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
|
||||
<div class="text-center truncate">{destination.network}</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
32
src/routes/services/[id]/configuration/type.json.ts
Normal file
32
src/routes/services/[id]/configuration/type.json.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler, supportedServiceTypesAndVersions } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
types: supportedServiceTypesAndVersions
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
const { type } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.configureServiceType({ id, type });
|
||||
return {
|
||||
status: 201
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
79
src/routes/services/[id]/configuration/type.svelte
Normal file
79
src/routes/services/[id]/configuration/type.svelte
Normal file
@@ -0,0 +1,79 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
const { service } = stuff;
|
||||
if (service?.type && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}`
|
||||
};
|
||||
}
|
||||
const endpoint = `/services/${params.id}/configuration/type.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
|
||||
import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
|
||||
import MinIo from '$lib/components/svg/services/MinIO.svelte';
|
||||
import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte';
|
||||
import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let types;
|
||||
|
||||
async function handleSubmit(type) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/type.json`, { type });
|
||||
return await goto(from || `/services/${id}`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Select a Service</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each types as type}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
|
||||
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600">
|
||||
{#if type.name === 'plausibleanalytics'}
|
||||
<PlausibleAnalytics isAbsolute />
|
||||
{:else if type.name === 'nocodb'}
|
||||
<NocoDb isAbsolute />
|
||||
{:else if type.name === 'minio'}
|
||||
<MinIo isAbsolute />
|
||||
{:else if type.name === 'vscodeserver'}
|
||||
<VsCodeServer isAbsolute />
|
||||
{:else if type.name === 'wordpress'}
|
||||
<Wordpress isAbsolute />
|
||||
{/if}{type.fancyName}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
40
src/routes/services/[id]/configuration/version.json.ts
Normal file
40
src/routes/services/[id]/configuration/version.json.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler, supportedServiceTypesAndVersions } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const { type } = await db.getService({ id, teamId });
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
versions: supportedServiceTypesAndVersions.find((name) => name.name === type).versions
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
const { version } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.setService({ id, version });
|
||||
return {
|
||||
status: 201
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
63
src/routes/services/[id]/configuration/version.svelte
Normal file
63
src/routes/services/[id]/configuration/version.svelte
Normal file
@@ -0,0 +1,63 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
const { service } = stuff;
|
||||
if (service?.version && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}`
|
||||
};
|
||||
}
|
||||
const endpoint = `/services/${params.id}/configuration/version.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let versions;
|
||||
async function handleSubmit(version) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/version.json`, { version });
|
||||
return await goto(from || `/services/${id}`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Select a Service version</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each versions as version}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(version)}>
|
||||
<button type="submit" class="box-selection text-xl font-bold hover:bg-pink-600"
|
||||
>{version}</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
18
src/routes/services/[id]/delete.json.ts
Normal file
18
src/routes/services/[id]/delete.json.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const del: RequestHandler = async (events) => {
|
||||
const { teamId, status, body } = await getUserDetails(events);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = events.params;
|
||||
|
||||
try {
|
||||
await db.removeService({ id });
|
||||
return { status: 200 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
69
src/routes/services/[id]/index.json.ts
Normal file
69
src/routes/services/[id]/index.json.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import {
|
||||
generateDatabaseConfiguration,
|
||||
getServiceImage,
|
||||
getVersions,
|
||||
PrismaErrorHandler
|
||||
} from '$lib/database';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, type, version } = service;
|
||||
|
||||
let isRunning = false;
|
||||
if (destinationDockerId) {
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
const baseImage = getServiceImage(type);
|
||||
docker.engine.pull(`${baseImage}:${version}`);
|
||||
try {
|
||||
const { stdout } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
|
||||
);
|
||||
|
||||
if (JSON.parse(stdout).Running) {
|
||||
isRunning = true;
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
return {
|
||||
body: {
|
||||
isRunning,
|
||||
service
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
// export const post: RequestHandler<Locals, FormData> = async (request) => {
|
||||
// const { teamId, status, body } = await getUserDetails(request);
|
||||
// if (status === 401) return { status, body }
|
||||
// const { id } = request.params
|
||||
|
||||
// const name = request.body.get('name')
|
||||
// const defaultDatabase = request.body.get('defaultDatabase')
|
||||
// const dbUser = request.body.get('dbUser')
|
||||
// const dbUserPassword = request.body.get('dbUserPassword')
|
||||
// const rootUser = request.body.get('rootUser')
|
||||
// const rootUserPassword = request.body.get('rootUserPassword')
|
||||
// const version = request.body.get('version')
|
||||
|
||||
// try {
|
||||
// return await db.updateDatabase({ id, name, defaultDatabase, dbUser, dbUserPassword, rootUser, rootUserPassword, version })
|
||||
// } catch (err) {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// }
|
||||
101
src/routes/services/[id]/index.svelte
Normal file
101
src/routes/services/[id]/index.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff }) => {
|
||||
if (stuff?.service?.id) {
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
isRunning: stuff.isRunning,
|
||||
readOnly: stuff.readOnly
|
||||
}
|
||||
};
|
||||
}
|
||||
const endpoint = `/services/${params.id}.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${endpoint}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
|
||||
import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
|
||||
import MinIo from '$lib/components/svg/services/MinIO.svelte';
|
||||
import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte';
|
||||
import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
|
||||
import Services from './_Services/_Services.svelte';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
|
||||
export let service;
|
||||
export let isRunning;
|
||||
export let readOnly;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center space-x-3 px-6 text-2xl font-bold"
|
||||
class:p-5={service.fqdn}
|
||||
class:p-6={!service.fqdn}
|
||||
>
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
{service.name}
|
||||
</div>
|
||||
{#if service.fqdn}
|
||||
<a
|
||||
href={service.fqdn}
|
||||
target="_blank"
|
||||
class="icons tooltip-bottom flex items-center 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="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||
<line x1="10" y1="14" x2="20" y2="4" />
|
||||
<polyline points="15 4 20 4 20 9" />
|
||||
</svg></a
|
||||
>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
<a href="https://plausible.io" target="_blank">
|
||||
<PlausibleAnalytics />
|
||||
</a>
|
||||
{:else if service.type === 'nocodb'}
|
||||
<a href="https://nocodb.com" target="_blank">
|
||||
<NocoDb />
|
||||
</a>
|
||||
{:else if service.type === 'minio'}
|
||||
<a href="https://min.io" target="_blank">
|
||||
<MinIo />
|
||||
</a>
|
||||
{:else if service.type === 'vscodeserver'}
|
||||
<a href="https://coder.com" target="_blank">
|
||||
<VsCodeServer />
|
||||
</a>
|
||||
{:else if service.type === 'wordpress'}
|
||||
<a href="https://wordpress.org" target="_blank">
|
||||
<Wordpress />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Services bind:service {isRunning} {readOnly} />
|
||||
21
src/routes/services/[id]/minio/index.json.ts
Normal file
21
src/routes/services/[id]/minio/index.json.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
let { name, fqdn } = await event.request.json();
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateNocoDbOrMinioService({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
107
src/routes/services/[id]/minio/start.json.ts
Normal file
107
src/routes/services/[id]/minio/start.json.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { promises as fs } from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { letsEncrypt } from '$lib/letsencrypt';
|
||||
import {
|
||||
configureSimpleServiceProxyOn,
|
||||
reloadHaproxy,
|
||||
startHttpProxy,
|
||||
startTcpProxy
|
||||
} from '$lib/haproxy';
|
||||
import getPort from 'get-port';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
fqdn,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
minio: { rootUser, rootUserPassword }
|
||||
} = service;
|
||||
|
||||
const domain = getDomain(fqdn);
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const publicPort = await getPort();
|
||||
const consolePort = 9001;
|
||||
const apiPort = 9000;
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
|
||||
const config = {
|
||||
image: `minio/minio:${version}`,
|
||||
volume: `${id}-minio-data:/data`,
|
||||
environmentVariables: {
|
||||
MINIO_ROOT_USER: rootUser,
|
||||
MINIO_ROOT_PASSWORD: rootUserPassword,
|
||||
MINIO_BROWSER_REDIRECT_URL: fqdn
|
||||
}
|
||||
};
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: `minio/minio:${version}`,
|
||||
command: `server /data --console-address ":${consolePort}"`,
|
||||
environment: config.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [config.volume],
|
||||
restart: 'always'
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[config.volume.split(':')[0]]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
try {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
await configureSimpleServiceProxyOn({ id, domain, port: consolePort });
|
||||
|
||||
await db.updateMinioService({ id, publicPort });
|
||||
await startHttpProxy(destinationDocker, id, publicPort, apiPort);
|
||||
|
||||
if (isHttps) {
|
||||
await letsEncrypt({ domain, id });
|
||||
}
|
||||
await reloadHaproxy(destinationDocker.engine);
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
46
src/routes/services/[id]/minio/stop.json.ts
Normal file
46
src/routes/services/[id]/minio/stop.json.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { getEngine, getUserDetails, removeDestinationDocker } from '$lib/common';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import { checkContainer, configureSimpleServiceProxyOff, stopTcpHttpProxy } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const {
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
fqdn,
|
||||
minio: { publicPort }
|
||||
} = service;
|
||||
await db.updateMinioService({ id, publicPort: null });
|
||||
const domain = getDomain(fqdn);
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
await stopTcpHttpProxy(destinationDocker, publicPort);
|
||||
await configureSimpleServiceProxyOff({ domain });
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
20
src/routes/services/[id]/nocodb/index.json.ts
Normal file
20
src/routes/services/[id]/nocodb/index.json.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
const { id } = event.params;
|
||||
|
||||
let { name, fqdn } = await event.request.json();
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateNocoDbOrMinioService({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
66
src/routes/services/[id]/nocodb/start.json.ts
Normal file
66
src/routes/services/[id]/nocodb/start.json.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { promises as fs } from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { letsEncrypt } from '$lib/letsencrypt';
|
||||
import { configureSimpleServiceProxyOn, reloadHaproxy } from '$lib/haproxy';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const { type, version, fqdn, destinationDockerId, destinationDocker } = service;
|
||||
|
||||
const domain = getDomain(fqdn);
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: `nocodb/nocodb:${version}`,
|
||||
networks: [network],
|
||||
restart: 'always'
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
await configureSimpleServiceProxyOn({ id, domain, port: 8080 });
|
||||
|
||||
if (isHttps) {
|
||||
await letsEncrypt({ domain, id });
|
||||
}
|
||||
await reloadHaproxy(destinationDocker.engine);
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
39
src/routes/services/[id]/nocodb/stop.json.ts
Normal file
39
src/routes/services/[id]/nocodb/stop.json.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { getUserDetails, removeDestinationDocker } from '$lib/common';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import { checkContainer, configureSimpleServiceProxyOff } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const domain = getDomain(fqdn);
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
await configureSimpleServiceProxyOff({ domain });
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
33
src/routes/services/[id]/plausibleanalytics/activate.json.ts
Normal file
33
src/routes/services/[id]/plausibleanalytics/activate.json.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = await event.request.json();
|
||||
|
||||
try {
|
||||
const {
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
plausibleAnalytics: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
|
||||
} = await db.getService({ id, teamId });
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
const container = await docker.engine.getContainer(id);
|
||||
const command = await container.exec({
|
||||
Cmd: [
|
||||
`psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
|
||||
]
|
||||
});
|
||||
await command.start();
|
||||
}
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
26
src/routes/services/[id]/plausibleanalytics/index.json.ts
Normal file
26
src/routes/services/[id]/plausibleanalytics/index.json.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
let {
|
||||
name,
|
||||
fqdn,
|
||||
plausibleAnalytics: { email, username }
|
||||
} = await event.request.json();
|
||||
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
if (email) email = email.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updatePlausibleAnalyticsService({ id, fqdn, name, email, username });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
195
src/routes/services/[id]/plausibleanalytics/start.json.ts
Normal file
195
src/routes/services/[id]/plausibleanalytics/start.json.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { promises as fs } from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { letsEncrypt } from '$lib/letsencrypt';
|
||||
import { configureSimpleServiceProxyOn, reloadHaproxy } from '$lib/haproxy';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
fqdn,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
plausibleAnalytics: {
|
||||
id: plausibleDbId,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
postgresqlDatabase,
|
||||
postgresqlPassword,
|
||||
postgresqlUser,
|
||||
secretKeyBase
|
||||
}
|
||||
} = service;
|
||||
|
||||
const domain = getDomain(fqdn);
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
|
||||
const config = {
|
||||
plausibleAnalytics: {
|
||||
image: `plausible/analytics:${version}`,
|
||||
environmentVariables: {
|
||||
ADMIN_USER_EMAIL: email,
|
||||
ADMIN_USER_NAME: username,
|
||||
ADMIN_USER_PWD: password,
|
||||
BASE_URL: fqdn,
|
||||
SECRET_KEY_BASE: secretKeyBase,
|
||||
DISABLE_AUTH: 'false',
|
||||
DISABLE_REGISTRATION: 'true',
|
||||
DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`,
|
||||
CLICKHOUSE_DATABASE_URL: `http://${id}-clickhouse:8123/plausible`
|
||||
}
|
||||
},
|
||||
postgresql: {
|
||||
volume: `${plausibleDbId}-postgresql-data:/var/lib/postgresql/data`,
|
||||
image: 'bitnami/postgresql:13.2.0',
|
||||
environmentVariables: {
|
||||
POSTGRESQL_PASSWORD: postgresqlPassword,
|
||||
POSTGRESQL_USERNAME: postgresqlUser,
|
||||
POSTGRESQL_DATABASE: postgresqlDatabase
|
||||
}
|
||||
},
|
||||
clickhouse: {
|
||||
volume: `${plausibleDbId}-clickhouse-data:/var/lib/clickhouse`,
|
||||
image: 'yandex/clickhouse-server:21.3.2.5',
|
||||
environmentVariables: {},
|
||||
ulimits: {
|
||||
nofile: {
|
||||
soft: 262144,
|
||||
hard: 262144
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const engine = destinationDocker.engine;
|
||||
// const labels = await makeLabelForPlausibleAnalytics({ id, })
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
|
||||
const clickhouseConfigXml = `
|
||||
<yandex>
|
||||
<logger>
|
||||
<level>warning</level>
|
||||
<console>true</console>
|
||||
</logger>
|
||||
|
||||
<!-- Stop all the unnecessary logging -->
|
||||
<query_thread_log remove="remove"/>
|
||||
<query_log remove="remove"/>
|
||||
<text_log remove="remove"/>
|
||||
<trace_log remove="remove"/>
|
||||
<metric_log remove="remove"/>
|
||||
<asynchronous_metric_log remove="remove"/>
|
||||
</yandex>`;
|
||||
const clickhouseUserConfigXml = `
|
||||
<yandex>
|
||||
<profiles>
|
||||
<default>
|
||||
<log_queries>0</log_queries>
|
||||
<log_query_threads>0</log_query_threads>
|
||||
</default>
|
||||
</profiles>
|
||||
</yandex>`;
|
||||
|
||||
const initQuery = 'CREATE DATABASE IF NOT EXISTS plausible;';
|
||||
const initScript = 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query';
|
||||
await fs.writeFile(`${workdir}/clickhouse-config.xml`, clickhouseConfigXml);
|
||||
await fs.writeFile(`${workdir}/clickhouse-user-config.xml`, clickhouseUserConfigXml);
|
||||
await fs.writeFile(`${workdir}/init.query`, initQuery);
|
||||
await fs.writeFile(`${workdir}/init-db.sh`, initScript);
|
||||
|
||||
const Dockerfile = `
|
||||
FROM ${config.clickhouse.image}
|
||||
COPY ./clickhouse-config.xml /etc/clickhouse-server/users.d/logging.xml
|
||||
COPY ./clickhouse-user-config.xml /etc/clickhouse-server/config.d/logging.xml
|
||||
COPY ./init.query /docker-entrypoint-initdb.d/init.query
|
||||
COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
|
||||
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: config.plausibleAnalytics.image,
|
||||
command:
|
||||
'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"',
|
||||
networks: [network],
|
||||
environment: config.plausibleAnalytics.environmentVariables,
|
||||
volumes: [config.postgresql.volume],
|
||||
restart: 'always',
|
||||
depends_on: [`${id}-postgresql`, `${id}-clickhouse`]
|
||||
},
|
||||
[`${id}-postgresql`]: {
|
||||
container_name: `${id}-postgresql`,
|
||||
image: config.postgresql.image,
|
||||
networks: [network],
|
||||
environment: config.postgresql.environmentVariables,
|
||||
volumes: [config.postgresql.volume],
|
||||
restart: 'always'
|
||||
},
|
||||
[`${id}-clickhouse`]: {
|
||||
build: workdir,
|
||||
container_name: `${id}-clickhouse`,
|
||||
networks: [network],
|
||||
environment: config.clickhouse.environmentVariables,
|
||||
volumes: [config.clickhouse.volume],
|
||||
restart: 'always'
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[config.postgresql.volume.split(':')[0]]: {
|
||||
external: true
|
||||
},
|
||||
[config.clickhouse.volume.split(':')[0]]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
try {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker volume create ${config.postgresql.volume.split(':')[0]}`
|
||||
);
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker volume create ${config.clickhouse.volume.split(':')[0]}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d`
|
||||
);
|
||||
await configureSimpleServiceProxyOn({ id, domain, port: 8000 });
|
||||
|
||||
if (isHttps) {
|
||||
await letsEncrypt({ domain, id });
|
||||
}
|
||||
await reloadHaproxy(destinationDocker.engine);
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
49
src/routes/services/[id]/plausibleanalytics/stop.json.ts
Normal file
49
src/routes/services/[id]/plausibleanalytics/stop.json.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { getUserDetails, removeDestinationDocker } from '$lib/common';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import { checkContainer, configureSimpleServiceProxyOff } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const domain = getDomain(fqdn);
|
||||
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
let found = await checkContainer(engine, id);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id, engine });
|
||||
}
|
||||
found = await checkContainer(engine, `${id}-postgresql`);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
|
||||
}
|
||||
found = await checkContainer(engine, `${id}-clickhouse`);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id: `${id}-clickhouse`, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
await configureSimpleServiceProxyOff({ domain });
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
21
src/routes/services/[id]/vscodeserver/index.json.ts
Normal file
21
src/routes/services/[id]/vscodeserver/index.json.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
let { name, fqdn } = await event.request.json();
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateVsCodeServer({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
93
src/routes/services/[id]/vscodeserver/start.json.ts
Normal file
93
src/routes/services/[id]/vscodeserver/start.json.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { promises as fs } from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { letsEncrypt } from '$lib/letsencrypt';
|
||||
import { configureSimpleServiceProxyOn, reloadHaproxy } from '$lib/haproxy';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
fqdn,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
vscodeserver: { password }
|
||||
} = service;
|
||||
|
||||
const domain = getDomain(fqdn);
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const config = {
|
||||
image: `codercom/code-server:${version}`,
|
||||
volume: `${id}-vscodeserver-data:/home/coder`,
|
||||
environmentVariables: {
|
||||
PASSWORD: password
|
||||
}
|
||||
};
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: config.image,
|
||||
environment: config.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [config.volume],
|
||||
restart: 'always'
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[config.volume.split(':')[0]]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
try {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
await configureSimpleServiceProxyOn({ id, domain, port: 8080 });
|
||||
|
||||
if (isHttps) {
|
||||
await letsEncrypt({ domain, id });
|
||||
}
|
||||
await reloadHaproxy(destinationDocker.engine);
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
38
src/routes/services/[id]/vscodeserver/stop.json.ts
Normal file
38
src/routes/services/[id]/vscodeserver/stop.json.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { getUserDetails, removeDestinationDocker } from '$lib/common';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import { checkContainer, configureSimpleServiceProxyOff } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const domain = getDomain(fqdn);
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
await configureSimpleServiceProxyOff({ domain });
|
||||
}
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
24
src/routes/services/[id]/wordpress/index.json.ts
Normal file
24
src/routes/services/[id]/wordpress/index.json.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
const { id } = event.params;
|
||||
|
||||
let {
|
||||
name,
|
||||
fqdn,
|
||||
wordpress: { extraConfig, mysqlDatabase }
|
||||
} = await event.request.json();
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateWordpress({ id, fqdn, name, extraConfig, mysqlDatabase });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
131
src/routes/services/[id]/wordpress/start.json.ts
Normal file
131
src/routes/services/[id]/wordpress/start.json.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { promises as fs } from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { letsEncrypt } from '$lib/letsencrypt';
|
||||
import { configureSimpleServiceProxyOn, reloadHaproxy } from '$lib/haproxy';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
fqdn,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
wordpress: {
|
||||
mysqlDatabase,
|
||||
mysqlUser,
|
||||
mysqlPassword,
|
||||
extraConfig,
|
||||
mysqlRootUser,
|
||||
mysqlRootUserPassword
|
||||
}
|
||||
} = service;
|
||||
|
||||
const domain = getDomain(fqdn);
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const config = {
|
||||
wordpress: {
|
||||
image: `wordpress:${version}`,
|
||||
volume: `${id}-wordpress-data:/var/www/html`,
|
||||
environmentVariables: {
|
||||
WORDPRESS_DB_HOST: `${id}-mysql`,
|
||||
WORDPRESS_DB_USER: mysqlUser,
|
||||
WORDPRESS_DB_PASSWORD: mysqlPassword,
|
||||
WORDPRESS_DB_NAME: mysqlDatabase,
|
||||
WORDPRESS_CONFIG_EXTRA: extraConfig
|
||||
}
|
||||
},
|
||||
mysql: {
|
||||
image: `bitnami/mysql:5.7`,
|
||||
volume: `${id}-mysql-data:/bitnami/mysql/data`,
|
||||
environmentVariables: {
|
||||
MYSQL_ROOT_PASSWORD: mysqlRootUserPassword,
|
||||
MYSQL_ROOT_USER: mysqlRootUser,
|
||||
MYSQL_USER: mysqlUser,
|
||||
MYSQL_PASSWORD: mysqlPassword,
|
||||
MYSQL_DATABASE: mysqlDatabase
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: config.wordpress.image,
|
||||
environment: config.wordpress.environmentVariables,
|
||||
networks: [network],
|
||||
restart: 'always',
|
||||
depends_on: [`${id}-mysql`]
|
||||
},
|
||||
[`${id}-mysql`]: {
|
||||
container_name: `${id}-mysql`,
|
||||
image: config.mysql.image,
|
||||
environment: config.mysql.environmentVariables,
|
||||
networks: [network],
|
||||
restart: 'always'
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[config.mysql.volume.split(':')[0]]: {
|
||||
external: true
|
||||
},
|
||||
[config.wordpress.volume.split(':')[0]]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
try {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker volume create ${config.mysql.volume.split(':')[0]}`
|
||||
);
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker volume create ${config.wordpress.volume.split(':')[0]}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
await configureSimpleServiceProxyOn({ id, domain, port: 80 });
|
||||
|
||||
if (isHttps) {
|
||||
await letsEncrypt({ domain, id });
|
||||
}
|
||||
await reloadHaproxy(destinationDocker.engine);
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
42
src/routes/services/[id]/wordpress/stop.json.ts
Normal file
42
src/routes/services/[id]/wordpress/stop.json.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getUserDetails, removeDestinationDocker } from '$lib/common';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import { checkContainer, configureSimpleServiceProxyOff } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const domain = getDomain(fqdn);
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
try {
|
||||
let found = await checkContainer(engine, id);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id, engine });
|
||||
}
|
||||
found = await checkContainer(engine, `${id}-mysql`);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id: `${id}-mysql`, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
await configureSimpleServiceProxyOff({ domain });
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
20
src/routes/services/index.json.ts
Normal file
20
src/routes/services/index.json.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
try {
|
||||
const services = await db.listServices(teamId);
|
||||
return {
|
||||
body: {
|
||||
services
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
85
src/routes/services/index.svelte
Normal file
85
src/routes/services/index.svelte
Normal file
@@ -0,0 +1,85 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch }) => {
|
||||
const url = `/services.json`;
|
||||
const res = await fetch(url);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
|
||||
import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
|
||||
import MinIo from '$lib/components/svg/services/MinIO.svelte';
|
||||
import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte';
|
||||
import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
|
||||
|
||||
export let services;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Services</div>
|
||||
<a href="/new/service" class="add-icon bg-pink-600 hover:bg-pink-500">
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#if !services || services.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">No services found</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each services as service}
|
||||
<a href="/services/{service.id}" class="no-underline p-2 w-96">
|
||||
<div class="box-selection relative hover:bg-pink-600 group">
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
<PlausibleAnalytics isAbsolute />
|
||||
{:else if service.type === 'nocodb'}
|
||||
<NocoDb isAbsolute />
|
||||
{:else if service.type === 'minio'}
|
||||
<MinIo isAbsolute />
|
||||
{:else if service.type === 'vscodeserver'}
|
||||
<VsCodeServer isAbsolute />
|
||||
{:else if service.type === 'wordpress'}
|
||||
<Wordpress isAbsolute />
|
||||
{/if}
|
||||
<div class="font-bold text-xl text-center truncate">
|
||||
{service.name}
|
||||
</div>
|
||||
{#if !service.type || !service.fqdn}
|
||||
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center truncate">{service.type}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user