Just v2
This commit is contained in:
Andras Bacsai
2022-02-10 15:47:44 +01:00
committed by GitHub
parent a64b095c13
commit 460ae85226
403 changed files with 22039 additions and 12465 deletions

View 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>

View 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> -->

View 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>

View 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>

View 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>

View 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 />

View 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);
}
};

View 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);
}
};

View 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>

View 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);
}
};

View 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>

View 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);
}
};

View 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>

View 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);
}
};

View 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
// }
// }

View 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} />

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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>