diff --git a/prisma/migrations/20220427133656_hasura/migration.sql b/prisma/migrations/20220427133656_hasura/migration.sql new file mode 100644 index 000000000..c679ad0fb --- /dev/null +++ b/prisma/migrations/20220427133656_hasura/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "Hasura" ( + "id" TEXT NOT NULL PRIMARY KEY, + "serviceId" TEXT NOT NULL, + "postgresqlUser" TEXT NOT NULL, + "postgresqlPassword" TEXT NOT NULL, + "postgresqlDatabase" TEXT NOT NULL, + "postgresqlPublicPort" INTEGER, + "graphQLAdminPassword" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Hasura_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Hasura_serviceId_key" ON "Hasura"("serviceId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 267dd1991..1c10822f7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -305,6 +305,7 @@ model Service { meiliSearch MeiliSearch? persistentStorage ServicePersistentStorage[] umami Umami? + hasura Hasura? } model PlausibleAnalytics { @@ -403,3 +404,16 @@ model Umami { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model Hasura { + id String @id @default(cuid()) + serviceId String @unique + postgresqlUser String + postgresqlPassword String + postgresqlDatabase String + postgresqlPublicPort Int? + graphQLAdminPassword String + service Service @relation(fields: [serviceId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/lib/components/ServiceLinks.svelte b/src/lib/components/ServiceLinks.svelte index 980505119..6097abbd2 100644 --- a/src/lib/components/ServiceLinks.svelte +++ b/src/lib/components/ServiceLinks.svelte @@ -1,6 +1,7 @@ + + + + + + + + + + + + diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index 4cb2e8b22..9e3d1b467 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -14,7 +14,8 @@ const include: Prisma.ServiceInclude = { wordpress: true, ghost: true, meiliSearch: true, - umami: true + umami: true, + hasura: true }; export async function listServicesWithIncludes() { return await prisma.service.findMany({ @@ -97,6 +98,11 @@ export async function getService({ id, teamId }: { id: string; teamId: string }) body.umami.umamiAdminPassword = decrypt(body.umami.umamiAdminPassword); if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt); + if (body.hasura?.postgresqlPassword) + body.hasura.postgresqlPassword = decrypt(body.hasura.postgresqlPassword); + if (body.hasura?.graphQLAdminPassword) + body.hasura.graphQLAdminPassword = decrypt(body.hasura.graphQLAdminPassword); + const settings = await prisma.setting.findFirst(); return { ...body, settings }; @@ -243,6 +249,25 @@ export async function configureServiceType({ } } }); + } else if (type === 'hasura') { + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword()); + const postgresqlDatabase = 'hasura'; + const graphQLAdminPassword = encrypt(generatePassword()); + await prisma.service.update({ + where: { id }, + data: { + type, + hasura: { + create: { + postgresqlDatabase, + postgresqlPassword, + postgresqlUser, + graphQLAdminPassword + } + } + } + }); } } @@ -400,6 +425,7 @@ export async function removeService({ id }: { id: string }): Promise { await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } }); await prisma.umami.deleteMany({ where: { serviceId: id } }); + await prisma.hasura.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); diff --git a/src/routes/services/[id]/_Services/_Hasura.svelte b/src/routes/services/[id]/_Services/_Hasura.svelte new file mode 100644 index 000000000..5930e2ded --- /dev/null +++ b/src/routes/services/[id]/_Services/_Hasura.svelte @@ -0,0 +1,58 @@ + + +
+
Hasura
+
+ +
+ + +
+ +
+
PostgreSQL
+
+ +
+ + +
+
+ + +
+
+ + +
diff --git a/src/routes/services/[id]/_Services/_Services.svelte b/src/routes/services/[id]/_Services/_Services.svelte index 85c1c5abf..21db6a598 100644 --- a/src/routes/services/[id]/_Services/_Services.svelte +++ b/src/routes/services/[id]/_Services/_Services.svelte @@ -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 @@ {:else if service.type === 'umami'} + {:else if service.type === 'hasura'} + {/if} diff --git a/src/routes/services/[id]/configuration/type.svelte b/src/routes/services/[id]/configuration/type.svelte index 6e4b3628f..788a53c9d 100644 --- a/src/routes/services/[id]/configuration/type.svelte +++ b/src/routes/services/[id]/configuration/type.svelte @@ -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 @@ {:else if type.name === 'umami'} + {:else if type.name === 'hasura'} + {/if}{type.fancyName} diff --git a/src/routes/services/[id]/hasura/index.json.ts b/src/routes/services/[id]/hasura/index.json.ts new file mode 100644 index 000000000..d717502c5 --- /dev/null +++ b/src/routes/services/[id]/hasura/index.json.ts @@ -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); + } +}; diff --git a/src/routes/services/[id]/hasura/start.json.ts b/src/routes/services/[id]/hasura/start.json.ts new file mode 100644 index 000000000..325d6e33c --- /dev/null +++ b/src/routes/services/[id]/hasura/start.json.ts @@ -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); + } +}; diff --git a/src/routes/services/[id]/hasura/stop.json.ts b/src/routes/services/[id]/hasura/stop.json.ts new file mode 100644 index 000000000..67dd96d04 --- /dev/null +++ b/src/routes/services/[id]/hasura/stop.json.ts @@ -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); + } +}; diff --git a/src/routes/services/index.svelte b/src/routes/services/index.svelte index fd784d049..587037c86 100644 --- a/src/routes/services/index.svelte +++ b/src/routes/services/index.svelte @@ -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 @@ {:else if service.type === 'umami'} + {:else if service.type === 'hasura'} + {/if}
{service.name} @@ -138,6 +141,8 @@ {:else if service.type === 'umami'} + {:else if service.type === 'hasura'} + {/if}
{service.name}