tons of updates
This commit is contained in:
@@ -198,7 +198,7 @@ export const encrypt = (text: string) => {
|
||||
if (text) {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv);
|
||||
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
|
||||
const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]);
|
||||
return JSON.stringify({
|
||||
iv: iv.toString('hex'),
|
||||
content: encrypted.toString('hex')
|
||||
@@ -1681,7 +1681,9 @@ export function persistentVolumes(id, persistentStorage, config) {
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (value.volumes) {
|
||||
for (const volume of value.volumes) {
|
||||
volumeSet.add(volume);
|
||||
if (!volume.startsWith('/var/run/docker.sock')) {
|
||||
volumeSet.add(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,82 +6,83 @@ import { ServiceStartStop } from '../../routes/api/v1/services/types';
|
||||
import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common';
|
||||
import { defaultServiceConfigurations } from '../services';
|
||||
import { OnlyId } from '../../types';
|
||||
import templates from '../templates'
|
||||
|
||||
export async function startService(request: FastifyRequest<ServiceStartStop>) {
|
||||
try {
|
||||
const { type } = request.params
|
||||
if (type === 'plausibleanalytics') {
|
||||
return await startPlausibleAnalyticsService(request)
|
||||
}
|
||||
if (type === 'nocodb') {
|
||||
return await startNocodbService(request)
|
||||
}
|
||||
if (type === 'minio') {
|
||||
return await startMinioService(request)
|
||||
}
|
||||
if (type === 'vscodeserver') {
|
||||
return await startVscodeService(request)
|
||||
}
|
||||
if (type === 'wordpress') {
|
||||
return await startWordpressService(request)
|
||||
}
|
||||
if (type === 'vaultwarden') {
|
||||
return await startVaultwardenService(request)
|
||||
}
|
||||
if (type === 'languagetool') {
|
||||
return await startLanguageToolService(request)
|
||||
}
|
||||
if (type === 'n8n') {
|
||||
return await startN8nService(request)
|
||||
}
|
||||
if (type === 'uptimekuma') {
|
||||
return await startUptimekumaService(request)
|
||||
}
|
||||
if (type === 'ghost') {
|
||||
return await startGhostService(request)
|
||||
}
|
||||
if (type === 'meilisearch') {
|
||||
return await startMeilisearchService(request)
|
||||
}
|
||||
if (type === 'umami') {
|
||||
return await startUmamiService(request)
|
||||
}
|
||||
if (type === 'hasura') {
|
||||
return await startHasuraService(request)
|
||||
}
|
||||
if (type === 'fider') {
|
||||
return await startFiderService(request)
|
||||
}
|
||||
if (type === 'moodle') {
|
||||
return await startMoodleService(request)
|
||||
}
|
||||
if (type === 'appwrite') {
|
||||
return await startAppWriteService(request)
|
||||
}
|
||||
if (type === 'glitchTip') {
|
||||
return await startGlitchTipService(request)
|
||||
}
|
||||
if (type === 'searxng') {
|
||||
return await startSearXNGService(request)
|
||||
}
|
||||
if (type === 'weblate') {
|
||||
return await startWeblateService(request)
|
||||
}
|
||||
if (type === 'taiga') {
|
||||
return await startTaigaService(request)
|
||||
}
|
||||
if (type === 'grafana') {
|
||||
return await startGrafanaService(request)
|
||||
}
|
||||
if (type === 'trilium') {
|
||||
return await startTriliumService(request)
|
||||
}
|
||||
// export async function startService(request: FastifyRequest<ServiceStartStop>) {
|
||||
// try {
|
||||
// const { type } = request.params
|
||||
// if (type === 'plausibleanalytics') {
|
||||
// return await startPlausibleAnalyticsService(request)
|
||||
// }
|
||||
// if (type === 'nocodb') {
|
||||
// return await startNocodbService(request)
|
||||
// }
|
||||
// if (type === 'minio') {
|
||||
// return await startMinioService(request)
|
||||
// }
|
||||
// if (type === 'vscodeserver') {
|
||||
// return await startVscodeService(request)
|
||||
// }
|
||||
// if (type === 'wordpress') {
|
||||
// return await startWordpressService(request)
|
||||
// }
|
||||
// if (type === 'vaultwarden') {
|
||||
// return await startVaultwardenService(request)
|
||||
// }
|
||||
// if (type === 'languagetool') {
|
||||
// return await startLanguageToolService(request)
|
||||
// }
|
||||
// if (type === 'n8n') {
|
||||
// return await startN8nService(request)
|
||||
// }
|
||||
// if (type === 'uptimekuma') {
|
||||
// return await startUptimekumaService(request)
|
||||
// }
|
||||
// if (type === 'ghost') {
|
||||
// return await startGhostService(request)
|
||||
// }
|
||||
// if (type === 'meilisearch') {
|
||||
// return await startMeilisearchService(request)
|
||||
// }
|
||||
// if (type === 'umami') {
|
||||
// return await startUmamiService(request)
|
||||
// }
|
||||
// if (type === 'hasura') {
|
||||
// return await startHasuraService(request)
|
||||
// }
|
||||
// if (type === 'fider') {
|
||||
// return await startFiderService(request)
|
||||
// }
|
||||
// if (type === 'moodle') {
|
||||
// return await startMoodleService(request)
|
||||
// }
|
||||
// if (type === 'appwrite') {
|
||||
// return await startAppWriteService(request)
|
||||
// }
|
||||
// if (type === 'glitchTip') {
|
||||
// return await startGlitchTipService(request)
|
||||
// }
|
||||
// if (type === 'searxng') {
|
||||
// return await startSearXNGService(request)
|
||||
// }
|
||||
// if (type === 'weblate') {
|
||||
// return await startWeblateService(request)
|
||||
// }
|
||||
// if (type === 'taiga') {
|
||||
// return await startTaigaService(request)
|
||||
// }
|
||||
// if (type === 'grafana') {
|
||||
// return await startGrafanaService(request)
|
||||
// }
|
||||
// if (type === 'trilium') {
|
||||
// return await startTriliumService(request)
|
||||
// }
|
||||
|
||||
throw `Service type ${type} not supported.`
|
||||
} catch (error) {
|
||||
throw { status: 500, message: error?.message || error }
|
||||
}
|
||||
}
|
||||
// throw `Service type ${type} not supported.`
|
||||
// } catch (error) {
|
||||
// throw { status: 500, message: error?.message || error }
|
||||
// }
|
||||
// }
|
||||
export async function stopService(request: FastifyRequest<ServiceStartStop>) {
|
||||
try {
|
||||
return await stopServiceContainers(request)
|
||||
@@ -684,54 +685,54 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
|
||||
}
|
||||
}
|
||||
|
||||
async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
|
||||
export async function startService(request: FastifyRequest<ServiceStartStop>) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
|
||||
service;
|
||||
|
||||
let template = templates.find((template) => template.name === type);
|
||||
|
||||
template = JSON.parse(JSON.stringify(template).replaceAll('$$id', id).replaceAll('$$fqdn', service.fqdn))
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const port = getServiceMainPort('n8n');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const image = getServiceImage(type);
|
||||
|
||||
const config = {
|
||||
n8n: {
|
||||
image: `${image}:${version}`,
|
||||
volumes: [`${id}-n8n:/root/.n8n`],
|
||||
environmentVariables: {
|
||||
WEBHOOK_URL: `${service.fqdn}`
|
||||
}
|
||||
const config = {};
|
||||
for (const service in template.services) {
|
||||
config[service] = {
|
||||
container_name: id,
|
||||
image: template.services[service].image.replace('$$core_version', version),
|
||||
expose: template.services[service].ports,
|
||||
// ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||
volumes: template.services[service].volumes,
|
||||
environment: {},
|
||||
depends_on: template.services[service].depends_on,
|
||||
ulimits: template.services[service].ulimits,
|
||||
labels: makeLabelForServices(type),
|
||||
...defaultComposeConfiguration(network),
|
||||
}
|
||||
if (serviceSecret.length > 0) {
|
||||
serviceSecret.forEach((secret) => {
|
||||
config[service].environment[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
};
|
||||
if (serviceSecret.length > 0) {
|
||||
serviceSecret.forEach((secret) => {
|
||||
config.n8n.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
|
||||
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: config.n8n.image,
|
||||
volumes: config.n8n.volumes,
|
||||
environment: config.n8n.environmentVariables,
|
||||
labels: makeLabelForServices('n8n'),
|
||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||
...defaultComposeConfiguration(network),
|
||||
}
|
||||
},
|
||||
services: config,
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: volumeMounts
|
||||
};
|
||||
}
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
await startServiceContainers(destinationDocker.id, composeFileDestination)
|
||||
|
175
apps/api/src/lib/templates.ts
Normal file
175
apps/api/src/lib/templates.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
export default [
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "0.198.1",
|
||||
"name": "n8n",
|
||||
"displayName": "n8n.io",
|
||||
"isOfficial": true,
|
||||
"description": "n8n is a free and open node based Workflow Automation Tool.",
|
||||
"services": {
|
||||
"$$id": {
|
||||
"documentation": "Taken from https://hub.docker.com/r/n8nio/n8n",
|
||||
"depends_on": [],
|
||||
"image": "n8nio/n8n:$$core_version",
|
||||
"volumes": [
|
||||
"$$id-data:/root/.n8n",
|
||||
"$$id-data-write:/files",
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
],
|
||||
"environment": [
|
||||
"WEBHOOK_URL=$$fqdn"
|
||||
],
|
||||
"ports": [
|
||||
"5678"
|
||||
]
|
||||
}
|
||||
},
|
||||
"variables": []
|
||||
},
|
||||
{
|
||||
"templateVersion": "1.0.0",
|
||||
"serviceDefaultVersion": "stable",
|
||||
"name": "plausibleanalytics",
|
||||
"displayName": "PlausibleAnalytics",
|
||||
"isOfficial": true,
|
||||
"description": "Plausible is a lightweight and open-source website analytics tool.",
|
||||
"services": {
|
||||
"$$id": {
|
||||
"documentation": "Taken from https://plausible.io/",
|
||||
"command": ['sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"'],
|
||||
"depends_on": [
|
||||
"$$id-postgresql",
|
||||
"$$id-clickhouse"
|
||||
],
|
||||
"image": "plausible/analytics:$$core_version",
|
||||
"environment": [
|
||||
"ADMIN_USER_EMAIL=$$secret_email",
|
||||
"ADMIN_USER_NAME=$$secret_name",
|
||||
"ADMIN_USER_PASSWORD=$$secret_password",
|
||||
"BASE_URL=$$fqdn",
|
||||
"SECRET_KEY_BASE=$$secret_key_base",
|
||||
"DISABLE_AUTH=$$secret_disable_auth",
|
||||
"DISABLE_REGISTRATION=$$secret_disable_registration",
|
||||
"DATABASE_URL=postgresql://$$secret_postgresql_username:$$secret_postgresql_password@$$id-postgresql:5432/$$secret_postgresql_database",
|
||||
"CLICKHOUSE_DATABASE_URL=http://$$id-clickhouse:8123/plausible",
|
||||
],
|
||||
"ports": [
|
||||
"8000"
|
||||
],
|
||||
},
|
||||
"$$id-postgresql": {
|
||||
"documentation": "Taken from https://plausible.io/",
|
||||
"image": "bitnami/postgresql:13.2.0",
|
||||
"environment": [
|
||||
"POSTGRESQL_PASSWORD=$$secret_postgresql_password",
|
||||
"POSTGRESQL_USERNAME=$$secret_postgresql_username",
|
||||
"POSTGRESQL_DATABASE=$$secret_postgresql_database",
|
||||
],
|
||||
|
||||
},
|
||||
"$$id-clickhouse": {
|
||||
"documentation": "Taken from https://plausible.io/",
|
||||
"build": "$$workdir",
|
||||
"image": "yandex/clickhouse-server:21.3.2.5",
|
||||
"ulimits": {
|
||||
"nofile": {
|
||||
"soft": 262144,
|
||||
"hard": 262144
|
||||
}
|
||||
},
|
||||
"extras": {
|
||||
"files:": [
|
||||
{
|
||||
location: '$$workdir/clickhouse-config.xml',
|
||||
content: '<yandex><logger><level>warning</level><console>true</console></logger><query_thread_log remove="remove"/><query_log remove="remove"/><text_log remove="remove"/><trace_log remove="remove"/><metric_log remove="remove"/><asynchronous_metric_log remove="remove"/><session_log remove="remove"/><part_log remove="remove"/></yandex>'
|
||||
},
|
||||
{
|
||||
location: '$$workdir/clickhouse-user-config.xml',
|
||||
content: '<yandex><profiles><default><log_queries>0</log_queries><log_query_threads>0</log_query_threads></default></profiles></yandex>'
|
||||
},
|
||||
{
|
||||
location: '$$workdir/init.query',
|
||||
content: 'CREATE DATABASE IF NOT EXISTS plausible;'
|
||||
},
|
||||
{
|
||||
location: '$$workdir/init-db.sh',
|
||||
content: 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"id": "$$secret_email",
|
||||
"label": "Admin Email",
|
||||
"defaultValue": "admin@example.com",
|
||||
"description": "This is the admin email. Please change it.",
|
||||
"validRegex": /^([^\s^\/])+$/
|
||||
},
|
||||
{
|
||||
"id": "$$secret_name",
|
||||
"label": "Admin Name",
|
||||
"defaultValue": "admin",
|
||||
"description": "This is the admin username. Please change it.",
|
||||
"validRegex": /^([^\s^\/])+$/
|
||||
},
|
||||
{
|
||||
"id": "$$secret_password",
|
||||
"label": "Admin Password",
|
||||
"description": "This is the admin password. Please change it.",
|
||||
"validRegex": /^([^\s^\/])+$/
|
||||
},
|
||||
{
|
||||
"id": "$$secret_secret_key_base",
|
||||
"label": "Secret Key Base",
|
||||
"description": "",
|
||||
"validRegex": /^([^\s^\/])+$/
|
||||
},
|
||||
{
|
||||
"id": "$$secret_disable_auth",
|
||||
"label": "Disable Auth",
|
||||
"defaultValue": "false",
|
||||
"description": "",
|
||||
"validRegex": /^([^\s^\/])+$/
|
||||
},
|
||||
{
|
||||
"id": "$$secret_disable_registration",
|
||||
"label": "Disable Registration",
|
||||
"defaultValue": "true",
|
||||
"description": "",
|
||||
"validRegex": /^([^\s^\/])+$/
|
||||
},
|
||||
{
|
||||
"id": "$$secret_disable_registration",
|
||||
"label": "Disable Registration",
|
||||
"defaultValue": "true",
|
||||
"description": "",
|
||||
"validRegex": /^([^\s^\/])+$/
|
||||
},
|
||||
{
|
||||
"id": "$$secret_postgresql_username",
|
||||
"label": "PostgreSQL Username",
|
||||
"defaultValue": "postgresql",
|
||||
"description": "",
|
||||
"validRegex": /^([^\s^\/])+$/
|
||||
},
|
||||
{
|
||||
"id": "$$secret_postgresql_password",
|
||||
"label": "PostgreSQL Password",
|
||||
"defaultValue": "postgresql",
|
||||
"description": "",
|
||||
"validRegex": /^([^\s^\/])+$/
|
||||
}
|
||||
,
|
||||
{
|
||||
"id": "$$secret_postgresql_database",
|
||||
"label": "PostgreSQL Database",
|
||||
"defaultValue": "plausible",
|
||||
"description": "",
|
||||
"validRegex": /^([^\s^\/])+$/
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -73,25 +74,53 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
|
||||
let isRestarting = false;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, settings } = service;
|
||||
|
||||
let payload = {}
|
||||
if (destinationDockerId) {
|
||||
const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
|
||||
if (status?.found) {
|
||||
isRunning = status.status.isRunning;
|
||||
isExited = status.status.isExited;
|
||||
isRestarting = status.status.isRestarting
|
||||
const { stdout: containers } = await executeDockerCmd({
|
||||
dockerId: service.destinationDocker.id,
|
||||
command:
|
||||
`docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'`
|
||||
});
|
||||
const containersArray = containers.trim().split('\n');
|
||||
if (containersArray.length > 0 && containersArray[0] !== '') {
|
||||
for (const container of containersArray) {
|
||||
let isRunning = false;
|
||||
let isExited = false;
|
||||
let isRestarting = false;
|
||||
const containerObj = JSON.parse(container);
|
||||
const status = containerObj.State
|
||||
if (status === 'running') {
|
||||
isRunning = true;
|
||||
}
|
||||
if (status === 'exited') {
|
||||
isExited = true;
|
||||
}
|
||||
if (status === 'restarting') {
|
||||
isRestarting = true;
|
||||
}
|
||||
payload[containerObj.Names] = {
|
||||
status: {
|
||||
isRunning,
|
||||
isExited,
|
||||
isRestarting
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
isRunning,
|
||||
isExited,
|
||||
settings
|
||||
}
|
||||
return payload
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
function parseAndFindServiceTemplates(service: any) {
|
||||
const foundTemplate = templates.find(t => t.name === service.type)
|
||||
if (foundTemplate) {
|
||||
return JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', service.id).replaceAll('$$fqdn', service.fqdn))
|
||||
}
|
||||
|
||||
}
|
||||
export async function getService(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
@@ -102,7 +131,8 @@ export async function getService(request: FastifyRequest<OnlyId>) {
|
||||
}
|
||||
return {
|
||||
settings: await listSettings(),
|
||||
service
|
||||
service,
|
||||
template: parseAndFindServiceTemplates(service)
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
@@ -111,7 +141,7 @@ export async function getService(request: FastifyRequest<OnlyId>) {
|
||||
export async function getServiceType(request: FastifyRequest) {
|
||||
try {
|
||||
return {
|
||||
types: supportedServiceTypesAndVersions
|
||||
services: templates
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
@@ -120,8 +150,21 @@ export async function getServiceType(request: FastifyRequest) {
|
||||
export async function saveServiceType(request: FastifyRequest<SaveServiceType>, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const { type } = request.body;
|
||||
await configureServiceType({ id, type });
|
||||
const { name, variables = [], serviceDefaultVersion = 'latest' } = request.body;
|
||||
if (variables.length > 0) {
|
||||
for (const variable of variables) {
|
||||
const { id: variableId, defaultValue, value = null } = variable;
|
||||
if (variableId.startsWith('$$secret_')) {
|
||||
const secretName = variableId.replace('$$secret_', '');
|
||||
let secretValue = defaultValue || value || null;
|
||||
if (secretValue) secretValue = encrypt(secretValue);
|
||||
await prisma.serviceSecret.create({
|
||||
data: { name: secretName, value: secretValue, service: { connect: { id } } }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
await prisma.service.update({ where: { id }, data: { type: name, version: serviceDefaultVersion } })
|
||||
return reply.code(201).send()
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
|
Reference in New Issue
Block a user