feat: Hasura as a service

This commit is contained in:
Andras Bacsai
2022-04-27 15:37:50 +02:00
parent d75d2880e5
commit 48fa4ff245
13 changed files with 353 additions and 1 deletions

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
export let service;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Hasura</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="graphQLAdminPassword">GraphQL Admin Password</label>
<CopyPasswordField
name="graphQLAdminPassword"
id="graphQLAdminPassword"
isPasswordField
value={service.hasura.graphQLAdminPassword}
readonly
disabled
/>
</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.hasura.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.hasura.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.hasura.postgresqlDatabase}
readonly
disabled
/>
</div>

View File

@@ -13,6 +13,7 @@
import { t } from '$lib/translations';
import { toast } from '@zerodevx/svelte-toast';
import Ghost from './_Ghost.svelte';
import Hasura from './_Hasura.svelte';
import MeiliSearch from './_MeiliSearch.svelte';
import MinIo from './_MinIO.svelte';
import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
@@ -172,6 +173,8 @@
<MeiliSearch bind:service />
{:else if service.type === 'umami'}
<Umami bind:service />
{:else if service.type === 'hasura'}
<Hasura bind:service />
{/if}
</div>
</form>

View File

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

View File

@@ -0,0 +1,21 @@
import { getUserDetails } from '$lib/common';
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 } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
try {
await db.updateService({ id, fqdn, name });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,122 @@
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 { 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,
destinationDockerId,
destinationDocker,
serviceSecret,
hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
hasura: {
image: `${image}:${version}`,
environmentVariables: {
HASURA_GRAPHQL_METADATA_DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`
}
},
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.hasura.environmentVariables[secret.name] = secret.value;
});
}
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.hasura.image,
environment: config.hasura.environmentVariables,
networks: [network],
volumes: [],
restart: 'always',
labels: makeLabelForServices('hasura'),
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

@@ -16,6 +16,7 @@
import { session } from '$app/stores';
import { getDomain } from '$lib/components/common';
import Umami from '$lib/components/svg/services/Umami.svelte';
import Hasura from '$lib/components/svg/services/Hasura.svelte';
export let services;
async function newService() {
@@ -89,6 +90,8 @@
<MeiliSearch isAbsolute />
{:else if service.type === 'umami'}
<Umami isAbsolute />
{:else if service.type === 'hasura'}
<Hasura isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold">
{service.name}
@@ -138,6 +141,8 @@
<MeiliSearch isAbsolute />
{:else if service.type === 'umami'}
<Umami isAbsolute />
{:else if service.type === 'hasura'}
<Hasura isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold">
{service.name}