diff --git a/prisma/migrations/20220418214843_persistent_storage_services/migration.sql b/prisma/migrations/20220418214843_persistent_storage_services/migration.sql new file mode 100644 index 000000000..f85fd31df --- /dev/null +++ b/prisma/migrations/20220418214843_persistent_storage_services/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "ServicePersistentStorage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "serviceId" TEXT NOT NULL, + "path" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ServicePersistentStorage_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "ServicePersistentStorage_serviceId_path_key" ON "ServicePersistentStorage"("serviceId", "path"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 928562abd..712958add 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,6 +1,6 @@ generator client { - provider = "prisma-client-js" - binaryTargets = ["linux-musl"] + provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl"] } datasource db { @@ -118,14 +118,25 @@ model ApplicationSettings { model ApplicationPersistentStorage { id String @id @default(cuid()) application Application @relation(fields: [applicationId], references: [id]) - applicationId String - path String + applicationId String + path String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([applicationId, path]) } +model ServicePersistentStorage { + id String @id @default(cuid()) + service Service @relation(fields: [serviceId], references: [id]) + serviceId String + path String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([serviceId, path]) +} + model Secret { id String @id @default(cuid()) name String @@ -267,17 +278,17 @@ model DatabaseSettings { } model Service { - id String @id @default(cuid()) + id String @id @default(cuid()) name String fqdn String? - dualCerts Boolean @default(false) + dualCerts Boolean @default(false) type String? version String? teams Team[] destinationDockerId String? - destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt plausibleAnalytics PlausibleAnalytics? minio Minio? vscodeserver Vscodeserver? @@ -285,6 +296,7 @@ model Service { ghost Ghost? serviceSecret ServiceSecret[] meiliSearch MeiliSearch? + persistentStorage ServicePersistentStorage[] } model PlausibleAnalytics { diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index 7beaaf76a..314cf4548 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -27,25 +27,35 @@ export async function newService({ export async function getService({ id, teamId }: { id: string; teamId: string }): Promise { let body; - const include = { - destinationDocker: true, - plausibleAnalytics: true, - minio: true, - vscodeserver: true, - wordpress: true, - ghost: true, - serviceSecret: true, - meiliSearch: true - }; if (teamId === '0') { body = await prisma.service.findFirst({ where: { id }, - include + include: { + destinationDocker: true, + plausibleAnalytics: true, + minio: true, + vscodeserver: true, + wordpress: true, + ghost: true, + serviceSecret: true, + meiliSearch: true, + persistentStorage: true + } }); } else { body = await prisma.service.findFirst({ where: { id, teams: { some: { id: teamId } } }, - include + include: { + destinationDocker: true, + plausibleAnalytics: true, + minio: true, + vscodeserver: true, + wordpress: true, + ghost: true, + serviceSecret: true, + meiliSearch: true, + persistentStorage: true + } }); } @@ -362,6 +372,7 @@ export async function updateGhostService({ } export async function removeService({ id }: { id: string }): Promise { + await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); diff --git a/src/routes/services/[id]/__layout.svelte b/src/routes/services/[id]/__layout.svelte index 37158d711..64b6c72c1 100644 --- a/src/routes/services/[id]/__layout.svelte +++ b/src/routes/services/[id]/__layout.svelte @@ -239,6 +239,35 @@ + +
{/if} +
+ {:else} +
+
+ +
+
+ +
+
+ {/if} + diff --git a/src/routes/services/[id]/storage/index.json.ts b/src/routes/services/[id]/storage/index.json.ts new file mode 100644 index 000000000..a2a240fe4 --- /dev/null +++ b/src/routes/services/[id]/storage/index.json.ts @@ -0,0 +1,65 @@ +import { getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const { status, body, teamId } = await getUserDetails(event, false); + if (status === 401) return { status, body }; + + const { id } = event.params; + try { + const persistentStorages = await db.prisma.servicePersistentStorage.findMany({ + where: { serviceId: id } + }); + return { + body: { + persistentStorages + } + }; + } catch (error) { + return ErrorHandler(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 { path, newStorage, storageId } = await event.request.json(); + try { + if (newStorage) { + await db.prisma.servicePersistentStorage.create({ + data: { path, service: { connect: { id } } } + }); + } else { + await db.prisma.servicePersistentStorage.update({ + where: { id: storageId }, + data: { path } + }); + } + return { + status: 201 + }; + } catch (error) { + return ErrorHandler(error); + } +}; + +export const del: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + const { path } = await event.request.json(); + + try { + await db.prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id, path } }); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/storage/index.svelte b/src/routes/services/[id]/storage/index.svelte new file mode 100644 index 000000000..3d5c43c1b --- /dev/null +++ b/src/routes/services/[id]/storage/index.svelte @@ -0,0 +1,102 @@ + + + + +
+
+
+ Persistent Storage +
+ {service.name} +
+ {#if service.fqdn} + + + + + + + {/if} + + +
+ +
+
+ This is useful for storing data for VSCode server or WordPress.'} + /> +
+ + + + + + + + {#each persistentStorages as storage} + {#key storage.id} + + + + {/key} + {/each} + + + + +
Path
+
diff --git a/src/routes/services/[id]/vscodeserver/start.json.ts b/src/routes/services/[id]/vscodeserver/start.json.ts index be65c362a..e928de7bb 100644 --- a/src/routes/services/[id]/vscodeserver/start.json.ts +++ b/src/routes/services/[id]/vscodeserver/start.json.ts @@ -21,6 +21,7 @@ export const post: RequestHandler = async (event) => { destinationDockerId, destinationDocker, serviceSecret, + persistentStorage, vscodeserver: { password } } = service; @@ -42,6 +43,28 @@ export const post: RequestHandler = async (event) => { config.environmentVariables[secret.name] = secret.value; }); } + + const volumes = + persistentStorage?.map((storage) => { + return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`; + }) || []; + + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + const volumeMounts = Object.assign( + {}, + { + [config.volume.split(':')[0]]: { + name: config.volume.split(':')[0] + } + }, + ...composeVolumes + ); const composeFile: ComposeFile = { version: '3.8', services: { @@ -50,7 +73,7 @@ export const post: RequestHandler = async (event) => { image: config.image, environment: config.environmentVariables, networks: [network], - volumes: [config.volume], + volumes: [config.volume, ...volumes], restart: 'always', labels: makeLabelForServices('vscodeServer'), deploy: { @@ -68,17 +91,21 @@ export const post: RequestHandler = async (event) => { external: true } }, - volumes: { - [config.volume.split(':')[0]]: { - name: config.volume.split(':')[0] - } - } + volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); + + const changePermissionOn = persistentStorage.map((p) => p.path); + + await asyncExecShell( + `DOCKER_HOST=${host} docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join( + ' ' + )}` + ); return { status: 200 };