diff --git a/prisma/migrations/20220304141408_service_secrets/migration.sql b/prisma/migrations/20220304141408_service_secrets/migration.sql new file mode 100644 index 000000000..baa0c3f54 --- /dev/null +++ b/prisma/migrations/20220304141408_service_secrets/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "ServiceSecret" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "value" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "serviceId" TEXT NOT NULL, + CONSTRAINT "ServiceSecret_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "ServiceSecret_name_serviceId_key" ON "ServiceSecret"("name", "serviceId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 688e312b7..8c4fdab53 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -122,6 +122,18 @@ model Secret { @@unique([name, applicationId, isPRMRSecret]) } +model ServiceSecret { + id String @id @default(cuid()) + name String + value String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + service Service @relation(fields: [serviceId], references: [id]) + serviceId String + + @@unique([name, serviceId]) +} + model BuildLog { id String @id @default(cuid()) applicationId String? @@ -252,6 +264,7 @@ model Service { minio Minio? vscodeserver Vscodeserver? wordpress Wordpress? + serviceSecret ServiceSecret[] } model PlausibleAnalytics { diff --git a/src/lib/database/checks.ts b/src/lib/database/checks.ts index d825de701..534d81966 100644 --- a/src/lib/database/checks.ts +++ b/src/lib/database/checks.ts @@ -15,6 +15,9 @@ export async function isDockerNetworkExists({ network }) { return await prisma.destinationDocker.findFirst({ where: { network } }); } +export async function isServiceSecretExists({ id, name }) { + return await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } }); +} export async function isSecretExists({ id, name, isPRMRSecret }) { return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } }); } diff --git a/src/lib/database/secrets.ts b/src/lib/database/secrets.ts index 167d061f5..835eb4def 100644 --- a/src/lib/database/secrets.ts +++ b/src/lib/database/secrets.ts @@ -1,6 +1,19 @@ import { encrypt, decrypt } from '$lib/crypto'; import { prisma } from './common'; +export async function listServiceSecrets(serviceId: string) { + let secrets = await prisma.serviceSecret.findMany({ + where: { serviceId }, + orderBy: { createdAt: 'desc' } + }); + secrets = secrets.map((secret) => { + secret.value = decrypt(secret.value); + return secret; + }); + + return secrets; +} + export async function listSecrets(applicationId: string) { let secrets = await prisma.secret.findMany({ where: { applicationId }, @@ -14,6 +27,12 @@ export async function listSecrets(applicationId: string) { return secrets; } +export async function createServiceSecret({ id, name, value }) { + value = encrypt(value); + return await prisma.serviceSecret.create({ + data: { name, value, service: { connect: { id } } } + }); +} export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecret }) { value = encrypt(value); return await prisma.secret.create({ @@ -21,10 +40,24 @@ export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecre }); } +export async function updateServiceSecret({ id, name, value }) { + value = encrypt(value); + const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } }); + + if (found) { + return await prisma.serviceSecret.updateMany({ + where: { serviceId: id, name }, + data: { value } + }); + } else { + return await prisma.serviceSecret.create({ + data: { name, value, service: { connect: { id } } } + }); + } +} export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecret }) { value = encrypt(value); const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } }); - console.log(found); if (found) { return await prisma.secret.updateMany({ @@ -38,6 +71,10 @@ export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecre } } +export async function removeServiceSecret({ id, name }) { + return await prisma.serviceSecret.deleteMany({ where: { serviceId: id, name } }); +} + export async function removeSecret({ id, name }) { return await prisma.secret.deleteMany({ where: { applicationId: id, name } }); } diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index 56606109b..ea0be0a83 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -19,7 +19,8 @@ export async function getService({ id, teamId }) { plausibleAnalytics: true, minio: true, vscodeserver: true, - wordpress: true + wordpress: true, + serviceSecret: true } }); @@ -42,6 +43,12 @@ export async function getService({ id, teamId }) { if (body.wordpress?.mysqlRootUserPassword) body.wordpress.mysqlRootUserPassword = decrypt(body.wordpress.mysqlRootUserPassword); + if (body?.serviceSecret.length > 0) { + body.serviceSecret = body.serviceSecret.map((s) => { + s.value = decrypt(s.value); + return s; + }); + } return { ...body }; } @@ -159,5 +166,7 @@ export async function removeService({ id }) { await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); await prisma.wordpress.deleteMany({ where: { serviceId: id } }); + await prisma.serviceSecret.deleteMany({ where: { serviceId: id } }); + await prisma.service.delete({ where: { id } }); } diff --git a/src/routes/services/[id]/_Services/_Wordpress.svelte b/src/routes/services/[id]/_Services/_Wordpress.svelte index aa61aeb55..883178a22 100644 --- a/src/routes/services/[id]/_Services/_Wordpress.svelte +++ b/src/routes/services/[id]/_Services/_Wordpress.svelte @@ -25,7 +25,7 @@ define('WP_ALLOW_MULTISITE', true); define('MULTISITE', true); define('SUBDOMAIN_INSTALL', false);` - : null}>{service.wordpress.extraConfig || 'N/A'}{service.wordpress.extraConfig}
diff --git a/src/routes/services/[id]/__layout.svelte b/src/routes/services/[id]/__layout.svelte index a096c14cd..fffce0007 100644 --- a/src/routes/services/[id]/__layout.svelte +++ b/src/routes/services/[id]/__layout.svelte @@ -57,13 +57,13 @@ + + + + + + + + + + {#if isNewSecret} +
+ +
+ {:else} +
+
+ +
+
+ +
+
+ {/if} + diff --git a/src/routes/services/[id]/secrets/index.json.ts b/src/routes/services/[id]/secrets/index.json.ts new file mode 100644 index 000000000..22c22ae29 --- /dev/null +++ b/src/routes/services/[id]/secrets/index.json.ts @@ -0,0 +1,70 @@ +import { getTeam, 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 { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + try { + const secrets = await db.listServiceSecrets(id); + return { + status: 200, + body: { + secrets: secrets.sort((a, b) => { + return ('' + a.name).localeCompare(b.name); + }) + } + }; + } 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 { name, value, isBuildSecret, isPRMRSecret, isNew } = await event.request.json(); + try { + if (isNew) { + const found = await db.isServiceSecretExists({ id, name }); + if (found) { + throw { + error: `Secret ${name} already exists.` + }; + } else { + await db.createServiceSecret({ id, name, value }); + return { + status: 201 + }; + } + } else { + await db.updateServiceSecret({ id, name, value }); + 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 { name } = await event.request.json(); + + try { + await db.removeServiceSecret({ id, name }); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/secrets/index.svelte b/src/routes/services/[id]/secrets/index.svelte new file mode 100644 index 000000000..d850fd37f --- /dev/null +++ b/src/routes/services/[id]/secrets/index.svelte @@ -0,0 +1,67 @@ + + + + +
+
+ Secrets {#if service.fqdn} + {getDomain(service.fqdn)} + {/if} +
+
+
+ + + + + + + + + + {#each secrets as secret} + {#key secret.id} + + + + {/key} + {/each} + + + + +
NameValueAction
+
diff --git a/src/routes/services/[id]/vaultwarden/start.json.ts b/src/routes/services/[id]/vaultwarden/start.json.ts index f42481200..703a71473 100644 --- a/src/routes/services/[id]/vaultwarden/start.json.ts +++ b/src/routes/services/[id]/vaultwarden/start.json.ts @@ -14,25 +14,31 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); const { workdir } = await createDirectories({ repository: type, buildId: id }); - const baseImage = getServiceImage(type); + const image = getServiceImage(type); const config = { - image: `${baseImage}:${version}`, - volume: `${id}-vaultwarden-data:/data/` + image: `${image}:${version}`, + volume: `${id}-vaultwarden-data:/data/`, + environmentVariables: {} }; - + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } const composeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.image, + environment: config.environmentVariables, networks: [network], volumes: [config.volume], restart: 'always', diff --git a/src/routes/services/[id]/vscodeserver/start.json.ts b/src/routes/services/[id]/vscodeserver/start.json.ts index 29bf3326a..be43cb2a7 100644 --- a/src/routes/services/[id]/vscodeserver/start.json.ts +++ b/src/routes/services/[id]/vscodeserver/start.json.ts @@ -3,7 +3,7 @@ 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 } from '$lib/database'; +import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; export const post: RequestHandler = async (event) => { @@ -19,6 +19,7 @@ export const post: RequestHandler = async (event) => { version, destinationDockerId, destinationDocker, + serviceSecret, vscodeserver: { password } } = service; @@ -26,13 +27,20 @@ export const post: RequestHandler = async (event) => { const host = getEngine(destinationDocker.engine); const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + const config = { - image: `codercom/code-server:${version}`, + image: `${image}:${version}`, volume: `${id}-vscodeserver-data:/home/coder`, environmentVariables: { PASSWORD: password } }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } const composeFile = { version: '3.8', services: { diff --git a/src/routes/services/[id]/wordpress/start.json.ts b/src/routes/services/[id]/wordpress/start.json.ts index 54080478e..d1685d8b1 100644 --- a/src/routes/services/[id]/wordpress/start.json.ts +++ b/src/routes/services/[id]/wordpress/start.json.ts @@ -3,7 +3,7 @@ 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 } from '$lib/database'; +import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; export const post: RequestHandler = async (event) => { @@ -19,6 +19,7 @@ export const post: RequestHandler = async (event) => { version, fqdn, destinationDockerId, + serviceSecret, destinationDocker, wordpress: { mysqlDatabase, @@ -32,11 +33,12 @@ export const post: RequestHandler = async (event) => { const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const image = getServiceImage(type); const { workdir } = await createDirectories({ repository: type, buildId: id }); const config = { wordpress: { - image: `wordpress:${version}`, + image: `${image}:${version}`, volume: `${id}-wordpress-data:/var/www/html`, environmentVariables: { WORDPRESS_DB_HOST: `${id}-mysql`, @@ -58,6 +60,11 @@ export const post: RequestHandler = async (event) => { } } }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.wordpress.environmentVariables[secret.name] = secret.value; + }); + } const composeFile = { version: '3.8', services: {