feat: Fider service

This commit is contained in:
Andras Bacsai 2022-04-29 22:25:04 +02:00
parent e5b1ce4eef
commit a3fd95020d
9 changed files with 456 additions and 0 deletions

View File

@ -12,6 +12,7 @@
import VaultWarden from './svg/services/VaultWarden.svelte'; import VaultWarden from './svg/services/VaultWarden.svelte';
import VsCodeServer from './svg/services/VSCodeServer.svelte'; import VsCodeServer from './svg/services/VSCodeServer.svelte';
import Wordpress from './svg/services/Wordpress.svelte'; import Wordpress from './svg/services/Wordpress.svelte';
import Fider from './svg/services/Fider.svelte';
</script> </script>
{#if service.type === 'plausibleanalytics'} {#if service.type === 'plausibleanalytics'}
@ -62,4 +63,8 @@
<a href="https://hasura.io" target="_blank"> <a href="https://hasura.io" target="_blank">
<Hasura /> <Hasura />
</a> </a>
{:else if service.type === 'fider'}
<a href="https://fider.io" target="_blank">
<Fider />
</a>
{/if} {/if}

View File

@ -202,5 +202,16 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 8080 main: 8080
} }
},
{
name: 'fider',
fancyName: 'Fider',
baseImage: 'getfider/fider',
images: ['postgres:12-alpine'],
versions: ['stable'],
recommendedVersion: 'stable',
ports: {
main: 3000
}
} }
]; ];

View File

@ -0,0 +1,183 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
import Select from 'svelte-select';
export let service;
export let readOnly;
let mailgunRegions = [
{
value: 'EU',
label: 'EU'
},
{
value: 'US',
label: 'US'
}
];
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Fider</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="jwtSecret">JWT Secret</label>
<CopyPasswordField
name="jwtSecret"
id="jwtSecret"
isPasswordField
value={service.fider.jwtSecret}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailNoreply">Noreply Email</label>
<input
name="emailNoreply"
id="emailNoreply"
type="email"
required
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailNoreply}
placeholder="{$t('forms.eg')}: noreply@yourdomain.com"
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Email</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailMailgunApiKey">Mailgun API Key</label>
<CopyPasswordField
name="emailMailgunApiKey"
id="emailMailgunApiKey"
isPasswordField
bind:value={service.fider.emailMailgunApiKey}
readonly={readOnly}
disabled={readOnly}
placeholder="{$t('forms.eg')}: key-yourkeygoeshere"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailMailgunDomain">Mailgun Domain</label>
<input
name="emailMailgunDomain"
id="emailMailgunDomain"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailMailgunDomain}
placeholder="{$t('forms.eg')}: yourdomain.com"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailMailgunRegion">Mailgun Region</label>
<div class="custom-select-wrapper">
<Select
id="baseBuildImages"
items={mailgunRegions}
on:select={(event) => (service.fider.emailMailgunRegion = event.detail.value)}
value={service.fider.emailMailgunRegion || 'EU'}
isClearable={false}
/>
</div>
</div>
<div class="flex space-x-1 py-5 px-10 font-bold">
<div class="text-lg">Or</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpHost">SMTP Host</label>
<input
name="emailSmtpHost"
id="emailSmtpHost"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailSmtpHost}
placeholder="{$t('forms.eg')}: smtp.yourdomain.com"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPort">SMTP Port</label>
<input
name="emailSmtpPort"
id="emailSmtpPort"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailSmtpPort}
placeholder="{$t('forms.eg')}: 587"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpUser">SMTP User</label>
<input
name="emailSmtpUser"
id="emailSmtpUser"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailSmtpUser}
placeholder="{$t('forms.eg')}: user@yourdomain.com"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPassword">SMTP Password</label>
<CopyPasswordField
name="emailSmtpPassword"
id="emailSmtpPassword"
isPasswordField
value={service.fider.emailSmtpPassword}
readonly={readOnly}
disabled={readOnly}
placeholder="{$t('forms.eg')}: s0m3p4ssw0rd"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpEnableStartTls">SMTP Start TLS</label>
<input
name="emailSmtpEnableStartTls"
id="emailSmtpEnableStartTls"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailSmtpEnableStartTls}
placeholder="{$t('forms.eg')}: true"
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser">{$t('forms.username')}</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.fider.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="postgresqlPassword"
isPasswordField
readonly
disabled
name="postgresqlPassword"
value={service.fider.postgresqlPassword}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
value={service.fider.postgresqlDatabase}
readonly
disabled
/>
</div>

View File

@ -12,6 +12,7 @@
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import Fider from './_Fider.svelte';
import Ghost from './_Ghost.svelte'; import Ghost from './_Ghost.svelte';
import Hasura from './_Hasura.svelte'; import Hasura from './_Hasura.svelte';
import MeiliSearch from './_MeiliSearch.svelte'; import MeiliSearch from './_MeiliSearch.svelte';
@ -175,6 +176,8 @@
<Umami bind:service /> <Umami bind:service />
{:else if service.type === 'hasura'} {:else if service.type === 'hasura'}
<Hasura bind:service /> <Hasura bind:service />
{:else if service.type === 'fider'}
<Fider bind:service {readOnly} />
{/if} {/if}
</div> </div>
</form> </form>

