diff --git a/apps/api/src/lib.ts b/apps/api/src/lib.ts index 5e8461489..4ad53f14d 100644 --- a/apps/api/src/lib.ts +++ b/apps/api/src/lib.ts @@ -7,6 +7,9 @@ export async function migrateServicesToNewTemplate() { try { const services = await prisma.service.findMany({ include: includeServices }) for (const service of services) { + if (!service.type) { + continue; + } if (service.type === 'plausibleanalytics' && service.plausibleAnalytics) await plausibleAnalytics(service) if (service.type === 'fider' && service.fider) await fider(service) if (service.type === 'minio' && service.minio) await minio(service) @@ -16,7 +19,9 @@ export async function migrateServicesToNewTemplate() { if (service.type === 'meilisearch' && service.meiliSearch) await meilisearch(service) if (service.type === 'umami' && service.umami) await umami(service) if (service.type === 'hasura' && service.hasura) await hasura(service) - if (service.type === 'glitchtip' && service.glitchTip) await glitchtip(service) + if (service.type === 'glitchTip' && service.glitchTip) await glitchtip(service) + if (service.type === 'searxng' && service.searxng) await searxng(service) + await createVolumes(service); } } catch (error) { @@ -24,6 +29,23 @@ export async function migrateServicesToNewTemplate() { } } +async function searxng(service: any) { + const { secretKey, redisPassword } = service.searxng + + const secrets = [ + `SECRET_KEY@@@${secretKey}`, + `REDIS_PASSWORD@@@${redisPassword}`, + ] + + const settings = [ + `SEARXNG_BASE_URL@@@$$generate_fqdn` + ] + await migrateSecrets(secrets, service); + await migrateSettings(settings, service); + + // Remove old service data + // await prisma.service.update({ where: { id: service.id }, data: { wordpress: { delete: true } } }) +} async function glitchtip(service: any) { const { postgresqlUser, postgresqlPassword, postgresqlDatabase, secretKeyBase, defaultEmail, defaultUsername, defaultPassword, defaultEmailFrom, emailSmtpHost, emailSmtpPort, emailSmtpUser, emailSmtpPassword, emailSmtpUseTls, emailSmtpUseSsl, emailBackend, mailgunApiKey, sendgridApiKey, enableOpenUserRegistration } = service.glitchTip @@ -228,7 +250,7 @@ async function fider(service: any) { } async function plausibleAnalytics(service: any) { - const { email = 'admin@example.com', username = 'admin', password, postgresqlUser, postgresqlPassword, postgresqlDatabase, secretKeyBase, scriptName } = service.plausibleAnalytics; + const { email, username, password, postgresqlUser, postgresqlPassword, postgresqlDatabase, secretKeyBase, scriptName } = service.plausibleAnalytics; const settings = [ `BASE_URL@@@$$generate_fqdn`, @@ -248,7 +270,6 @@ async function plausibleAnalytics(service: any) { ] await migrateSettings(settings, service); await migrateSecrets(secrets, service); - await createVolumes(service); // Remove old service data // await prisma.service.update({ where: { id: service.id }, data: { plausibleAnalytics: { delete: true } } }) @@ -257,7 +278,10 @@ async function plausibleAnalytics(service: any) { async function migrateSettings(settings: any[], service: any) { for (const setting of settings) { if (!setting) continue; - const [name, value] = setting.split('@@@') + let [name, value] = setting.split('@@@') + if (!value || value === 'null') { + continue; + } // console.log('Migrating setting', name, value, 'for service', service.id, ', service name:', service.name) await prisma.serviceSetting.findFirst({ where: { name, serviceId: service.id } }) || await prisma.serviceSetting.create({ data: { name, value, service: { connect: { id: service.id } } } }) } @@ -265,14 +289,17 @@ async function migrateSettings(settings: any[], service: any) { async function migrateSecrets(secrets: any[], service: any) { for (const secret of secrets) { if (!secret) continue; - const [name, value] = secret.split('@@@') + let [name, value] = secret.split('@@@') + if (!value || value === 'null') { + continue + } // console.log('Migrating secret', name, value, 'for service', service.id, ', service name:', service.name) await prisma.serviceSecret.findFirst({ where: { name, serviceId: service.id } }) || await prisma.serviceSecret.create({ data: { name, value, service: { connect: { id: service.id } } } }) } } async function createVolumes(service: any) { const volumes = []; - let template = templates.find(t => t.name === service.type) + let template = templates.find(t => t.name === service.type.toLowerCase()); if (template) { template = JSON.parse(JSON.stringify(template).replaceAll('$$id', service.id)) for (const s of Object.keys(template.services)) { diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index a52c71e89..1cfac5862 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -698,9 +698,15 @@ export async function startService(request: FastifyRequest) { const { workdir } = await createDirectories({ repository: type, buildId: id }); const template: any = await parseAndFindServiceTemplates(service, workdir, true) const network = destinationDockerId && destinationDocker.network; - const config = {}; for (const service in template.services) { + let newEnviroments = [] + for (const environment of template.services[service].environment) { + const [env, value] = environment.split("="); + if (!value.startsWith('$$secret') && value !== '') { + newEnviroments.push(`${env}=${value}`) + } + } config[service] = { container_name: service, build: template.services[service].build || undefined, @@ -709,9 +715,11 @@ export async function startService(request: FastifyRequest) { expose: template.services[service].ports, // ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), volumes: template.services[service].volumes, - environment: template.services[service].environment, + environment: newEnviroments, depends_on: template.services[service].depends_on, ulimits: template.services[service].ulimits, + cap_drop: template.services[service].cap_drop, + cap_add: template.services[service].cap_add, labels: makeLabelForServices(type), ...defaultComposeConfiguration(network), } diff --git a/apps/api/src/lib/templates.ts b/apps/api/src/lib/templates.ts index 0ed33d45f..7ae3021b7 100644 --- a/apps/api/src/lib/templates.ts +++ b/apps/api/src/lib/templates.ts @@ -1,4 +1,87 @@ export default [ + { + "templateVersion": "1.0.0", + "serviceDefaultVersion": "2022.10.14-1a5b0965", + "name": "searxng", + "displayName": "SearXNG", + "description": "", + "services": { + "$$id": { + "name": "SearXNG", + "depends_on": [ + "$$id-redis" + ], + "image": "searxng/searxng:$$core_version", + "volumes": [ + "$$id-postgresql-searxng:/etc/searxng", + ], + "environment": [ + "SEARXNG_BASE_URL=$$config_searxng_base_url", + "SECRET_KEY=$$secret_secret_key", + ], + "ports": [ + "8080" + ], + "extras": { + "files": [ + { + source: "$$workdir/schema.postgresql.sql", + destination: "/docker-entrypoint-initdb.d/schema.postgresql.sql", + content: ` + # see https://docs.searxng.org/admin/engines/settings.html#use-default-settings + use_default_settings: true + server: + secret_key: $$secret_secret_key + limiter: true + image_proxy: true + ui: + static_use_hash: true + redis: + url: redis://:$$secret_redis_password@$$id-redis:6379/0` + } + ] + } + }, + "$$id-redis": { + "name": "Redis", + "command": `redis-server --requirepass $$secret_redis_password --save "" --appendonly "no"`, + "depends_on": [], + "image": "redis:7-alpine", + "volumes": [ + "$$id-redis-data:/data", + ], + "environment": [ + "REDIS_PASSWORD=$$secret_redis_password", + ], + "ports": [], + "cap_drop": ['ALL'], + "cap_add": ['SETGID', 'SETUID', 'DAC_OVERRIDE'], + } + }, + "variables": [ + { + "id": "$$config_searxng_base_url", + "name": "SEARXNG_BASE_URL", + "label": "SearXNG Base URL", + "defaultValue": "$$generate_fqdn", + "description": "", + }, + { + "id": "$$secret_secret_key", + "name": "SECRET_KEY", + "label": "Secret Key", + "defaultValue": "$$generate_passphrase", + "description": "", + }, + { + "id": "$$secret_redis_password", + "name": "REDIS_PASSWORD", + "label": "Redis Password", + "defaultValue": "$$generate_password", + "description": "", + } + ] + }, { "templateVersion": "1.0.0", "serviceDefaultVersion": "v2.0.6", @@ -9,7 +92,8 @@ export default [ "$$id": { "name": "GlitchTip", "depends_on": [ - "$$id-postgresql" + "$$id-postgresql", + "$$id-redis" ], "image": "glitchtip/glitchtip:$$core_version", "volumes": [], @@ -67,7 +151,7 @@ export default [ { "id": "$$config_glitchtip_domain", "name": "GLITCHTIP_DOMAIN", - "label": "GLITCHTIP_DOMAIN URL", + "label": "GlitchTip Domain", "defaultValue": "$$generate_fqdn", "description": "", }, diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 2e774a9b7..fc20e545f 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -6,13 +6,13 @@ import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage import { day } from '../../../../lib/dayjs'; import { checkContainer, isContainerExited } from '../../../../lib/docker'; import cuid from 'cuid'; -import templates from '../../../../lib/templates'; import type { OnlyId } from '../../../../types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types'; import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions'; import { configureServiceType, removeService } from '../../../../lib/services/common'; import { hashPassword } from '../handlers'; +import templates from '../../../../lib/templates'; export async function listServices(request: FastifyRequest) { try { @@ -113,7 +113,7 @@ export async function getServiceStatus(request: FastifyRequest) { } } export async function parseAndFindServiceTemplates(service: any, workdir?: string, isDeploy: boolean = false) { - const foundTemplate = templates.find(t => t.name === service.type) + const foundTemplate = templates.find(t => t.name === service.type.toLowerCase()) let parsedTemplate = {} if (foundTemplate) { if (!isDeploy) { @@ -155,12 +155,13 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin if (service.serviceSetting.length > 0) { for (const setting of service.serviceSetting) { const { name, value } = setting + const regex = new RegExp(`\\$\\$config_${name}\\"`, 'gi') if (service.fqdn && value === '$$generate_fqdn') { - parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(`$$config_${name.toLowerCase()}`, service.fqdn)) + parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, service.fqdn + "\"")) } else if (service.fqdn && value === '$$generate_domain') { - parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(`$$config_${name.toLowerCase()}`, getDomain(service.fqdn))) + parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, getDomain(service.fqdn) + "\"")) } else { - parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(`$$config_${name.toLowerCase()}`, value)) + parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regex, value + "\"")) } } @@ -170,7 +171,9 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin if (service.serviceSecret.length > 0) { for (const secret of service.serviceSecret) { const { name, value } = secret - parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(`$$hashed$$secret_${name.toLowerCase()}`, bcrypt.hashSync(value, 10)).replaceAll(`$$secret_${name.toLowerCase()}`, value)) + const regex = new RegExp(`\\$\\$secret_${name}\\"`, 'gi') + const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\\"`, 'gi') + parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(regexHashed, bcrypt.hashSync(value, 10)).replaceAll(regex, value)) } } } @@ -185,13 +188,17 @@ export async function getService(request: FastifyRequest) { if (!service) { throw { status: 404, message: 'Service not found.' } } - const template = await parseAndFindServiceTemplates(service) + let template = {} + if (service.type) { + template = await parseAndFindServiceTemplates(service) + } return { settings: await listSettings(), service, template, } } catch ({ status, message }) { + console.log(status, message) return errorHandler({ status, message }) } } @@ -218,19 +225,22 @@ export async function saveServiceType(request: FastifyRequest, if (foundTemplate.variables.length > 0) { foundTemplate.variables = foundTemplate.variables.map(variable => { let { id: variableId } = variable; + console.log(variableId) if (variableId.startsWith('$$secret_')) { const length = variable?.extras && variable.extras['length'] if (variable.defaultValue === '$$generate_password') { variable.value = generatePassword({ length }); } else if (variable.defaultValue === '$$generate_passphrase') { variable.value = generatePassword({ length }); + } else if (!variable.defaultValue) { + variable.defaultValue = undefined } } if (variableId.startsWith('$$config_')) { if (variable.defaultValue === '$$generate_username') { variable.value = cuid(); } else { - variable.value = variable.defaultValue + variable.value = variable.defaultValue || '' } } if (variable.value) { @@ -246,19 +256,28 @@ export async function saveServiceType(request: FastifyRequest, variable.value = variable.defaultValue for (const generatedVariable of generatedVariables) { let [id, value] = generatedVariable.split('=') - variable.value = variable.value.replaceAll(id, value) + if (variable.value) { + variable.value = variable.value.replaceAll(id, value) + } } } return variable }) } + for (const variable of foundTemplate.variables) { if (variable.id.startsWith('$$secret_')) { + if (!variable.value) { + continue; + } await prisma.serviceSecret.create({ data: { name: variable.name, value: encrypt(variable.value), service: { connect: { id } } } }) } if (variable.id.startsWith('$$config_')) { + if (!variable.value) { + variable.value = ''; + } await prisma.serviceSetting.create({ data: { name: variable.name, value: variable.value, service: { connect: { id } } } })