View File

@ -45,6 +45,7 @@
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte'; import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
import Umami from '$lib/components/svg/services/Umami.svelte'; import Umami from '$lib/components/svg/services/Umami.svelte';
import Hasura from '$lib/components/svg/services/Hasura.svelte'; import Hasura from '$lib/components/svg/services/Hasura.svelte';
import Fider from '$lib/components/svg/services/Fider.svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@ -96,6 +97,8 @@
<Umami isAbsolute /> <Umami isAbsolute />
{:else if type.name === 'hasura'} {:else if type.name === 'hasura'}
<Hasura isAbsolute /> <Hasura isAbsolute />
{:else if type.name === 'fider'}
<Fider isAbsolute />
{/if}{type.fancyName} {/if}{type.fancyName}
</button> </button>
</form> </form>

View File

@ -0,0 +1,57 @@
import { getUserDetails } from '$lib/common';
import { encrypt } from '$lib/crypto';
import * as db from '$lib/database';
import { ErrorHandler } 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,
fider: {
emailNoreply,
emailMailgunApiKey,
emailMailgunDomain,
emailMailgunRegion,
emailSmtpHost,
emailSmtpPort,
emailSmtpUser,
emailSmtpPassword,
emailSmtpEnableStartTls
}
} = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
if (emailNoreply) emailNoreply = emailNoreply.toLowerCase();
if (emailSmtpHost) emailSmtpHost = emailSmtpHost.toLowerCase();
if (emailSmtpPassword) {
emailSmtpPassword = encrypt(emailSmtpPassword);
}
if (emailSmtpPort) emailSmtpPort = Number(emailSmtpPort);
if (emailSmtpEnableStartTls) emailSmtpEnableStartTls = Boolean(emailSmtpEnableStartTls);
try {
await db.updateFiderService({
id,
fqdn,
name,
emailNoreply,
emailMailgunApiKey,
emailMailgunDomain,
emailMailgunRegion,
emailSmtpHost,
emailSmtpPort,
emailSmtpUser,
emailSmtpPassword,
emailSmtpEnableStartTls
});
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,147 @@
import {
asyncExecShell,
createDirectories,
getDomain,
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 { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
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: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } =
await db.getService({ id, teamId });
const {
type,
version,
fqdn,
destinationDockerId,
destinationDocker,
serviceSecret,
fider: {
postgresqlUser,
postgresqlPassword,
postgresqlDatabase,
jwtSecret,
emailNoreply,
emailMailgunApiKey,
emailMailgunDomain,
emailSmtpHost,
emailSmtpPort,
emailSmtpUser,
emailSmtpPassword,
emailSmtpEnableStartTls
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const domain = getDomain(fqdn);
const config = {
fider: {
image: `${image}:${version}`,
environmentVariables: {
HOST_DOMAIN: domain,
DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`,
JWT_SECRET: `${jwtSecret.replace(/\$/g, '$$$')}`,
EMAIL_NOREPLY: emailNoreply,
EMAIL_MAILGUN_API: emailMailgunApiKey || null,
EMAIL_MAILGUN_DOMAIN: emailMailgunDomain || null
}
},
postgresql: {
image: 'postgres:12-alpine',
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
environmentVariables: {
POSTGRES_USER: postgresqlUser,
POSTGRES_PASSWORD: postgresqlPassword,
POSTGRES_DB: postgresqlDatabase
}
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.fider.environmentVariables[secret.name] = secret.value;
});
}
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.fider.image,
environment: config.fider.environmentVariables,
networks: [network],
volumes: [],
restart: 'always',
labels: makeLabelForServices('fider'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
},
depends_on: [`${id}-postgresql`]
},
[`${id}-postgresql`]: {
image: config.postgresql.image,
container_name: `${id}-postgresql`,
environment: config.postgresql.environmentVariables,
networks: [network],
volumes: [config.postgresql.volume],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.postgresql.volume.split(':')[0]]: {
name: config.postgresql.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
console.log(error);
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,42 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer, 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 } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-postgresql`);
if (found) {
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -17,6 +17,7 @@
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import Umami from '$lib/components/svg/services/Umami.svelte'; import Umami from '$lib/components/svg/services/Umami.svelte';
import Hasura from '$lib/components/svg/services/Hasura.svelte'; import Hasura from '$lib/components/svg/services/Hasura.svelte';
import Fider from '$lib/components/svg/services/Fider.svelte';
export let services; export let services;
async function newService() { async function newService() {
@ -92,6 +93,8 @@
<Umami isAbsolute /> <Umami isAbsolute />
{:else if service.type === 'hasura'} {:else if service.type === 'hasura'}
<Hasura isAbsolute /> <Hasura isAbsolute />
{:else if service.type === 'fider'}
<Fider isAbsolute />
{/if} {/if}
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
{service.name} {service.name}
@ -143,6 +146,8 @@
<Umami isAbsolute /> <Umami isAbsolute />
{:else if service.type === 'hasura'} {:else if service.type === 'hasura'}
<Hasura isAbsolute /> <Hasura isAbsolute />
{:else if service.type === 'fider'}
<Fider isAbsolute />
{/if} {/if}
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
{service.name} {service.name}