mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-26 04:59:31 +00:00
fix: cleanupStuckedContainers
This commit is contained in:
8
apps/trpc-experimental/server/src/api/index.ts
Normal file
8
apps/trpc-experimental/server/src/api/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { FastifyPluginAsync } from 'fastify';
|
||||
|
||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.get('/', async function (_request, _reply) {
|
||||
return { status: 'ok' };
|
||||
});
|
||||
};
|
||||
export default root;
|
||||
7
apps/trpc-experimental/server/src/config.ts
Normal file
7
apps/trpc-experimental/server/src/config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ServerOptions } from './server';
|
||||
|
||||
export const serverConfig: ServerOptions = {
|
||||
dev: false,
|
||||
port: 2022,
|
||||
prefix: '/trpc'
|
||||
};
|
||||
23
apps/trpc-experimental/server/src/env.js
Normal file
23
apps/trpc-experimental/server/src/env.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const dotenv = require('dotenv');
|
||||
// const isDev = process.env.NODE_ENV === 'development';
|
||||
// dotenv.config({ path: isDev ? '../../.env' : '.env' });
|
||||
dotenv.config();
|
||||
const { z } = require('zod');
|
||||
|
||||
/*eslint sort-keys: "error"*/
|
||||
const envSchema = z.object({
|
||||
CODESANDBOX_HOST: z.string().optional(),
|
||||
NODE_ENV: z.enum(['development', 'test', 'production']),
|
||||
COOLIFY_DATABASE_URL: z.string(),
|
||||
COOLIFY_SECRET_KEY: z.string().length(32),
|
||||
COOLIFY_WHITE_LABELED: z.string().optional(),
|
||||
COOLIFY_WHITE_LABELED_ICON: z.string().optional()
|
||||
});
|
||||
|
||||
const env = envSchema.safeParse(process.env);
|
||||
|
||||
if (!env.success) {
|
||||
console.error('❌ Invalid environment variables:', JSON.stringify(env.error.format(), null, 4));
|
||||
process.exit(1);
|
||||
}
|
||||
module.exports.env = env.data;
|
||||
7
apps/trpc-experimental/server/src/index.ts
Normal file
7
apps/trpc-experimental/server/src/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { serverConfig } from './config';
|
||||
import { createServer } from './server';
|
||||
|
||||
const server = createServer(serverConfig);
|
||||
|
||||
server.start();
|
||||
|
||||
851
apps/trpc-experimental/server/src/jobs/applicationBuildQueue.ts
Normal file
851
apps/trpc-experimental/server/src/jobs/applicationBuildQueue.ts
Normal file
@@ -0,0 +1,851 @@
|
||||
import { parentPort } from 'node:worker_threads';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs/promises';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
import {
|
||||
copyBaseConfigurationFiles,
|
||||
makeLabelForSimpleDockerfile,
|
||||
makeLabelForStandaloneApplication,
|
||||
saveBuildLog,
|
||||
saveDockerRegistryCredentials,
|
||||
setDefaultConfiguration
|
||||
} from '../lib/buildPacks/common';
|
||||
import {
|
||||
createDirectories,
|
||||
decrypt,
|
||||
getDomain,
|
||||
generateSecrets,
|
||||
decryptApplication,
|
||||
pushToRegistry
|
||||
} from '../lib/common';
|
||||
import * as importers from '../lib/importers';
|
||||
import * as buildpacks from '../lib/buildPacks';
|
||||
import { prisma } from '../prisma';
|
||||
import { executeCommand } from '../lib/executeCommand';
|
||||
import { defaultComposeConfiguration } from '../lib/docker';
|
||||
|
||||
(async () => {
|
||||
if (parentPort) {
|
||||
parentPort.on('message', async (message) => {
|
||||
if (message === 'error') throw new Error('oops');
|
||||
if (message === 'cancel') {
|
||||
parentPort.postMessage('cancelled');
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
const pThrottle = await import('p-throttle');
|
||||
const throttle = pThrottle.default({
|
||||
limit: 1,
|
||||
interval: 2000
|
||||
});
|
||||
|
||||
const th = throttle(async () => {
|
||||
try {
|
||||
const queuedBuilds = await prisma.build.findMany({
|
||||
where: { status: { in: ['queued', 'running'] } },
|
||||
orderBy: { createdAt: 'asc' }
|
||||
});
|
||||
const { concurrentBuilds } = await prisma.setting.findFirst({});
|
||||
if (queuedBuilds.length > 0) {
|
||||
parentPort.postMessage({ deploying: true });
|
||||
const concurrency = concurrentBuilds;
|
||||
const pAll = await import('p-all');
|
||||
const actions = [];
|
||||
|
||||
for (const queueBuild of queuedBuilds) {
|
||||
actions.push(async () => {
|
||||
let application = await prisma.application.findUnique({
|
||||
where: { id: queueBuild.applicationId },
|
||||
include: {
|
||||
dockerRegistry: true,
|
||||
destinationDocker: true,
|
||||
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
||||
persistentStorage: true,
|
||||
secrets: true,
|
||||
settings: true,
|
||||
teams: true
|
||||
}
|
||||
});
|
||||
|
||||
let {
|
||||
id: buildId,
|
||||
type,
|
||||
gitSourceId,
|
||||
sourceBranch = null,
|
||||
pullmergeRequestId = null,
|
||||
previewApplicationId = null,
|
||||
forceRebuild,
|
||||
sourceRepository = null
|
||||
} = queueBuild;
|
||||
application = decryptApplication(application);
|
||||
|
||||
if (!gitSourceId && application.simpleDockerfile) {
|
||||
const {
|
||||
id: applicationId,
|
||||
destinationDocker,
|
||||
destinationDockerId,
|
||||
secrets,
|
||||
port,
|
||||
persistentStorage,
|
||||
exposePort,
|
||||
simpleDockerfile,
|
||||
dockerRegistry
|
||||
} = application;
|
||||
const { workdir } = await createDirectories({ repository: applicationId, buildId });
|
||||
try {
|
||||
if (queueBuild.status === 'running') {
|
||||
await saveBuildLog({
|
||||
line: 'Building halted, restarting...',
|
||||
buildId,
|
||||
applicationId: application.id
|
||||
});
|
||||
}
|
||||
const volumes =
|
||||
persistentStorage?.map((storage) => {
|
||||
if (storage.oldPath) {
|
||||
return `${applicationId}${storage.path
|
||||
.replace(/\//gi, '-')
|
||||
.replace('-app', '')}:${storage.path}`;
|
||||
}
|
||||
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
|
||||
}) || [];
|
||||
|
||||
if (destinationDockerId) {
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: { status: 'running' }
|
||||
});
|
||||
try {
|
||||
const { stdout: containers } = await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}`
|
||||
});
|
||||
if (containers) {
|
||||
const containerArray = containers.split('\n');
|
||||
if (containerArray.length > 0) {
|
||||
for (const container of containerArray) {
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker stop -t 0 ${container}`
|
||||
});
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker rm --force ${container}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
let envs = [];
|
||||
if (secrets.length > 0) {
|
||||
envs = [
|
||||
...envs,
|
||||
...generateSecrets(secrets, pullmergeRequestId, false, port)
|
||||
];
|
||||
}
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile);
|
||||
if (dockerRegistry) {
|
||||
const { url, username, password } = dockerRegistry;
|
||||
await saveDockerRegistryCredentials({ url, username, password, workdir });
|
||||
}
|
||||
|
||||
const labels = makeLabelForSimpleDockerfile({
|
||||
applicationId,
|
||||
type,
|
||||
port: exposePort ? `${exposePort}:${port}` : port
|
||||
});
|
||||
try {
|
||||
const composeVolumes = volumes.map((volume) => {
|
||||
return {
|
||||
[`${volume.split(':')[0]}`]: {
|
||||
name: volume.split(':')[0]
|
||||
}
|
||||
};
|
||||
});
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[applicationId]: {
|
||||
build: {
|
||||
context: workdir
|
||||
},
|
||||
image: `${applicationId}:${buildId}`,
|
||||
container_name: applicationId,
|
||||
volumes,
|
||||
labels,
|
||||
environment: envs,
|
||||
depends_on: [],
|
||||
expose: [port],
|
||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||
...defaultComposeConfiguration(destinationDocker.network)
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[destinationDocker.network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: Object.assign({}, ...composeVolumes)
|
||||
};
|
||||
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
|
||||
await executeCommand({
|
||||
debug: true,
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml up -d`
|
||||
});
|
||||
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
|
||||
} catch (error) {
|
||||
await saveBuildLog({ line: error, buildId, applicationId });
|
||||
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
|
||||
if (foundBuild) {
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: {
|
||||
status: 'failed'
|
||||
}
|
||||
});
|
||||
}
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
|
||||
if (foundBuild) {
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: {
|
||||
status: 'failed'
|
||||
}
|
||||
});
|
||||
}
|
||||
if (error !== 1) {
|
||||
await saveBuildLog({ line: error, buildId, applicationId: application.id });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
await saveBuildLog({
|
||||
line: error.message,
|
||||
buildId,
|
||||
applicationId: application.id
|
||||
});
|
||||
}
|
||||
await fs.rm(workdir, { recursive: true, force: true });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (application.dockerRegistryImageName) {
|
||||
const customTag = application.dockerRegistryImageName.split(':')[1] || buildId;
|
||||
const imageName = application.dockerRegistryImageName.split(':')[0];
|
||||
await saveBuildLog({
|
||||
line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`,
|
||||
buildId,
|
||||
applicationId: application.id
|
||||
});
|
||||
await pushToRegistry(application, workdir, buildId, imageName, customTag);
|
||||
await saveBuildLog({ line: 'Success', buildId, applicationId: application.id });
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.stdout) {
|
||||
await saveBuildLog({ line: error.stdout, buildId, applicationId });
|
||||
}
|
||||
if (error.stderr) {
|
||||
await saveBuildLog({ line: error.stderr, buildId, applicationId });
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(workdir, { recursive: true, force: true });
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: { status: 'success' }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const originalApplicationId = application.id;
|
||||
const {
|
||||
id: applicationId,
|
||||
name,
|
||||
destinationDocker,
|
||||
destinationDockerId,
|
||||
gitSource,
|
||||
configHash,
|
||||
fqdn,
|
||||
projectId,
|
||||
secrets,
|
||||
phpModules,
|
||||
settings,
|
||||
persistentStorage,
|
||||
pythonWSGI,
|
||||
pythonModule,
|
||||
pythonVariable,
|
||||
denoOptions,
|
||||
exposePort,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
deploymentType,
|
||||
gitCommitHash,
|
||||
dockerRegistry
|
||||
} = application;
|
||||
|
||||
let {
|
||||
branch,
|
||||
repository,
|
||||
buildPack,
|
||||
port,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
publishDirectory,
|
||||
dockerFileLocation,
|
||||
dockerComposeFileLocation,
|
||||
dockerComposeConfiguration,
|
||||
denoMainFile
|
||||
} = application;
|
||||
|
||||
let imageId = applicationId;
|
||||
let domain = getDomain(fqdn);
|
||||
|
||||
let location = null;
|
||||
|
||||
let tag = null;
|
||||
let customTag = null;
|
||||
let imageName = null;
|
||||
|
||||
let imageFoundLocally = false;
|
||||
let imageFoundRemotely = false;
|
||||
|
||||
if (pullmergeRequestId) {
|
||||
const previewApplications = await prisma.previewApplication.findMany({
|
||||
where: { applicationId: originalApplicationId, pullmergeRequestId }
|
||||
});
|
||||
if (previewApplications.length > 0) {
|
||||
previewApplicationId = previewApplications[0].id;
|
||||
}
|
||||
// Previews, we need to get the source branch and set subdomain
|
||||
branch = sourceBranch;
|
||||
domain = `${pullmergeRequestId}.${domain}`;
|
||||
imageId = `${applicationId}-${pullmergeRequestId}`;
|
||||
repository = sourceRepository || repository;
|
||||
}
|
||||
const { workdir, repodir } = await createDirectories({ repository, buildId });
|
||||
try {
|
||||
if (queueBuild.status === 'running') {
|
||||
await saveBuildLog({
|
||||
line: 'Building halted, restarting...',
|
||||
buildId,
|
||||
applicationId: application.id
|
||||
});
|
||||
}
|
||||
|
||||
const currentHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(
|
||||
JSON.stringify({
|
||||
pythonWSGI,
|
||||
pythonModule,
|
||||
pythonVariable,
|
||||
deploymentType,
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
buildPack,
|
||||
port,
|
||||
exposePort,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
secrets,
|
||||
branch,
|
||||
repository,
|
||||
fqdn
|
||||
})
|
||||
)
|
||||
.digest('hex');
|
||||
const { debug } = settings;
|
||||
if (!debug) {
|
||||
await saveBuildLog({
|
||||
line: `Debug logging is disabled. Enable it above if necessary!`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
}
|
||||
const volumes =
|
||||
persistentStorage?.map((storage) => {
|
||||
if (storage.oldPath) {
|
||||
return `${applicationId}${storage.path
|
||||
.replace(/\//gi, '-')
|
||||
.replace('-app', '')}:${storage.path}`;
|
||||
}
|
||||
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
|
||||
}) || [];
|
||||
|
||||
try {
|
||||
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration);
|
||||
} catch (error) {}
|
||||
let deployNeeded = true;
|
||||
let destinationType;
|
||||
|
||||
if (destinationDockerId) {
|
||||
destinationType = 'docker';
|
||||
}
|
||||
if (destinationType === 'docker') {
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: { status: 'running' }
|
||||
});
|
||||
|
||||
const configuration = await setDefaultConfiguration(application);
|
||||
|
||||
buildPack = configuration.buildPack;
|
||||
port = configuration.port;
|
||||
installCommand = configuration.installCommand;
|
||||
startCommand = configuration.startCommand;
|
||||
buildCommand = configuration.buildCommand;
|
||||
publishDirectory = configuration.publishDirectory;
|
||||
baseDirectory = configuration.baseDirectory || '';
|
||||
dockerFileLocation = configuration.dockerFileLocation;
|
||||
dockerComposeFileLocation = configuration.dockerComposeFileLocation;
|
||||
denoMainFile = configuration.denoMainFile;
|
||||
const commit = await importers[gitSource.type]({
|
||||
applicationId,
|
||||
debug,
|
||||
workdir,
|
||||
repodir,
|
||||
githubAppId: gitSource.githubApp?.id,
|
||||
gitlabAppId: gitSource.gitlabApp?.id,
|
||||
customPort: gitSource.customPort,
|
||||
gitCommitHash,
|
||||
configuration,
|
||||
repository,
|
||||
branch,
|
||||
buildId,
|
||||
apiUrl: gitSource.apiUrl,
|
||||
htmlUrl: gitSource.htmlUrl,
|
||||
projectId,
|
||||
deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
|
||||
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null,
|
||||
forPublic: gitSource.forPublic
|
||||
});
|
||||
if (!commit) {
|
||||
throw new Error('No commit found?');
|
||||
}
|
||||
tag = commit.slice(0, 7);
|
||||
if (pullmergeRequestId) {
|
||||
tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`;
|
||||
}
|
||||
if (application.dockerRegistryImageName) {
|
||||
imageName = application.dockerRegistryImageName.split(':')[0];
|
||||
customTag = application.dockerRegistryImageName.split(':')[1] || tag;
|
||||
} else {
|
||||
customTag = tag;
|
||||
imageName = applicationId;
|
||||
}
|
||||
|
||||
if (pullmergeRequestId) {
|
||||
customTag = `${customTag}-${pullmergeRequestId}`;
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.build.update({ where: { id: buildId }, data: { commit } });
|
||||
} catch (err) {}
|
||||
|
||||
if (!pullmergeRequestId) {
|
||||
if (configHash !== currentHash) {
|
||||
deployNeeded = true;
|
||||
if (configHash) {
|
||||
await saveBuildLog({
|
||||
line: 'Configuration changed',
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
}
|
||||
} else {
|
||||
deployNeeded = false;
|
||||
}
|
||||
} else {
|
||||
deployNeeded = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await executeCommand({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker image inspect ${applicationId}:${tag}`
|
||||
});
|
||||
imageFoundLocally = true;
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
if (dockerRegistry) {
|
||||
const { url, username, password } = dockerRegistry;
|
||||
location = await saveDockerRegistryCredentials({
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
workdir
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await executeCommand({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker ${
|
||||
location ? `--config ${location}` : ''
|
||||
} pull ${imageName}:${customTag}`
|
||||
});
|
||||
imageFoundRemotely = true;
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
let imageFound = `${applicationId}:${tag}`;
|
||||
if (imageFoundRemotely) {
|
||||
imageFound = `${imageName}:${customTag}`;
|
||||
}
|
||||
await copyBaseConfigurationFiles(
|
||||
buildPack,
|
||||
workdir,
|
||||
buildId,
|
||||
applicationId,
|
||||
baseImage
|
||||
);
|
||||
const labels = makeLabelForStandaloneApplication({
|
||||
applicationId,
|
||||
fqdn,
|
||||
name,
|
||||
type,
|
||||
pullmergeRequestId,
|
||||
buildPack,
|
||||
repository,
|
||||
branch,
|
||||
projectId,
|
||||
port: exposePort ? `${exposePort}:${port}` : port,
|
||||
commit,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
publishDirectory
|
||||
});
|
||||
if (forceRebuild) deployNeeded = true;
|
||||
if ((!imageFoundLocally && !imageFoundRemotely) || deployNeeded) {
|
||||
if (buildPack === 'static') {
|
||||
await buildpacks.staticApp({
|
||||
dockerId: destinationDocker.id,
|
||||
network: destinationDocker.network,
|
||||
buildId,
|
||||
applicationId,
|
||||
domain,
|
||||
name,
|
||||
type,
|
||||
volumes,
|
||||
labels,
|
||||
pullmergeRequestId,
|
||||
buildPack,
|
||||
repository,
|
||||
branch,
|
||||
projectId,
|
||||
publishDirectory,
|
||||
debug,
|
||||
commit,
|
||||
tag,
|
||||
workdir,
|
||||
port: exposePort ? `${exposePort}:${port}` : port,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
phpModules,
|
||||
pythonWSGI,
|
||||
pythonModule,
|
||||
pythonVariable,
|
||||
dockerFileLocation,
|
||||
dockerComposeConfiguration,
|
||||
dockerComposeFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
deploymentType,
|
||||
forceRebuild
|
||||
});
|
||||
} else if (buildpacks[buildPack])
|
||||
await buildpacks[buildPack]({
|
||||
dockerId: destinationDocker.id,
|
||||
network: destinationDocker.network,
|
||||
buildId,
|
||||
applicationId,
|
||||
domain,
|
||||
name,
|
||||
type,
|
||||
volumes,
|
||||
labels,
|
||||
pullmergeRequestId,
|
||||
buildPack,
|
||||
repository,
|
||||
branch,
|
||||
projectId,
|
||||
publishDirectory,
|
||||
debug,
|
||||
commit,
|
||||
tag,
|
||||
workdir,
|
||||
port: exposePort ? `${exposePort}:${port}` : port,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
phpModules,
|
||||
pythonWSGI,
|
||||
pythonModule,
|
||||
pythonVariable,
|
||||
dockerFileLocation,
|
||||
dockerComposeConfiguration,
|
||||
dockerComposeFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
deploymentType,
|
||||
forceRebuild
|
||||
});
|
||||
else {
|
||||
await saveBuildLog({
|
||||
line: `Build pack ${buildPack} not found`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
throw new Error(`Build pack ${buildPack} not found.`);
|
||||
}
|
||||
} else {
|
||||
if (imageFoundRemotely || deployNeeded) {
|
||||
await saveBuildLog({
|
||||
line: `Container image ${imageFound} found in Docker Registry - reuising it`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
} else {
|
||||
if (imageFoundLocally || deployNeeded) {
|
||||
await saveBuildLog({
|
||||
line: `Container image ${imageFound} found locally - reuising it`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buildPack === 'compose') {
|
||||
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`;
|
||||
try {
|
||||
const { stdout: containers } = await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}`
|
||||
});
|
||||
if (containers) {
|
||||
const containerArray = containers.split('\n');
|
||||
if (containerArray.length > 0) {
|
||||
for (const container of containerArray) {
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker stop -t 0 ${container}`
|
||||
});
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker rm --force ${container}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
try {
|
||||
await executeCommand({
|
||||
debug,
|
||||
buildId,
|
||||
applicationId,
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker compose --project-directory ${workdir} -f ${fileYaml} up -d`
|
||||
});
|
||||
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: { status: 'success' }
|
||||
});
|
||||
await prisma.application.update({
|
||||
where: { id: applicationId },
|
||||
data: { configHash: currentHash }
|
||||
});
|
||||
} catch (error) {
|
||||
await saveBuildLog({ line: error, buildId, applicationId });
|
||||
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
|
||||
if (foundBuild) {
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: {
|
||||
status: 'failed'
|
||||
}
|
||||
});
|
||||
}
|
||||
throw new Error(error);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { stdout: containers } = await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=com.docker.compose.service=${
|
||||
pullmergeRequestId ? imageId : applicationId
|
||||
}' --format {{.ID}}`
|
||||
});
|
||||
if (containers) {
|
||||
const containerArray = containers.split('\n');
|
||||
if (containerArray.length > 0) {
|
||||
for (const container of containerArray) {
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker stop -t 0 ${container}`
|
||||
});
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker rm --force ${container}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
let envs = [];
|
||||
if (secrets.length > 0) {
|
||||
envs = [
|
||||
...envs,
|
||||
...generateSecrets(secrets, pullmergeRequestId, false, port)
|
||||
];
|
||||
}
|
||||
if (dockerRegistry) {
|
||||
const { url, username, password } = dockerRegistry;
|
||||
await saveDockerRegistryCredentials({ url, username, password, workdir });
|
||||
}
|
||||
try {
|
||||
const composeVolumes = volumes.map((volume) => {
|
||||
return {
|
||||
[`${volume.split(':')[0]}`]: {
|
||||
name: volume.split(':')[0]
|
||||
}
|
||||
};
|
||||
});
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[imageId]: {
|
||||
image: imageFound,
|
||||
container_name: imageId,
|
||||
volumes,
|
||||
environment: envs,
|
||||
labels,
|
||||
depends_on: [],
|
||||
expose: [port],
|
||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||
...defaultComposeConfiguration(destinationDocker.network)
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[destinationDocker.network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: Object.assign({}, ...composeVolumes)
|
||||
};
|
||||
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
|
||||
await executeCommand({
|
||||
debug,
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml up -d`
|
||||
});
|
||||
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
|
||||
} catch (error) {
|
||||
await saveBuildLog({ line: error, buildId, applicationId });
|
||||
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
|
||||
if (foundBuild) {
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: {
|
||||
status: 'failed'
|
||||
}
|
||||
});
|
||||
}
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (!pullmergeRequestId)
|
||||
await prisma.application.update({
|
||||
where: { id: applicationId },
|
||||
data: { configHash: currentHash }
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
|
||||
if (foundBuild) {
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: {
|
||||
status: 'failed'
|
||||
}
|
||||
});
|
||||
}
|
||||
if (error !== 1) {
|
||||
await saveBuildLog({ line: error, buildId, applicationId: application.id });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
await saveBuildLog({
|
||||
line: error.message,
|
||||
buildId,
|
||||
applicationId: application.id
|
||||
});
|
||||
}
|
||||
await fs.rm(workdir, { recursive: true, force: true });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (application.dockerRegistryImageName && (!imageFoundRemotely || forceRebuild)) {
|
||||
await saveBuildLog({
|
||||
line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`,
|
||||
buildId,
|
||||
applicationId: application.id
|
||||
});
|
||||
await pushToRegistry(application, workdir, tag, imageName, customTag);
|
||||
await saveBuildLog({ line: 'Success', buildId, applicationId: application.id });
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.stdout) {
|
||||
await saveBuildLog({ line: error.stdout, buildId, applicationId });
|
||||
}
|
||||
if (error.stderr) {
|
||||
await saveBuildLog({ line: error.stderr, buildId, applicationId });
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(workdir, { recursive: true, force: true });
|
||||
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
|
||||
}
|
||||
});
|
||||
}
|
||||
await pAll.default(actions, { concurrency });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
while (true) {
|
||||
await th();
|
||||
}
|
||||
} else {
|
||||
console.log('hello');
|
||||
process.exit(0);
|
||||
}
|
||||
})();
|
||||
843
apps/trpc-experimental/server/src/lib/buildPacks/common.ts
Normal file
843
apps/trpc-experimental/server/src/lib/buildPacks/common.ts
Normal file
@@ -0,0 +1,843 @@
|
||||
import {
|
||||
base64Encode,
|
||||
decrypt,
|
||||
encrypt,
|
||||
generateSecrets,
|
||||
generateTimestamp,
|
||||
getDomain,
|
||||
isARM,
|
||||
isDev,
|
||||
version
|
||||
} from '../common';
|
||||
import { promises as fs } from 'fs';
|
||||
import { day } from '../dayjs';
|
||||
import { prisma } from '../../prisma';
|
||||
import { executeCommand } from '../executeCommand';
|
||||
|
||||
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
|
||||
const nodeBased = [
|
||||
'react',
|
||||
'preact',
|
||||
'vuejs',
|
||||
'svelte',
|
||||
'gatsby',
|
||||
'astro',
|
||||
'eleventy',
|
||||
'node',
|
||||
'nestjs',
|
||||
'nuxtjs',
|
||||
'nextjs'
|
||||
];
|
||||
|
||||
export function setDefaultBaseImage(
|
||||
buildPack: string | null,
|
||||
deploymentType: string | null = null
|
||||
) {
|
||||
const nodeVersions = [
|
||||
{
|
||||
value: 'node:lts',
|
||||
label: 'node:lts'
|
||||
},
|
||||
{
|
||||
value: 'node:18',
|
||||
label: 'node:18'
|
||||
},
|
||||
{
|
||||
value: 'node:17',
|
||||
label: 'node:17'
|
||||
},
|
||||
{
|
||||
value: 'node:16',
|
||||
label: 'node:16'
|
||||
},
|
||||
{
|
||||
value: 'node:14',
|
||||
label: 'node:14'
|
||||
},
|
||||
{
|
||||
value: 'node:12',
|
||||
label: 'node:12'
|
||||
}
|
||||
];
|
||||
const staticVersions = [
|
||||
{
|
||||
value: 'webdevops/nginx:alpine',
|
||||
label: 'webdevops/nginx:alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/apache:alpine',
|
||||
label: 'webdevops/apache:alpine'
|
||||
},
|
||||
{
|
||||
value: 'nginx:alpine',
|
||||
label: 'nginx:alpine'
|
||||
},
|
||||
{
|
||||
value: 'httpd:alpine',
|
||||
label: 'httpd:alpine (Apache)'
|
||||
}
|
||||
];
|
||||
const rustVersions = [
|
||||
{
|
||||
value: 'rust:latest',
|
||||
label: 'rust:latest'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60',
|
||||
label: 'rust:1.60'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-buster',
|
||||
label: 'rust:1.60-buster'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-bullseye',
|
||||
label: 'rust:1.60-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-slim-buster',
|
||||
label: 'rust:1.60-slim-buster'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-slim-bullseye',
|
||||
label: 'rust:1.60-slim-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-alpine3.14',
|
||||
label: 'rust:1.60-alpine3.14'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-alpine3.15',
|
||||
label: 'rust:1.60-alpine3.15'
|
||||
}
|
||||
];
|
||||
const phpVersions = [
|
||||
{
|
||||
value: 'webdevops/php-apache:8.2',
|
||||
label: 'webdevops/php-apache:8.2'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.2',
|
||||
label: 'webdevops/php-nginx:8.2'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.1',
|
||||
label: 'webdevops/php-apache:8.1'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.1',
|
||||
label: 'webdevops/php-nginx:8.1'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.0',
|
||||
label: 'webdevops/php-apache:8.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.0',
|
||||
label: 'webdevops/php-nginx:8.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.4',
|
||||
label: 'webdevops/php-apache:7.4'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.4',
|
||||
label: 'webdevops/php-nginx:7.4'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.3',
|
||||
label: 'webdevops/php-apache:7.3'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.3',
|
||||
label: 'webdevops/php-nginx:7.3'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.2',
|
||||
label: 'webdevops/php-apache:7.2'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.2',
|
||||
label: 'webdevops/php-nginx:7.2'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.1',
|
||||
label: 'webdevops/php-apache:7.1'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.1',
|
||||
label: 'webdevops/php-nginx:7.1'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.0',
|
||||
label: 'webdevops/php-apache:7.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.0',
|
||||
label: 'webdevops/php-nginx:7.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:5.6',
|
||||
label: 'webdevops/php-apache:5.6'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:5.6',
|
||||
label: 'webdevops/php-nginx:5.6'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.2-alpine',
|
||||
label: 'webdevops/php-apache:8.2-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.2-alpine',
|
||||
label: 'webdevops/php-nginx:8.2-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.1-alpine',
|
||||
label: 'webdevops/php-apache:8.1-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.1-alpine',
|
||||
label: 'webdevops/php-nginx:8.1-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.0-alpine',
|
||||
label: 'webdevops/php-apache:8.0-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.0-alpine',
|
||||
label: 'webdevops/php-nginx:8.0-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.4-alpine',
|
||||
label: 'webdevops/php-apache:7.4-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.4-alpine',
|
||||
label: 'webdevops/php-nginx:7.4-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.3-alpine',
|
||||
label: 'webdevops/php-apache:7.3-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.3-alpine',
|
||||
label: 'webdevops/php-nginx:7.3-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.2-alpine',
|
||||
label: 'webdevops/php-apache:7.2-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.2-alpine',
|
||||
label: 'webdevops/php-nginx:7.2-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.1-alpine',
|
||||
label: 'webdevops/php-apache:7.1-alpine'
|
||||
},
|
||||
{
|
||||
value: 'php:8.1-fpm',
|
||||
label: 'php:8.1-fpm'
|
||||
},
|
||||
{
|
||||
value: 'php:8.0-fpm',
|
||||
label: 'php:8.0-fpm'
|
||||
},
|
||||
{
|
||||
value: 'php:8.1-fpm-alpine',
|
||||
label: 'php:8.1-fpm-alpine'
|
||||
},
|
||||
{
|
||||
value: 'php:8.0-fpm-alpine',
|
||||
label: 'php:8.0-fpm-alpine'
|
||||
}
|
||||
];
|
||||
const pythonVersions = [
|
||||
{
|
||||
value: 'python:3.10-alpine',
|
||||
label: 'python:3.10-alpine'
|
||||
},
|
||||
{
|
||||
value: 'python:3.10-buster',
|
||||
label: 'python:3.10-buster'
|
||||
},
|
||||
{
|
||||
value: 'python:3.10-bullseye',
|
||||
label: 'python:3.10-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.10-slim-bullseye',
|
||||
label: 'python:3.10-slim-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.9-alpine',
|
||||
label: 'python:3.9-alpine'
|
||||
},
|
||||
{
|
||||
value: 'python:3.9-buster',
|
||||
label: 'python:3.9-buster'
|
||||
},
|
||||
{
|
||||
value: 'python:3.9-bullseye',
|
||||
label: 'python:3.9-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.9-slim-bullseye',
|
||||
label: 'python:3.9-slim-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.8-alpine',
|
||||
label: 'python:3.8-alpine'
|
||||
},
|
||||
{
|
||||
value: 'python:3.8-buster',
|
||||
label: 'python:3.8-buster'
|
||||
},
|
||||
{
|
||||
value: 'python:3.8-bullseye',
|
||||
label: 'python:3.8-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.8-slim-bullseye',
|
||||
label: 'python:3.8-slim-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.7-alpine',
|
||||
label: 'python:3.7-alpine'
|
||||
},
|
||||
{
|
||||
value: 'python:3.7-buster',
|
||||
label: 'python:3.7-buster'
|
||||
},
|
||||
{
|
||||
value: 'python:3.7-bullseye',
|
||||
label: 'python:3.7-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.7-slim-bullseye',
|
||||
label: 'python:3.7-slim-bullseye'
|
||||
}
|
||||
];
|
||||
const herokuVersions = [
|
||||
{
|
||||
value: 'heroku/builder:22',
|
||||
label: 'heroku/builder:22'
|
||||
},
|
||||
{
|
||||
value: 'heroku/buildpacks:20',
|
||||
label: 'heroku/buildpacks:20'
|
||||
},
|
||||
{
|
||||
value: 'heroku/builder-classic:22',
|
||||
label: 'heroku/builder-classic:22'
|
||||
}
|
||||
];
|
||||
let payload: any = {
|
||||
baseImage: null,
|
||||
baseBuildImage: null,
|
||||
baseImages: [],
|
||||
baseBuildImages: []
|
||||
};
|
||||
if (nodeBased.includes(buildPack)) {
|
||||
if (deploymentType === 'static') {
|
||||
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine';
|
||||
payload.baseImages = isARM(process.arch)
|
||||
? staticVersions.filter((version) => !version.value.includes('webdevops'))
|
||||
: staticVersions;
|
||||
payload.baseBuildImage = 'node:lts';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
} else {
|
||||
payload.baseImage = 'node:lts';
|
||||
payload.baseImages = nodeVersions;
|
||||
payload.baseBuildImage = 'node:lts';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
}
|
||||
}
|
||||
if (staticApps.includes(buildPack)) {
|
||||
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine';
|
||||
payload.baseImages = isARM(process.arch)
|
||||
? staticVersions.filter((version) => !version.value.includes('webdevops'))
|
||||
: staticVersions;
|
||||
payload.baseBuildImage = 'node:lts';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
}
|
||||
if (buildPack === 'python') {
|
||||
payload.baseImage = 'python:3.10-alpine';
|
||||
payload.baseImages = pythonVersions;
|
||||
}
|
||||
if (buildPack === 'rust') {
|
||||
payload.baseImage = 'rust:latest';
|
||||
payload.baseBuildImage = 'rust:latest';
|
||||
payload.baseImages = rustVersions;
|
||||
payload.baseBuildImages = rustVersions;
|
||||
}
|
||||
if (buildPack === 'deno') {
|
||||
payload.baseImage = 'denoland/deno:latest';
|
||||
}
|
||||
if (buildPack === 'php') {
|
||||
payload.baseImage = isARM(process.arch)
|
||||
? 'php:8.1-fpm-alpine'
|
||||
: 'webdevops/php-apache:8.2-alpine';
|
||||
payload.baseImages = isARM(process.arch)
|
||||
? phpVersions.filter((version) => !version.value.includes('webdevops'))
|
||||
: phpVersions;
|
||||
}
|
||||
if (buildPack === 'laravel') {
|
||||
payload.baseImage = isARM(process.arch)
|
||||
? 'php:8.1-fpm-alpine'
|
||||
: 'webdevops/php-apache:8.2-alpine';
|
||||
payload.baseImages = isARM(process.arch)
|
||||
? phpVersions.filter((version) => !version.value.includes('webdevops'))
|
||||
: phpVersions;
|
||||
payload.baseBuildImage = 'node:18';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
}
|
||||
if (buildPack === 'heroku') {
|
||||
payload.baseImage = 'heroku/buildpacks:20';
|
||||
payload.baseImages = herokuVersions;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
export const setDefaultConfiguration = async (data: any) => {
|
||||
let {
|
||||
buildPack,
|
||||
port,
|
||||
installCommand,
|
||||
startCommand,
|
||||
buildCommand,
|
||||
publishDirectory,
|
||||
baseDirectory,
|
||||
dockerFileLocation,
|
||||
dockerComposeFileLocation,
|
||||
denoMainFile
|
||||
} = data;
|
||||
//@ts-ignore
|
||||
const template = scanningTemplates[buildPack];
|
||||
if (!port) {
|
||||
port = template?.port || 3000;
|
||||
|
||||
if (buildPack === 'static') port = 80;
|
||||
else if (buildPack === 'node') port = 3000;
|
||||
else if (buildPack === 'php') port = 80;
|
||||
else if (buildPack === 'python') port = 8000;
|
||||
}
|
||||
if (!installCommand && buildPack !== 'static' && buildPack !== 'laravel')
|
||||
installCommand = template?.installCommand || 'yarn install';
|
||||
if (!startCommand && buildPack !== 'static' && buildPack !== 'laravel')
|
||||
startCommand = template?.startCommand || 'yarn start';
|
||||
if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
|
||||
buildCommand = template?.buildCommand || null;
|
||||
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
|
||||
if (baseDirectory) {
|
||||
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
|
||||
if (baseDirectory.endsWith('/') && baseDirectory !== '/')
|
||||
baseDirectory = baseDirectory.slice(0, -1);
|
||||
}
|
||||
if (dockerFileLocation) {
|
||||
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
|
||||
if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1);
|
||||
} else {
|
||||
dockerFileLocation = '/Dockerfile';
|
||||
}
|
||||
if (dockerComposeFileLocation) {
|
||||
if (!dockerComposeFileLocation.startsWith('/'))
|
||||
dockerComposeFileLocation = `/${dockerComposeFileLocation}`;
|
||||
if (dockerComposeFileLocation.endsWith('/'))
|
||||
dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1);
|
||||
} else {
|
||||
dockerComposeFileLocation = '/Dockerfile';
|
||||
}
|
||||
if (!denoMainFile) {
|
||||
denoMainFile = 'main.ts';
|
||||
}
|
||||
|
||||
return {
|
||||
buildPack,
|
||||
port,
|
||||
installCommand,
|
||||
startCommand,
|
||||
buildCommand,
|
||||
publishDirectory,
|
||||
baseDirectory,
|
||||
dockerFileLocation,
|
||||
dockerComposeFileLocation,
|
||||
denoMainFile
|
||||
};
|
||||
};
|
||||
|
||||
export const scanningTemplates = {
|
||||
'@sveltejs/kit': {
|
||||
buildPack: 'nodejs'
|
||||
},
|
||||
astro: {
|
||||
buildPack: 'astro'
|
||||
},
|
||||
'@11ty/eleventy': {
|
||||
buildPack: 'eleventy'
|
||||
},
|
||||
svelte: {
|
||||
buildPack: 'svelte'
|
||||
},
|
||||
'@nestjs/core': {
|
||||
buildPack: 'nestjs'
|
||||
},
|
||||
next: {
|
||||
buildPack: 'nextjs'
|
||||
},
|
||||
nuxt: {
|
||||
buildPack: 'nuxtjs'
|
||||
},
|
||||
'react-scripts': {
|
||||
buildPack: 'react'
|
||||
},
|
||||
'parcel-bundler': {
|
||||
buildPack: 'static'
|
||||
},
|
||||
'@vue/cli-service': {
|
||||
buildPack: 'vuejs'
|
||||
},
|
||||
vuejs: {
|
||||
buildPack: 'vuejs'
|
||||
},
|
||||
gatsby: {
|
||||
buildPack: 'gatsby'
|
||||
},
|
||||
'preact-cli': {
|
||||
buildPack: 'react'
|
||||
}
|
||||
};
|
||||
|
||||
export const saveBuildLog = async ({
|
||||
line,
|
||||
buildId,
|
||||
applicationId
|
||||
}: {
|
||||
line: string;
|
||||
buildId: string;
|
||||
applicationId: string;
|
||||
}): Promise<any> => {
|
||||
if (buildId === 'undefined' || buildId === 'null' || !buildId) return;
|
||||
if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return;
|
||||
const { default: got } = await import('got');
|
||||
if (typeof line === 'object' && line) {
|
||||
if (line.shortMessage) {
|
||||
line = line.shortMessage + '\n' + line.stderr;
|
||||
} else {
|
||||
line = JSON.stringify(line);
|
||||
}
|
||||
}
|
||||
if (line && typeof line === 'string' && line.includes('ghs_')) {
|
||||
const regex = /ghs_.*@/g;
|
||||
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
||||
}
|
||||
const addTimestamp = `[${generateTimestamp()}] ${line}`;
|
||||
const fluentBitUrl = isDev
|
||||
? process.env.COOLIFY_CONTAINER_DEV === 'true'
|
||||
? 'http://coolify-fluentbit:24224'
|
||||
: 'http://localhost:24224'
|
||||
: 'http://coolify-fluentbit:24224';
|
||||
|
||||
if (isDev && !process.env.COOLIFY_CONTAINER_DEV) {
|
||||
console.debug(`[${applicationId}] ${addTimestamp}`);
|
||||
}
|
||||
try {
|
||||
return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, {
|
||||
json: {
|
||||
line: encrypt(line)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return await prisma.buildLog.create({
|
||||
data: {
|
||||
line: addTimestamp,
|
||||
buildId,
|
||||
time: Number(day().valueOf()),
|
||||
applicationId
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export async function copyBaseConfigurationFiles(
|
||||
buildPack,
|
||||
workdir,
|
||||
buildId,
|
||||
applicationId,
|
||||
baseImage
|
||||
) {
|
||||
try {
|
||||
if (buildPack === 'php') {
|
||||
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
|
||||
await saveBuildLog({
|
||||
line: 'Copied default configuration file for PHP.',
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
} else if (baseImage?.includes('nginx')) {
|
||||
await fs.writeFile(
|
||||
`${workdir}/nginx.conf`,
|
||||
`user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /docker.stdout;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /docker.stdout main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /app;
|
||||
index index.html;
|
||||
try_files $uri $uri/index.html $uri/ /index.html =404;
|
||||
}
|
||||
|
||||
error_page 404 /50x.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /app;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
`
|
||||
);
|
||||
}
|
||||
// TODO: Add more configuration files for other buildpacks, like apache2, etc.
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkPnpm(installCommand = null, buildCommand = null, startCommand = null) {
|
||||
return (
|
||||
installCommand?.includes('pnpm') ||
|
||||
buildCommand?.includes('pnpm') ||
|
||||
startCommand?.includes('pnpm')
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveDockerRegistryCredentials({ url, username, password, workdir }) {
|
||||
if (!username || !password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let decryptedPassword = decrypt(password);
|
||||
const location = `${workdir}/.docker`;
|
||||
|
||||
try {
|
||||
await fs.mkdir(`${workdir}/.docker`);
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
}
|
||||
const payload = JSON.stringify({
|
||||
auths: {
|
||||
[url]: {
|
||||
auth: Buffer.from(`${username}:${decryptedPassword}`).toString('base64')
|
||||
}
|
||||
}
|
||||
});
|
||||
await fs.writeFile(`${location}/config.json`, payload);
|
||||
return location;
|
||||
}
|
||||
export async function buildImage({
|
||||
applicationId,
|
||||
tag,
|
||||
workdir,
|
||||
buildId,
|
||||
dockerId,
|
||||
isCache = false,
|
||||
debug = false,
|
||||
dockerFileLocation = '/Dockerfile',
|
||||
commit,
|
||||
forceRebuild = false
|
||||
}) {
|
||||
if (isCache) {
|
||||
await saveBuildLog({ line: `Building cache image...`, buildId, applicationId });
|
||||
} else {
|
||||
await saveBuildLog({ line: `Building production image...`, buildId, applicationId });
|
||||
}
|
||||
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`;
|
||||
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`;
|
||||
let location = null;
|
||||
|
||||
const { dockerRegistry } = await prisma.application.findUnique({
|
||||
where: { id: applicationId },
|
||||
select: { dockerRegistry: true }
|
||||
});
|
||||
if (dockerRegistry) {
|
||||
const { url, username, password } = dockerRegistry;
|
||||
location = await saveDockerRegistryCredentials({ url, username, password, workdir });
|
||||
}
|
||||
|
||||
await executeCommand({
|
||||
stream: true,
|
||||
debug,
|
||||
buildId,
|
||||
applicationId,
|
||||
dockerId,
|
||||
command: `docker ${location ? `--config ${location}` : ''} build ${
|
||||
forceRebuild ? '--no-cache' : ''
|
||||
} --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}`
|
||||
});
|
||||
|
||||
const { status } = await prisma.build.findUnique({ where: { id: buildId } });
|
||||
if (status === 'canceled') {
|
||||
throw new Error('Canceled.');
|
||||
}
|
||||
}
|
||||
export function makeLabelForSimpleDockerfile({ applicationId, port, type }) {
|
||||
return [
|
||||
'coolify.managed=true',
|
||||
`coolify.version=${version}`,
|
||||
`coolify.applicationId=${applicationId}`,
|
||||
`coolify.type=standalone-application`
|
||||
];
|
||||
}
|
||||
export function makeLabelForStandaloneApplication({
|
||||
applicationId,
|
||||
fqdn,
|
||||
name,
|
||||
type,
|
||||
pullmergeRequestId = null,
|
||||
buildPack,
|
||||
repository,
|
||||
branch,
|
||||
projectId,
|
||||
port,
|
||||
commit,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
publishDirectory
|
||||
}) {
|
||||
if (pullmergeRequestId) {
|
||||
const protocol = fqdn.startsWith('https://') ? 'https' : 'http';
|
||||
const domain = getDomain(fqdn);
|
||||
fqdn = `${protocol}://${pullmergeRequestId}.${domain}`;
|
||||
}
|
||||
return [
|
||||
'coolify.managed=true',
|
||||
`coolify.version=${version}`,
|
||||
`coolify.applicationId=${applicationId}`,
|
||||
`coolify.type=standalone-application`,
|
||||
`coolify.name=${name}`,
|
||||
`coolify.configuration=${base64Encode(
|
||||
JSON.stringify({
|
||||
applicationId,
|
||||
fqdn,
|
||||
name,
|
||||
type,
|
||||
pullmergeRequestId,
|
||||
buildPack,
|
||||
repository,
|
||||
branch,
|
||||
projectId,
|
||||
port,
|
||||
commit,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
publishDirectory
|
||||
})
|
||||
)}`
|
||||
];
|
||||
}
|
||||
|
||||
export async function buildCacheImageWithNode(data, imageForBuild) {
|
||||
const {
|
||||
workdir,
|
||||
buildId,
|
||||
baseDirectory,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
secrets,
|
||||
pullmergeRequestId
|
||||
} = data;
|
||||
const isPnpm = checkPnpm(installCommand, buildCommand);
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${imageForBuild}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.push(env);
|
||||
});
|
||||
}
|
||||
if (isPnpm) {
|
||||
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
|
||||
}
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
if (installCommand) {
|
||||
Dockerfile.push(`RUN ${installCommand}`);
|
||||
}
|
||||
Dockerfile.push(`RUN ${buildCommand}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
||||
await buildImage({ ...data, isCache: true });
|
||||
}
|
||||
|
||||
export async function buildCacheImageForLaravel(data, imageForBuild) {
|
||||
const { workdir, buildId, secrets, pullmergeRequestId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${imageForBuild}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.push(env);
|
||||
});
|
||||
}
|
||||
Dockerfile.push(`COPY *.json *.mix.js /app/`);
|
||||
Dockerfile.push(`COPY resources /app/resources`);
|
||||
Dockerfile.push(`RUN yarn install && yarn production`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
||||
await buildImage({ ...data, isCache: true });
|
||||
}
|
||||
|
||||
export async function buildCacheImageWithCargo(data, imageForBuild) {
|
||||
const { applicationId, workdir, buildId } = data;
|
||||
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push('RUN cargo install cargo-chef');
|
||||
Dockerfile.push('COPY . .');
|
||||
Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json');
|
||||
Dockerfile.push(`FROM ${imageForBuild}`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push('RUN cargo install cargo-chef');
|
||||
Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
|
||||
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
|
||||
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
||||
await buildImage({ ...data, isCache: true });
|
||||
}
|
||||
137
apps/trpc-experimental/server/src/lib/buildPacks/compose.ts
Normal file
137
apps/trpc-experimental/server/src/lib/buildPacks/compose.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { saveBuildLog } from './common';
|
||||
import yaml from 'js-yaml';
|
||||
import { generateSecrets } from '../common';
|
||||
import { defaultComposeConfiguration } from '../docker';
|
||||
import { executeCommand } from '../executeCommand';
|
||||
|
||||
export default async function (data) {
|
||||
let {
|
||||
applicationId,
|
||||
debug,
|
||||
buildId,
|
||||
dockerId,
|
||||
network,
|
||||
volumes,
|
||||
labels,
|
||||
workdir,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId,
|
||||
dockerComposeConfiguration,
|
||||
dockerComposeFileLocation
|
||||
} = data;
|
||||
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`;
|
||||
const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8');
|
||||
const dockerComposeYaml = yaml.load(dockerComposeRaw);
|
||||
if (!dockerComposeYaml.services) {
|
||||
throw 'No Services found in docker-compose file.';
|
||||
}
|
||||
let envs = [];
|
||||
let buildEnvs = [];
|
||||
if (secrets.length > 0) {
|
||||
envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)];
|
||||
buildEnvs = [...buildEnvs, ...generateSecrets(secrets, pullmergeRequestId, true, null, true)];
|
||||
}
|
||||
|
||||
const composeVolumes = [];
|
||||
if (volumes.length > 0) {
|
||||
for (const volume of volumes) {
|
||||
let [v, path] = volume.split(':');
|
||||
composeVolumes[v] = {
|
||||
name: v
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let networks = {};
|
||||
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
|
||||
value['container_name'] = `${applicationId}-${key}`;
|
||||
|
||||
let environment = typeof value['environment'] === 'undefined' ? [] : value['environment'];
|
||||
if (Object.keys(environment).length > 0) {
|
||||
environment = Object.entries(environment).map(([key, value]) => `${key}=${value}`);
|
||||
}
|
||||
value['environment'] = [...environment, ...envs];
|
||||
|
||||
let build = typeof value['build'] === 'undefined' ? [] : value['build'];
|
||||
if (typeof build === 'string') {
|
||||
build = { context: build };
|
||||
}
|
||||
const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args'];
|
||||
let finalArgs = [...buildEnvs];
|
||||
if (Object.keys(buildArgs).length > 0) {
|
||||
for (const arg of buildArgs) {
|
||||
const [key, _] = arg.split('=');
|
||||
if (finalArgs.filter((env) => env.startsWith(key)).length === 0) {
|
||||
finalArgs.push(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
value['build'] = {
|
||||
...build,
|
||||
args: finalArgs
|
||||
};
|
||||
|
||||
value['labels'] = labels;
|
||||
// TODO: If we support separated volume for each service, we need to add it here
|
||||
if (value['volumes']?.length > 0) {
|
||||
value['volumes'] = value['volumes'].map((volume) => {
|
||||
let [v, path, permission] = volume.split(':');
|
||||
if (!path) {
|
||||
path = v;
|
||||
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
} else {
|
||||
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
}
|
||||
composeVolumes[v] = {
|
||||
name: v
|
||||
};
|
||||
return `${v}:${path}${permission ? ':' + permission : ''}`;
|
||||
});
|
||||
}
|
||||
if (volumes.length > 0) {
|
||||
for (const volume of volumes) {
|
||||
value['volumes'].push(volume);
|
||||
}
|
||||
}
|
||||
if (dockerComposeConfiguration[key].port) {
|
||||
value['expose'] = [dockerComposeConfiguration[key].port];
|
||||
}
|
||||
if (value['networks']?.length > 0) {
|
||||
value['networks'].forEach((network) => {
|
||||
networks[network] = {
|
||||
name: network
|
||||
};
|
||||
});
|
||||
}
|
||||
value['networks'] = [...(value['networks'] || ''), network];
|
||||
dockerComposeYaml.services[key] = {
|
||||
...dockerComposeYaml.services[key],
|
||||
restart: defaultComposeConfiguration(network).restart,
|
||||
deploy: defaultComposeConfiguration(network).deploy
|
||||
};
|
||||
}
|
||||
if (Object.keys(composeVolumes).length > 0) {
|
||||
dockerComposeYaml['volumes'] = { ...composeVolumes };
|
||||
}
|
||||
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } });
|
||||
|
||||
await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml));
|
||||
await executeCommand({
|
||||
debug,
|
||||
buildId,
|
||||
applicationId,
|
||||
dockerId,
|
||||
command: `docker compose --project-directory ${workdir} -f ${fileYaml} pull`
|
||||
});
|
||||
await saveBuildLog({ line: 'Pulling images from Compose file...', buildId, applicationId });
|
||||
await executeCommand({
|
||||
debug,
|
||||
buildId,
|
||||
applicationId,
|
||||
dockerId,
|
||||
command: `docker compose --project-directory ${workdir} -f ${fileYaml} build --progress plain`
|
||||
});
|
||||
await saveBuildLog({ line: 'Building images from Compose file...', buildId, applicationId });
|
||||
}
|
||||
52
apps/trpc-experimental/server/src/lib/buildPacks/deno.ts
Normal file
52
apps/trpc-experimental/server/src/lib/buildPacks/deno.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { generateSecrets } from '../common';
|
||||
import { buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const {
|
||||
workdir,
|
||||
port,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId,
|
||||
denoMainFile,
|
||||
denoOptions,
|
||||
buildId
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
let depsFound = false;
|
||||
try {
|
||||
await fs.readFile(`${workdir}${baseDirectory || ''}/deps.ts`);
|
||||
depsFound = true;
|
||||
} catch (error) {}
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.push(env);
|
||||
});
|
||||
}
|
||||
if (depsFound) {
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''}/deps.ts /app`);
|
||||
Dockerfile.push(`RUN deno cache deps.ts`);
|
||||
}
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
Dockerfile.push(`RUN deno cache ${denoMainFile}`);
|
||||
Dockerfile.push(`ENV NO_COLOR true`);
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
27
apps/trpc-experimental/server/src/lib/buildPacks/docker.ts
Normal file
27
apps/trpc-experimental/server/src/lib/buildPacks/docker.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { generateSecrets } from '../common';
|
||||
import { buildImage } from './common';
|
||||
|
||||
export default async function (data) {
|
||||
let { workdir, buildId, baseDirectory, secrets, pullmergeRequestId, dockerFileLocation } = data;
|
||||
const file = `${workdir}${baseDirectory}${dockerFileLocation}`;
|
||||
data.workdir = `${workdir}${baseDirectory}`;
|
||||
const DockerfileRaw = await fs.readFile(`${file}`, 'utf8');
|
||||
const Dockerfile: Array<string> = DockerfileRaw.toString().trim().split('\n');
|
||||
Dockerfile.forEach((line, index) => {
|
||||
if (line.startsWith('FROM')) {
|
||||
Dockerfile.splice(index + 1, 0, `LABEL coolify.buildId=${buildId}`);
|
||||
}
|
||||
});
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.forEach((line, index) => {
|
||||
if (line.startsWith('FROM')) {
|
||||
Dockerfile.splice(index + 1, 0, env);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
await fs.writeFile(`${data.workdir}${dockerFileLocation}`, Dockerfile.join('\n'));
|
||||
await buildImage(data);
|
||||
}
|
||||
28
apps/trpc-experimental/server/src/lib/buildPacks/gatsby.ts
Normal file
28
apps/trpc-experimental/server/src/lib/buildPacks/gatsby.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { buildCacheImageWithNode, buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, imageforBuild): Promise<void> => {
|
||||
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${imageforBuild}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
17
apps/trpc-experimental/server/src/lib/buildPacks/heroku.ts
Normal file
17
apps/trpc-experimental/server/src/lib/buildPacks/heroku.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { executeCommand } from "../executeCommand";
|
||||
import { saveBuildLog } from "./common";
|
||||
|
||||
export default async function (data: any): Promise<void> {
|
||||
const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory, baseImage } = data
|
||||
try {
|
||||
await saveBuildLog({ line: `Building production image...`, buildId, applicationId });
|
||||
await executeCommand({
|
||||
buildId,
|
||||
debug,
|
||||
dockerId,
|
||||
command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder ${baseImage}`
|
||||
})
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
41
apps/trpc-experimental/server/src/lib/buildPacks/index.ts
Normal file
41
apps/trpc-experimental/server/src/lib/buildPacks/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import node from './node';
|
||||
import staticApp from './static';
|
||||
import docker from './docker';
|
||||
import gatsby from './gatsby';
|
||||
import svelte from './svelte';
|
||||
import react from './react';
|
||||
import nestjs from './nestjs';
|
||||
import nextjs from './nextjs';
|
||||
import nuxtjs from './nuxtjs';
|
||||
import vuejs from './vuejs';
|
||||
import php from './php';
|
||||
import rust from './rust';
|
||||
import astro from './static';
|
||||
import eleventy from './static';
|
||||
import python from './python';
|
||||
import deno from './deno';
|
||||
import laravel from './laravel';
|
||||
import heroku from './heroku';
|
||||
import compose from './compose';
|
||||
|
||||
export {
|
||||
node,
|
||||
staticApp,
|
||||
docker,
|
||||
gatsby,
|
||||
svelte,
|
||||
react,
|
||||
nestjs,
|
||||
nextjs,
|
||||
nuxtjs,
|
||||
vuejs,
|
||||
php,
|
||||
rust,
|
||||
astro,
|
||||
eleventy,
|
||||
python,
|
||||
deno,
|
||||
laravel,
|
||||
heroku,
|
||||
compose
|
||||
};
|
||||
46
apps/trpc-experimental/server/src/lib/buildPacks/laravel.ts
Normal file
46
apps/trpc-experimental/server/src/lib/buildPacks/laravel.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { generateSecrets } from '../common';
|
||||
import { buildCacheImageForLaravel, buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { workdir, applicationId, tag, buildId, port, secrets, pullmergeRequestId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.push(env);
|
||||
});
|
||||
}
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`ENV WEB_DOCUMENT_ROOT /app/public`);
|
||||
Dockerfile.push(`COPY --chown=application:application composer.* ./`);
|
||||
Dockerfile.push(`COPY --chown=application:application database/ database/`);
|
||||
Dockerfile.push(
|
||||
`RUN composer install --ignore-platform-reqs --no-interaction --no-plugins --no-scripts --prefer-dist`
|
||||
);
|
||||
Dockerfile.push(
|
||||
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/public/js/ /app/public/js/`
|
||||
);
|
||||
Dockerfile.push(
|
||||
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/public/css/ /app/public/css/`
|
||||
);
|
||||
Dockerfile.push(
|
||||
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json`
|
||||
);
|
||||
Dockerfile.push(`COPY --chown=application:application . ./`);
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
try {
|
||||
await buildCacheImageForLaravel(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
31
apps/trpc-experimental/server/src/lib/buildPacks/nestjs.ts
Normal file
31
apps/trpc-experimental/server/src/lib/buildPacks/nestjs.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { buildCacheImageWithNode, buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { buildId, applicationId, tag, port, startCommand, workdir, baseDirectory } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
const isPnpm = startCommand.includes('pnpm');
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (isPnpm) {
|
||||
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
|
||||
}
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${baseDirectory || ''} ./`);
|
||||
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
Dockerfile.push(`CMD ${startCommand}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
66
apps/trpc-experimental/server/src/lib/buildPacks/nextjs.ts
Normal file
66
apps/trpc-experimental/server/src/lib/buildPacks/nextjs.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { generateSecrets } from '../common';
|
||||
import { buildCacheImageWithNode, buildImage, checkPnpm } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const {
|
||||
applicationId,
|
||||
buildId,
|
||||
tag,
|
||||
workdir,
|
||||
publishDirectory,
|
||||
port,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId,
|
||||
deploymentType,
|
||||
baseImage
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.push(env);
|
||||
});
|
||||
}
|
||||
if (isPnpm) {
|
||||
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
|
||||
}
|
||||
if (deploymentType === 'node') {
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
Dockerfile.push(`RUN ${installCommand}`);
|
||||
Dockerfile.push(`RUN ${buildCommand}`);
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
Dockerfile.push(`CMD ${startCommand}`);
|
||||
} else if (deploymentType === 'static') {
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
}
|
||||
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage, baseBuildImage, deploymentType, buildCommand } = data;
|
||||
if (deploymentType === 'node') {
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} else if (deploymentType === 'static') {
|
||||
if (buildCommand) await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
49
apps/trpc-experimental/server/src/lib/buildPacks/node.ts
Normal file
49
apps/trpc-experimental/server/src/lib/buildPacks/node.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { generateSecrets } from '../common';
|
||||
import { buildImage, checkPnpm } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const {
|
||||
workdir,
|
||||
port,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId,
|
||||
buildId
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.push(env);
|
||||
});
|
||||
}
|
||||
if (isPnpm) {
|
||||
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
|
||||
}
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
Dockerfile.push(`RUN ${installCommand}`);
|
||||
if (buildCommand) {
|
||||
Dockerfile.push(`RUN ${buildCommand}`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
Dockerfile.push(`CMD ${startCommand}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage } = data;
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
66
apps/trpc-experimental/server/src/lib/buildPacks/nuxtjs.ts
Normal file
66
apps/trpc-experimental/server/src/lib/buildPacks/nuxtjs.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { generateSecrets } from '../common';
|
||||
import { buildCacheImageWithNode, buildImage, checkPnpm } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const {
|
||||
applicationId,
|
||||
buildId,
|
||||
tag,
|
||||
workdir,
|
||||
publishDirectory,
|
||||
port,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId,
|
||||
deploymentType,
|
||||
baseImage
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.push(env);
|
||||
});
|
||||
}
|
||||
if (isPnpm) {
|
||||
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
|
||||
}
|
||||
if (deploymentType === 'node') {
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
Dockerfile.push(`RUN ${installCommand}`);
|
||||
Dockerfile.push(`RUN ${buildCommand}`);
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
Dockerfile.push(`CMD ${startCommand}`);
|
||||
} else if (deploymentType === 'static') {
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
}
|
||||
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage, baseBuildImage, deploymentType, buildCommand } = data;
|
||||
if (deploymentType === 'node') {
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} else if (deploymentType === 'static') {
|
||||
if (buildCommand) await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
50
apps/trpc-experimental/server/src/lib/buildPacks/php.ts
Normal file
50
apps/trpc-experimental/server/src/lib/buildPacks/php.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { generateSecrets } from '../common';
|
||||
import { buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
|
||||
const { workdir, baseDirectory, buildId, port, secrets, pullmergeRequestId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
let composerFound = false;
|
||||
try {
|
||||
await fs.readFile(`${workdir}${baseDirectory || ''}/composer.json`);
|
||||
composerFound = true;
|
||||
} catch (error) {}
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.push(env);
|
||||
});
|
||||
}
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} /app`);
|
||||
if (htaccessFound) {
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''}/.htaccess ./`);
|
||||
}
|
||||
if (composerFound) {
|
||||
Dockerfile.push(`RUN composer install`);
|
||||
}
|
||||
|
||||
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
const { workdir, baseDirectory, baseImage } = data;
|
||||
try {
|
||||
let htaccessFound = false;
|
||||
try {
|
||||
await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`);
|
||||
htaccessFound = true;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
await createDockerfile(data, baseImage, htaccessFound);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
67
apps/trpc-experimental/server/src/lib/buildPacks/python.ts
Normal file
67
apps/trpc-experimental/server/src/lib/buildPacks/python.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { generateSecrets } from '../common';
|
||||
import { buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const {
|
||||
workdir,
|
||||
port,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId,
|
||||
pythonWSGI,
|
||||
pythonModule,
|
||||
pythonVariable,
|
||||
buildId
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.push(env);
|
||||
});
|
||||
}
|
||||
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
|
||||
Dockerfile.push(`RUN pip install gunicorn`);
|
||||
} else if (pythonWSGI?.toLowerCase() === 'uvicorn') {
|
||||
Dockerfile.push(`RUN pip install uvicorn`);
|
||||
} else if (pythonWSGI?.toLowerCase() === 'uwsgi') {
|
||||
Dockerfile.push(`RUN apk add --no-cache uwsgi-python3`);
|
||||
// Dockerfile.push(`RUN pip install --no-cache-dir uwsgi`)
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.stat(`${workdir}${baseDirectory || ''}/requirements.txt`);
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''}/requirements.txt ./`);
|
||||
Dockerfile.push(`RUN pip install --no-cache-dir -r .${baseDirectory || ''}/requirements.txt`);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
|
||||
Dockerfile.push(`CMD gunicorn -w=4 -b=0.0.0.0:8000 ${pythonModule}:${pythonVariable}`);
|
||||
} else if (pythonWSGI?.toLowerCase() === 'uvicorn') {
|
||||
Dockerfile.push(`CMD uvicorn ${pythonModule}:${pythonVariable} --port ${port} --host 0.0.0.0`);
|
||||
} else if (pythonWSGI?.toLowerCase() === 'uwsgi') {
|
||||
Dockerfile.push(
|
||||
`CMD uwsgi --master -p 4 --http-socket 0.0.0.0:8000 --uid uwsgi --plugins python3 --protocol uwsgi --wsgi ${pythonModule}:${pythonVariable}`
|
||||
);
|
||||
} else {
|
||||
Dockerfile.push(`CMD python ${pythonModule}`);
|
||||
}
|
||||
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
28
apps/trpc-experimental/server/src/lib/buildPacks/react.ts
Normal file
28
apps/trpc-experimental/server/src/lib/buildPacks/react.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { buildCacheImageWithNode, buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
40
apps/trpc-experimental/server/src/lib/buildPacks/rust.ts
Normal file
40
apps/trpc-experimental/server/src/lib/buildPacks/rust.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import TOML from '@iarna/toml';
|
||||
import { buildCacheImageWithCargo, buildImage } from './common';
|
||||
import { executeCommand } from '../executeCommand';
|
||||
|
||||
const createDockerfile = async (data, image, name): Promise<void> => {
|
||||
const { workdir, port, applicationId, tag, buildId } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target target`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`);
|
||||
Dockerfile.push(`COPY . .`);
|
||||
Dockerfile.push(`RUN cargo build --release --bin ${name}`);
|
||||
Dockerfile.push('FROM debian:buster-slim');
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(
|
||||
`RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*`
|
||||
);
|
||||
Dockerfile.push(`RUN update-ca-certificates`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target/release/${name} ${name}`);
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
Dockerfile.push(`CMD ["/app/${name}"]`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { workdir, baseImage, baseBuildImage } = data;
|
||||
const { stdout: cargoToml } = await executeCommand({ command: `cat ${workdir}/Cargo.toml` });
|
||||
const parsedToml: any = TOML.parse(cargoToml);
|
||||
const name = parsedToml.package.name;
|
||||
await buildCacheImageWithCargo(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage, name);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
54
apps/trpc-experimental/server/src/lib/buildPacks/static.ts
Normal file
54
apps/trpc-experimental/server/src/lib/buildPacks/static.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { generateSecrets } from '../common';
|
||||
import { buildCacheImageWithNode, buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const {
|
||||
applicationId,
|
||||
tag,
|
||||
workdir,
|
||||
buildCommand,
|
||||
baseDirectory,
|
||||
publishDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId,
|
||||
baseImage,
|
||||
buildId,
|
||||
port
|
||||
} = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
if (baseImage?.includes('httpd')) {
|
||||
Dockerfile.push('WORKDIR /usr/local/apache2/htdocs/');
|
||||
} else {
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
}
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
if (secrets.length > 0) {
|
||||
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
|
||||
Dockerfile.push(env);
|
||||
});
|
||||
}
|
||||
if (buildCommand) {
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
} else {
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
}
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
if (data.buildCommand) await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
28
apps/trpc-experimental/server/src/lib/buildPacks/svelte.ts
Normal file
28
apps/trpc-experimental/server/src/lib/buildPacks/svelte.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { buildCacheImageWithNode, buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
28
apps/trpc-experimental/server/src/lib/buildPacks/vuejs.ts
Normal file
28
apps/trpc-experimental/server/src/lib/buildPacks/vuejs.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { buildCacheImageWithNode, buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
};
|
||||
|
||||
export default async function (data) {
|
||||
try {
|
||||
const { baseImage, baseBuildImage } = data;
|
||||
await buildCacheImageWithNode(data, baseBuildImage);
|
||||
await createDockerfile(data, baseImage);
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
939
apps/trpc-experimental/server/src/lib/common.ts
Normal file
939
apps/trpc-experimental/server/src/lib/common.ts
Normal file
@@ -0,0 +1,939 @@
|
||||
import type { Permission, Setting, Team, TeamInvitation, User } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import crypto from 'crypto';
|
||||
import { promises as dns } from 'dns';
|
||||
import fs from 'fs/promises';
|
||||
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
|
||||
import type { Config } from 'unique-names-generator';
|
||||
import { env } from '../env';
|
||||
import { day } from './dayjs';
|
||||
import { executeCommand } from './executeCommand';
|
||||
import { saveBuildLog } from './logging';
|
||||
import { checkContainer } from './docker';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
const customConfig: Config = {
|
||||
dictionaries: [adjectives, colors, animals],
|
||||
style: 'capital',
|
||||
separator: ' ',
|
||||
length: 3
|
||||
};
|
||||
const algorithm = 'aes-256-ctr';
|
||||
export const isDev = env.NODE_ENV === 'development';
|
||||
export const version = '3.13.0';
|
||||
export const sentryDSN =
|
||||
'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
|
||||
export const defaultTraefikImage = `traefik:v2.8`;
|
||||
export function getAPIUrl() {
|
||||
if (process.env.GITPOD_WORKSPACE_URL) {
|
||||
const { href } = new URL(process.env.GITPOD_WORKSPACE_URL);
|
||||
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
|
||||
return newURL;
|
||||
}
|
||||
if (process.env.CODESANDBOX_HOST) {
|
||||
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
|
||||
}
|
||||
return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000';
|
||||
}
|
||||
|
||||
export function getUIUrl() {
|
||||
if (process.env.GITPOD_WORKSPACE_URL) {
|
||||
const { href } = new URL(process.env.GITPOD_WORKSPACE_URL);
|
||||
const newURL = href.replace('https://', 'https://3000-').replace(/\/$/, '');
|
||||
return newURL;
|
||||
}
|
||||
if (process.env.CODESANDBOX_HOST) {
|
||||
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3000')}`;
|
||||
}
|
||||
return 'http://localhost:3000';
|
||||
}
|
||||
const mainTraefikEndpoint = isDev
|
||||
? `${getAPIUrl()}/webhooks/traefik/main.json`
|
||||
: 'http://coolify:3000/webhooks/traefik/main.json';
|
||||
|
||||
const otherTraefikEndpoint = isDev
|
||||
? `${getAPIUrl()}/webhooks/traefik/other.json`
|
||||
: 'http://coolify:3000/webhooks/traefik/other.json';
|
||||
|
||||
export async function listSettings(): Promise<Setting | null> {
|
||||
return await prisma.setting.findUnique({ where: { id: '0' } });
|
||||
}
|
||||
export async function getCurrentUser(
|
||||
userId: string
|
||||
): Promise<(User & { permission: Permission[]; teams: Team[] }) | null> {
|
||||
return await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { teams: true, permission: true }
|
||||
});
|
||||
}
|
||||
export async function getTeamInvitation(userId: string): Promise<TeamInvitation[]> {
|
||||
return await prisma.teamInvitation.findMany({ where: { uid: userId } });
|
||||
}
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 15;
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
export async function comparePassword(password: string, hashedPassword: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hashedPassword);
|
||||
}
|
||||
export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
|
||||
|
||||
export const decrypt = (hashString: string) => {
|
||||
if (hashString) {
|
||||
try {
|
||||
const hash = JSON.parse(hashString);
|
||||
const decipher = crypto.createDecipheriv(
|
||||
algorithm,
|
||||
env.COOLIFY_SECRET_KEY,
|
||||
Buffer.from(hash.iv, 'hex')
|
||||
);
|
||||
const decrpyted = Buffer.concat([
|
||||
decipher.update(Buffer.from(hash.content, 'hex')),
|
||||
decipher.final()
|
||||
]);
|
||||
return decrpyted.toString();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log({ decryptionError: error.message });
|
||||
}
|
||||
return hashString;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export function generateRangeArray(start: number, end: number) {
|
||||
return Array.from({ length: end - start }, (_v, k) => k + start);
|
||||
}
|
||||
export function generateTimestamp(): string {
|
||||
return `${day().format('HH:mm:ss.SSS')}`;
|
||||
}
|
||||
export const encrypt = (text: string) => {
|
||||
if (text) {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(algorithm, env.COOLIFY_SECRET_KEY, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]);
|
||||
return JSON.stringify({
|
||||
iv: iv.toString('hex'),
|
||||
content: encrypted.toString('hex')
|
||||
});
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export async function getTemplates() {
|
||||
const templatePath = isDev ? './templates.json' : '/app/templates.json';
|
||||
const open = await fs.open(templatePath, 'r');
|
||||
try {
|
||||
let data = await open.readFile({ encoding: 'utf-8' });
|
||||
let jsonData = JSON.parse(data);
|
||||
if (isARM(process.arch)) {
|
||||
jsonData = jsonData.filter((d: { arch: string }) => d.arch !== 'amd64');
|
||||
}
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
return [];
|
||||
} finally {
|
||||
await open?.close();
|
||||
}
|
||||
}
|
||||
export function isARM(arch: string) {
|
||||
if (arch === 'arm' || arch === 'arm64' || arch === 'aarch' || arch === 'aarch64') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function removeService({ id }: { id: string }): Promise<void> {
|
||||
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.serviceSetting.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.fider.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 } });
|
||||
await prisma.wordpress.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.glitchTip.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.moodle.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.appwrite.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.searxng.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.weblate.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.taiga.deleteMany({ where: { serviceId: id } });
|
||||
|
||||
await prisma.service.delete({ where: { id } });
|
||||
}
|
||||
|
||||
export const createDirectories = async ({
|
||||
repository,
|
||||
buildId
|
||||
}: {
|
||||
repository: string;
|
||||
buildId: string;
|
||||
}): Promise<{ workdir: string; repodir: string }> => {
|
||||
if (repository) repository = repository.replaceAll(' ', '');
|
||||
const repodir = `/tmp/build-sources/${repository}/`;
|
||||
const workdir = `/tmp/build-sources/${repository}/${buildId}`;
|
||||
let workdirFound = false;
|
||||
try {
|
||||
workdirFound = !!(await fs.stat(workdir));
|
||||
} catch (error) {}
|
||||
if (workdirFound) {
|
||||
await executeCommand({ command: `rm -fr ${workdir}` });
|
||||
}
|
||||
await executeCommand({ command: `mkdir -p ${workdir}` });
|
||||
return {
|
||||
workdir,
|
||||
repodir
|
||||
};
|
||||
};
|
||||
|
||||
export async function saveDockerRegistryCredentials({ url, username, password, workdir }) {
|
||||
if (!username || !password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let decryptedPassword = decrypt(password);
|
||||
const location = `${workdir}/.docker`;
|
||||
|
||||
try {
|
||||
await fs.mkdir(`${workdir}/.docker`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
const payload = JSON.stringify({
|
||||
auths: {
|
||||
[url]: {
|
||||
auth: Buffer.from(`${username}:${decryptedPassword}`).toString('base64')
|
||||
}
|
||||
}
|
||||
});
|
||||
await fs.writeFile(`${location}/config.json`, payload);
|
||||
return location;
|
||||
}
|
||||
export function getDomain(domain: string): string {
|
||||
if (domain) {
|
||||
return domain?.replace('https://', '').replace('http://', '');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function isDomainConfigured({
|
||||
id,
|
||||
fqdn,
|
||||
checkOwn = false,
|
||||
remoteIpAddress = undefined
|
||||
}: {
|
||||
id: string;
|
||||
fqdn: string;
|
||||
checkOwn?: boolean;
|
||||
remoteIpAddress?: string;
|
||||
}): Promise<boolean> {
|
||||
const domain = getDomain(fqdn);
|
||||
const nakedDomain = domain.replace('www.', '');
|
||||
const foundApp = await prisma.application.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ fqdn: { endsWith: `//${nakedDomain}` } },
|
||||
{ fqdn: { endsWith: `//www.${nakedDomain}` } },
|
||||
{ dockerComposeConfiguration: { contains: `//${nakedDomain}` } },
|
||||
{ dockerComposeConfiguration: { contains: `//www.${nakedDomain}` } }
|
||||
],
|
||||
id: { not: id },
|
||||
destinationDocker: {
|
||||
remoteIpAddress
|
||||
}
|
||||
},
|
||||
select: { fqdn: true }
|
||||
});
|
||||
const foundService = await prisma.service.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ fqdn: { endsWith: `//${nakedDomain}` } },
|
||||
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
|
||||
],
|
||||
id: { not: checkOwn ? undefined : id },
|
||||
destinationDocker: {
|
||||
remoteIpAddress
|
||||
}
|
||||
},
|
||||
select: { fqdn: true }
|
||||
});
|
||||
|
||||
const coolifyFqdn = await prisma.setting.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ fqdn: { endsWith: `//${nakedDomain}` } },
|
||||
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
|
||||
],
|
||||
id: { not: id }
|
||||
},
|
||||
select: { fqdn: true }
|
||||
});
|
||||
return !!(foundApp || foundService || coolifyFqdn);
|
||||
}
|
||||
|
||||
export async function checkExposedPort({
|
||||
id,
|
||||
configuredPort,
|
||||
exposePort,
|
||||
engine,
|
||||
remoteEngine,
|
||||
remoteIpAddress
|
||||
}: {
|
||||
id: string;
|
||||
configuredPort?: number;
|
||||
exposePort: number;
|
||||
engine: string;
|
||||
remoteEngine: boolean;
|
||||
remoteIpAddress?: string;
|
||||
}) {
|
||||
if (exposePort < 1024 || exposePort > 65535) {
|
||||
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` };
|
||||
}
|
||||
if (configuredPort) {
|
||||
if (configuredPort !== exposePort) {
|
||||
const availablePort = await getFreeExposedPort(
|
||||
id,
|
||||
exposePort,
|
||||
engine,
|
||||
remoteEngine,
|
||||
remoteIpAddress
|
||||
);
|
||||
if (availablePort.toString() !== exposePort.toString()) {
|
||||
throw { status: 500, message: `Port ${exposePort} is already in use.` };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const availablePort = await getFreeExposedPort(
|
||||
id,
|
||||
exposePort,
|
||||
engine,
|
||||
remoteEngine,
|
||||
remoteIpAddress
|
||||
);
|
||||
if (availablePort.toString() !== exposePort.toString()) {
|
||||
throw { status: 500, message: `Port ${exposePort} is already in use.` };
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress) {
|
||||
const { default: checkPort } = await import('is-port-reachable');
|
||||
if (remoteEngine) {
|
||||
const applicationUsed = await (
|
||||
await prisma.application.findMany({
|
||||
where: {
|
||||
exposePort: { not: null },
|
||||
id: { not: id },
|
||||
destinationDocker: { remoteIpAddress }
|
||||
},
|
||||
select: { exposePort: true }
|
||||
})
|
||||
).map((a) => a.exposePort);
|
||||
const serviceUsed = await (
|
||||
await prisma.service.findMany({
|
||||
where: {
|
||||
exposePort: { not: null },
|
||||
id: { not: id },
|
||||
destinationDocker: { remoteIpAddress }
|
||||
},
|
||||
select: { exposePort: true }
|
||||
})
|
||||
).map((a) => a.exposePort);
|
||||
const usedPorts = [...applicationUsed, ...serviceUsed];
|
||||
if (usedPorts.includes(exposePort)) {
|
||||
return false;
|
||||
}
|
||||
const found = await checkPort(exposePort, { host: remoteIpAddress });
|
||||
if (!found) {
|
||||
return exposePort;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
const applicationUsed = await (
|
||||
await prisma.application.findMany({
|
||||
where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } },
|
||||
select: { exposePort: true }
|
||||
})
|
||||
).map((a) => a.exposePort);
|
||||
const serviceUsed = await (
|
||||
await prisma.service.findMany({
|
||||
where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } },
|
||||
select: { exposePort: true }
|
||||
})
|
||||
).map((a) => a.exposePort);
|
||||
const usedPorts = [...applicationUsed, ...serviceUsed];
|
||||
if (usedPorts.includes(exposePort)) {
|
||||
return false;
|
||||
}
|
||||
const found = await checkPort(exposePort, { host: 'localhost' });
|
||||
if (!found) {
|
||||
return exposePort;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): Promise<any> {
|
||||
const { isIP } = await import('is-ip');
|
||||
const domain = getDomain(fqdn);
|
||||
const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`;
|
||||
|
||||
const { DNSServers } = await listSettings();
|
||||
if (DNSServers) {
|
||||
dns.setServers([...DNSServers.split(',')]);
|
||||
}
|
||||
|
||||
let resolves = [];
|
||||
try {
|
||||
if (isIP(hostname)) {
|
||||
resolves = [hostname];
|
||||
} else {
|
||||
resolves = await dns.resolve4(hostname);
|
||||
}
|
||||
} catch (error) {
|
||||
throw { status: 500, message: `Could not determine IP address for ${hostname}.` };
|
||||
}
|
||||
|
||||
if (dualCerts) {
|
||||
try {
|
||||
const ipDomain = await dns.resolve4(domain);
|
||||
const ipDomainDualCert = await dns.resolve4(domainDualCert);
|
||||
|
||||
let ipDomainFound = false;
|
||||
let ipDomainDualCertFound = false;
|
||||
|
||||
for (const ip of ipDomain) {
|
||||
if (resolves.includes(ip)) {
|
||||
ipDomainFound = true;
|
||||
}
|
||||
}
|
||||
for (const ip of ipDomainDualCert) {
|
||||
if (resolves.includes(ip)) {
|
||||
ipDomainDualCertFound = true;
|
||||
}
|
||||
}
|
||||
if (ipDomainFound && ipDomainDualCertFound) return { status: 200 };
|
||||
throw {
|
||||
status: 500,
|
||||
message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
|
||||
};
|
||||
} catch (error) {
|
||||
throw {
|
||||
status: 500,
|
||||
message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
|
||||
};
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const ipDomain = await dns.resolve4(domain);
|
||||
let ipDomainFound = false;
|
||||
for (const ip of ipDomain) {
|
||||
if (resolves.includes(ip)) {
|
||||
ipDomainFound = true;
|
||||
}
|
||||
}
|
||||
if (ipDomainFound) return { status: 200 };
|
||||
throw {
|
||||
status: 500,
|
||||
message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
|
||||
};
|
||||
} catch (error) {
|
||||
throw {
|
||||
status: 500,
|
||||
message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
export const setDefaultConfiguration = async (data: any) => {
|
||||
let {
|
||||
buildPack,
|
||||
port,
|
||||
installCommand,
|
||||
startCommand,
|
||||
buildCommand,
|
||||
publishDirectory,
|
||||
baseDirectory,
|
||||
dockerFileLocation,
|
||||
dockerComposeFileLocation,
|
||||
denoMainFile
|
||||
} = data;
|
||||
//@ts-ignore
|
||||
const template = scanningTemplates[buildPack];
|
||||
if (!port) {
|
||||
port = template?.port || 3000;
|
||||
|
||||
if (buildPack === 'static') port = 80;
|
||||
else if (buildPack === 'node') port = 3000;
|
||||
else if (buildPack === 'php') port = 80;
|
||||
else if (buildPack === 'python') port = 8000;
|
||||
}
|
||||
if (!installCommand && buildPack !== 'static' && buildPack !== 'laravel')
|
||||
installCommand = template?.installCommand || 'yarn install';
|
||||
if (!startCommand && buildPack !== 'static' && buildPack !== 'laravel')
|
||||
startCommand = template?.startCommand || 'yarn start';
|
||||
if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
|
||||
buildCommand = template?.buildCommand || null;
|
||||
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
|
||||
if (baseDirectory) {
|
||||
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
|
||||
if (baseDirectory.endsWith('/') && baseDirectory !== '/')
|
||||
baseDirectory = baseDirectory.slice(0, -1);
|
||||
}
|
||||
if (dockerFileLocation) {
|
||||
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
|
||||
if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1);
|
||||
} else {
|
||||
dockerFileLocation = '/Dockerfile';
|
||||
}
|
||||
if (dockerComposeFileLocation) {
|
||||
if (!dockerComposeFileLocation.startsWith('/'))
|
||||
dockerComposeFileLocation = `/${dockerComposeFileLocation}`;
|
||||
if (dockerComposeFileLocation.endsWith('/'))
|
||||
dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1);
|
||||
} else {
|
||||
dockerComposeFileLocation = '/Dockerfile';
|
||||
}
|
||||
if (!denoMainFile) {
|
||||
denoMainFile = 'main.ts';
|
||||
}
|
||||
|
||||
return {
|
||||
buildPack,
|
||||
port,
|
||||
installCommand,
|
||||
startCommand,
|
||||
buildCommand,
|
||||
publishDirectory,
|
||||
baseDirectory,
|
||||
dockerFileLocation,
|
||||
dockerComposeFileLocation,
|
||||
denoMainFile
|
||||
};
|
||||
};
|
||||
|
||||
export const scanningTemplates = {
|
||||
'@sveltejs/kit': {
|
||||
buildPack: 'nodejs'
|
||||
},
|
||||
astro: {
|
||||
buildPack: 'astro'
|
||||
},
|
||||
'@11ty/eleventy': {
|
||||
buildPack: 'eleventy'
|
||||
},
|
||||
svelte: {
|
||||
buildPack: 'svelte'
|
||||
},
|
||||
'@nestjs/core': {
|
||||
buildPack: 'nestjs'
|
||||
},
|
||||
next: {
|
||||
buildPack: 'nextjs'
|
||||
},
|
||||
nuxt: {
|
||||
buildPack: 'nuxtjs'
|
||||
},
|
||||
'react-scripts': {
|
||||
buildPack: 'react'
|
||||
},
|
||||
'parcel-bundler': {
|
||||
buildPack: 'static'
|
||||
},
|
||||
'@vue/cli-service': {
|
||||
buildPack: 'vuejs'
|
||||
},
|
||||
vuejs: {
|
||||
buildPack: 'vuejs'
|
||||
},
|
||||
gatsby: {
|
||||
buildPack: 'gatsby'
|
||||
},
|
||||
'preact-cli': {
|
||||
buildPack: 'react'
|
||||
}
|
||||
};
|
||||
|
||||
export async function cleanupDB(buildId: string, applicationId: string) {
|
||||
const data = await prisma.build.findUnique({ where: { id: buildId } });
|
||||
if (data?.status === 'queued' || data?.status === 'running') {
|
||||
await prisma.build.update({ where: { id: buildId }, data: { status: 'canceled' } });
|
||||
}
|
||||
await saveBuildLog({ line: 'Canceled.', buildId, applicationId });
|
||||
}
|
||||
|
||||
export const base64Encode = (text: string): string => {
|
||||
return Buffer.from(text).toString('base64');
|
||||
};
|
||||
export const base64Decode = (text: string): string => {
|
||||
return Buffer.from(text, 'base64').toString('ascii');
|
||||
};
|
||||
function parseSecret(secret, isBuild) {
|
||||
if (secret.value.includes('$')) {
|
||||
secret.value = secret.value.replaceAll('$', '$$$$');
|
||||
}
|
||||
if (secret.value.includes('\\n')) {
|
||||
if (isBuild) {
|
||||
return `ARG ${secret.name}=${secret.value}`;
|
||||
} else {
|
||||
return `${secret.name}=${secret.value}`;
|
||||
}
|
||||
} else if (secret.value.includes(' ')) {
|
||||
if (isBuild) {
|
||||
return `ARG ${secret.name}='${secret.value}'`;
|
||||
} else {
|
||||
return `${secret.name}='${secret.value}'`;
|
||||
}
|
||||
} else {
|
||||
if (isBuild) {
|
||||
return `ARG ${secret.name}=${secret.value}`;
|
||||
} else {
|
||||
return `${secret.name}=${secret.value}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
export function generateSecrets(
|
||||
secrets: Array<any>,
|
||||
pullmergeRequestId: string,
|
||||
isBuild = false,
|
||||
port = null,
|
||||
compose = false
|
||||
): Array<string> {
|
||||
const envs = [];
|
||||
const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret);
|
||||
const normalSecrets = secrets.filter((s) => !s.isPRMRSecret);
|
||||
if (pullmergeRequestId && isPRMRSecret.length > 0) {
|
||||
isPRMRSecret.forEach((secret) => {
|
||||
if ((isBuild && !secret.isBuildSecret) || (!isBuild && secret.isBuildSecret)) {
|
||||
return;
|
||||
}
|
||||
const build = isBuild && secret.isBuildSecret;
|
||||
envs.push(parseSecret(secret, compose ? false : build));
|
||||
});
|
||||
}
|
||||
if (!pullmergeRequestId && normalSecrets.length > 0) {
|
||||
normalSecrets.forEach((secret) => {
|
||||
if ((isBuild && !secret.isBuildSecret) || (!isBuild && secret.isBuildSecret)) {
|
||||
return;
|
||||
}
|
||||
const build = isBuild && secret.isBuildSecret;
|
||||
envs.push(parseSecret(secret, compose ? false : build));
|
||||
});
|
||||
}
|
||||
const portFound = envs.filter((env) => env.startsWith('PORT'));
|
||||
if (portFound.length === 0 && port && !isBuild) {
|
||||
envs.push(`PORT=${port}`);
|
||||
}
|
||||
const nodeEnv = envs.filter((env) => env.startsWith('NODE_ENV'));
|
||||
if (nodeEnv.length === 0 && !isBuild) {
|
||||
envs.push(`NODE_ENV=production`);
|
||||
}
|
||||
return envs;
|
||||
}
|
||||
export function decryptApplication(application: any) {
|
||||
if (application) {
|
||||
if (application?.gitSource?.githubApp?.clientSecret) {
|
||||
application.gitSource.githubApp.clientSecret =
|
||||
decrypt(application.gitSource.githubApp.clientSecret) || null;
|
||||
}
|
||||
if (application?.gitSource?.githubApp?.webhookSecret) {
|
||||
application.gitSource.githubApp.webhookSecret =
|
||||
decrypt(application.gitSource.githubApp.webhookSecret) || null;
|
||||
}
|
||||
if (application?.gitSource?.githubApp?.privateKey) {
|
||||
application.gitSource.githubApp.privateKey =
|
||||
decrypt(application.gitSource.githubApp.privateKey) || null;
|
||||
}
|
||||
if (application?.gitSource?.gitlabApp?.appSecret) {
|
||||
application.gitSource.gitlabApp.appSecret =
|
||||
decrypt(application.gitSource.gitlabApp.appSecret) || null;
|
||||
}
|
||||
if (application?.secrets.length > 0) {
|
||||
application.secrets = application.secrets.map((s: any) => {
|
||||
s.value = decrypt(s.value) || null;
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
return application;
|
||||
}
|
||||
}
|
||||
export async function pushToRegistry(
|
||||
application: any,
|
||||
workdir: string,
|
||||
tag: string,
|
||||
imageName: string,
|
||||
customTag: string
|
||||
) {
|
||||
const location = `${workdir}/.docker`;
|
||||
const tagCommand = `docker tag ${application.id}:${tag} ${imageName}:${customTag}`;
|
||||
const pushCommand = `docker --config ${location} push ${imageName}:${customTag}`;
|
||||
await executeCommand({
|
||||
dockerId: application.destinationDockerId,
|
||||
command: tagCommand
|
||||
});
|
||||
await executeCommand({
|
||||
dockerId: application.destinationDockerId,
|
||||
command: pushCommand
|
||||
});
|
||||
}
|
||||
|
||||
export async function getContainerUsage(dockerId: string, container: string): Promise<any> {
|
||||
try {
|
||||
const { stdout } = await executeCommand({
|
||||
dockerId,
|
||||
command: `docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"`
|
||||
});
|
||||
return JSON.parse(stdout);
|
||||
} catch (err) {
|
||||
return {
|
||||
MemUsage: 0,
|
||||
CPUPerc: 0,
|
||||
NetIO: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
export function fixType(type) {
|
||||
return type?.replaceAll(' ', '').toLowerCase() || null;
|
||||
}
|
||||
const compareSemanticVersions = (a: string, b: string) => {
|
||||
const a1 = a.split('.');
|
||||
const b1 = b.split('.');
|
||||
const len = Math.min(a1.length, b1.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const a2 = +a1[i] || 0;
|
||||
const b2 = +b1[i] || 0;
|
||||
if (a2 !== b2) {
|
||||
return a2 > b2 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
return b1.length - a1.length;
|
||||
};
|
||||
export async function getTags(type: string) {
|
||||
try {
|
||||
if (type) {
|
||||
const tagsPath = isDev ? './tags.json' : '/app/tags.json';
|
||||
const data = await fs.readFile(tagsPath, 'utf8');
|
||||
let tags = JSON.parse(data);
|
||||
if (tags) {
|
||||
tags = tags.find((tag: any) => tag.name.includes(type));
|
||||
tags.tags = tags.tags.sort(compareSemanticVersions).reverse();
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
export function makeLabelForServices(type) {
|
||||
return [
|
||||
'coolify.managed=true',
|
||||
`coolify.version=${version}`,
|
||||
`coolify.type=service`,
|
||||
`coolify.service.type=${type}`
|
||||
];
|
||||
}
|
||||
export const asyncSleep = (delay: number): Promise<unknown> =>
|
||||
new Promise((resolve) => setTimeout(resolve, delay));
|
||||
|
||||
export async function startTraefikTCPProxy(
|
||||
destinationDocker: any,
|
||||
id: string,
|
||||
publicPort: number,
|
||||
privatePort: number,
|
||||
type?: string
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
const { network, id: dockerId, remoteEngine } = destinationDocker;
|
||||
const container = `${id}-${publicPort}`;
|
||||
const { found } = await checkContainer({ dockerId, container, remove: true });
|
||||
const { ipv4, ipv6 } = await listSettings();
|
||||
|
||||
let dependentId = id;
|
||||
if (type === 'wordpressftp') dependentId = `${id}-ftp`;
|
||||
const { found: foundDependentContainer } = await checkContainer({
|
||||
dockerId,
|
||||
container: dependentId,
|
||||
remove: true
|
||||
});
|
||||
if (foundDependentContainer && !found) {
|
||||
const { stdout: Config } = await executeCommand({
|
||||
dockerId,
|
||||
command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'`
|
||||
});
|
||||
|
||||
const ip = JSON.parse(Config)[0].Gateway;
|
||||
let traefikUrl = otherTraefikEndpoint;
|
||||
if (remoteEngine) {
|
||||
let ip = null;
|
||||
if (isDev) {
|
||||
ip = getAPIUrl();
|
||||
} else {
|
||||
ip = `http://${ipv4 || ipv6}:3000`;
|
||||
}
|
||||
traefikUrl = `${ip}/webhooks/traefik/other.json`;
|
||||
}
|
||||
const tcpProxy = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[`${id}-${publicPort}`]: {
|
||||
container_name: container,
|
||||
image: defaultTraefikImage,
|
||||
command: [
|
||||
`--entrypoints.tcp.address=:${publicPort}`,
|
||||
`--entryPoints.tcp.forwardedHeaders.insecure=true`,
|
||||
`--providers.http.endpoint=${traefikUrl}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`,
|
||||
'--providers.http.pollTimeout=10s',
|
||||
'--log.level=error'
|
||||
],
|
||||
ports: [`${publicPort}:${publicPort}`],
|
||||
extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal: ${ip}`],
|
||||
volumes: ['/var/run/docker.sock:/var/run/docker.sock'],
|
||||
networks: ['coolify-infra', network]
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: false,
|
||||
name: network
|
||||
},
|
||||
'coolify-infra': {
|
||||
external: false,
|
||||
name: 'coolify-infra'
|
||||
}
|
||||
}
|
||||
};
|
||||
await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy));
|
||||
await executeCommand({
|
||||
dockerId,
|
||||
command: `docker compose -f /tmp/docker-compose-${id}.yaml up -d`
|
||||
});
|
||||
await fs.rm(`/tmp/docker-compose-${id}.yaml`);
|
||||
}
|
||||
if (!foundDependentContainer && found) {
|
||||
await executeCommand({
|
||||
dockerId,
|
||||
command: `docker stop -t 0 ${container} && docker rm ${container}`,
|
||||
shell: true
|
||||
});
|
||||
}
|
||||
}
|
||||
export async function startTraefikProxy(id: string): Promise<void> {
|
||||
const { engine, network, remoteEngine, remoteIpAddress } =
|
||||
await prisma.destinationDocker.findUnique({ where: { id } });
|
||||
const { found } = await checkContainer({
|
||||
dockerId: id,
|
||||
container: 'coolify-proxy',
|
||||
remove: true
|
||||
});
|
||||
const { id: settingsId, ipv4, ipv6 } = await listSettings();
|
||||
|
||||
if (!found) {
|
||||
const { stdout: coolifyNetwork } = await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`
|
||||
});
|
||||
|
||||
if (!coolifyNetwork) {
|
||||
await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker network create --attachable coolify-infra`
|
||||
});
|
||||
}
|
||||
const { stdout: Config } = await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'`
|
||||
});
|
||||
const ip = JSON.parse(Config)[0].Gateway;
|
||||
let traefikUrl = mainTraefikEndpoint;
|
||||
if (remoteEngine) {
|
||||
let ip = null;
|
||||
if (isDev) {
|
||||
ip = getAPIUrl();
|
||||
} else {
|
||||
ip = `http://${ipv4 || ipv6}:3000`;
|
||||
}
|
||||
traefikUrl = `${ip}/webhooks/traefik/remote/${id}`;
|
||||
}
|
||||
await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker run --restart always \
|
||||
--add-host 'host.docker.internal:host-gateway' \
|
||||
${ip ? `--add-host 'host.docker.internal:${ip}'` : ''} \
|
||||
-v coolify-traefik-letsencrypt:/etc/traefik/acme \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--network coolify-infra \
|
||||
-p "80:80" \
|
||||
-p "443:443" \
|
||||
--name coolify-proxy \
|
||||
-d ${defaultTraefikImage} \
|
||||
--entrypoints.web.address=:80 \
|
||||
--entrypoints.web.forwardedHeaders.insecure=true \
|
||||
--entrypoints.websecure.address=:443 \
|
||||
--entrypoints.websecure.forwardedHeaders.insecure=true \
|
||||
--providers.docker=true \
|
||||
--providers.docker.exposedbydefault=false \
|
||||
--providers.http.endpoint=${traefikUrl} \
|
||||
--providers.http.pollTimeout=5s \
|
||||
--certificatesresolvers.letsencrypt.acme.httpchallenge=true \
|
||||
--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \
|
||||
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \
|
||||
--log.level=error`
|
||||
});
|
||||
await prisma.destinationDocker.update({
|
||||
where: { id },
|
||||
data: { isCoolifyProxyUsed: true }
|
||||
});
|
||||
}
|
||||
// Configure networks for local docker engine
|
||||
if (engine) {
|
||||
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
|
||||
for (const destination of destinations) {
|
||||
await configureNetworkTraefikProxy(destination);
|
||||
}
|
||||
}
|
||||
// Configure networks for remote docker engine
|
||||
if (remoteEngine) {
|
||||
const destinations = await prisma.destinationDocker.findMany({ where: { remoteIpAddress } });
|
||||
for (const destination of destinations) {
|
||||
await configureNetworkTraefikProxy(destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureNetworkTraefikProxy(destination: any): Promise<void> {
|
||||
const { id } = destination;
|
||||
const { stdout: networks } = await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'`
|
||||
});
|
||||
const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(',');
|
||||
if (!configuredNetworks.includes(destination.network)) {
|
||||
await executeCommand({
|
||||
dockerId: destination.id,
|
||||
command: `docker network connect ${destination.network} coolify-proxy`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopTraefikProxy(id: string): Promise<{ stdout: string; stderr: string }> {
|
||||
const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy' });
|
||||
await prisma.destinationDocker.update({
|
||||
where: { id },
|
||||
data: { isCoolifyProxyUsed: false }
|
||||
});
|
||||
if (found) {
|
||||
return await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy`,
|
||||
shell: true
|
||||
});
|
||||
}
|
||||
return { stdout: '', stderr: '' };
|
||||
}
|
||||
7
apps/trpc-experimental/server/src/lib/dayjs.ts
Normal file
7
apps/trpc-experimental/server/src/lib/dayjs.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc.js';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime.js';
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export { dayjs as day };
|
||||
157
apps/trpc-experimental/server/src/lib/docker.ts
Normal file
157
apps/trpc-experimental/server/src/lib/docker.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { executeCommand } from './executeCommand';
|
||||
|
||||
export async function checkContainer({
|
||||
dockerId,
|
||||
container,
|
||||
remove = false
|
||||
}: {
|
||||
dockerId: string;
|
||||
container: string;
|
||||
remove?: boolean;
|
||||
}): Promise<{
|
||||
found: boolean;
|
||||
status?: { isExited: boolean; isRunning: boolean; isRestarting: boolean };
|
||||
}> {
|
||||
let containerFound = false;
|
||||
try {
|
||||
const { stdout } = await executeCommand({
|
||||
dockerId,
|
||||
command: `docker inspect --format '{{json .State}}' ${container}`
|
||||
});
|
||||
containerFound = true;
|
||||
const parsedStdout = JSON.parse(stdout);
|
||||
const status = parsedStdout.Status;
|
||||
const isRunning = status === 'running';
|
||||
const isRestarting = status === 'restarting';
|
||||
const isExited = status === 'exited';
|
||||
if (status === 'created') {
|
||||
await executeCommand({
|
||||
dockerId,
|
||||
command: `docker rm ${container}`
|
||||
});
|
||||
}
|
||||
if (remove && status === 'exited') {
|
||||
await executeCommand({
|
||||
dockerId,
|
||||
command: `docker rm ${container}`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
found: containerFound,
|
||||
status: {
|
||||
isRunning,
|
||||
isRestarting,
|
||||
isExited
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
// Container not found
|
||||
}
|
||||
return {
|
||||
found: false
|
||||
};
|
||||
}
|
||||
|
||||
export async function removeContainer({
|
||||
id,
|
||||
dockerId
|
||||
}: {
|
||||
id: string;
|
||||
dockerId: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const { stdout } = await executeCommand({
|
||||
dockerId,
|
||||
command: `docker inspect --format '{{json .State}}' ${id}`
|
||||
});
|
||||
if (JSON.parse(stdout).Running) {
|
||||
await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` });
|
||||
await executeCommand({ dockerId, command: `docker rm ${id}` });
|
||||
}
|
||||
if (JSON.parse(stdout).Status === 'exited') {
|
||||
await executeCommand({ dockerId, command: `docker rm ${id}` });
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopDatabaseContainer(database: any): Promise<boolean> {
|
||||
let everStarted = false;
|
||||
const {
|
||||
id,
|
||||
destinationDockerId,
|
||||
destinationDocker: { engine, id: dockerId }
|
||||
} = database;
|
||||
if (destinationDockerId) {
|
||||
try {
|
||||
const { stdout } = await executeCommand({
|
||||
dockerId,
|
||||
command: `docker inspect --format '{{json .State}}' ${id}`
|
||||
});
|
||||
|
||||
if (stdout) {
|
||||
everStarted = true;
|
||||
await removeContainer({ id, dockerId });
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
return everStarted;
|
||||
}
|
||||
export async function stopTcpHttpProxy(
|
||||
id: string,
|
||||
destinationDocker: any,
|
||||
publicPort: number,
|
||||
forceName: string | null = null
|
||||
): Promise<{ stdout: string; stderr: string } | Error | unknown> {
|
||||
const { id: dockerId } = destinationDocker;
|
||||
let container = `${id}-${publicPort}`;
|
||||
if (forceName) container = forceName;
|
||||
const { found } = await checkContainer({ dockerId, container });
|
||||
try {
|
||||
if (!found) return true;
|
||||
return await executeCommand({
|
||||
dockerId,
|
||||
command: `docker stop -t 0 ${container} && docker rm ${container}`,
|
||||
shell: true
|
||||
});
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatLabelsOnDocker(data: any) {
|
||||
return data
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((a) => JSON.parse(a))
|
||||
.map((container) => {
|
||||
const labels = container.Labels.split(',');
|
||||
let jsonLabels = {};
|
||||
labels.forEach((l) => {
|
||||
const name = l.split('=')[0];
|
||||
const value = l.split('=')[1];
|
||||
jsonLabels = { ...jsonLabels, ...{ [name]: value } };
|
||||
});
|
||||
container.Labels = jsonLabels;
|
||||
return container;
|
||||
});
|
||||
}
|
||||
|
||||
export function defaultComposeConfiguration(network: string): any {
|
||||
return {
|
||||
networks: [network],
|
||||
restart: 'on-failure',
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 10,
|
||||
window: '120s'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
196
apps/trpc-experimental/server/src/lib/executeCommand.ts
Normal file
196
apps/trpc-experimental/server/src/lib/executeCommand.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { prisma } from '../prisma';
|
||||
import os from 'os';
|
||||
import fs from 'fs/promises';
|
||||
import type { ExecaChildProcess } from 'execa';
|
||||
import sshConfig from 'ssh-config';
|
||||
|
||||
import { getFreeSSHLocalPort } from './ssh';
|
||||
import { env } from '../env';
|
||||
import { BuildLog, saveBuildLog } from './logging';
|
||||
import { decrypt } from './common';
|
||||
|
||||
export async function executeCommand({
|
||||
command,
|
||||
dockerId = null,
|
||||
sshCommand = false,
|
||||
shell = false,
|
||||
stream = false,
|
||||
buildId,
|
||||
applicationId,
|
||||
debug
|
||||
}: {
|
||||
command: string;
|
||||
sshCommand?: boolean;
|
||||
shell?: boolean;
|
||||
stream?: boolean;
|
||||
dockerId?: string | null;
|
||||
buildId?: string;
|
||||
applicationId?: string;
|
||||
debug?: boolean;
|
||||
}): Promise<ExecaChildProcess<string>> {
|
||||
const { execa, execaCommand } = await import('execa');
|
||||
const { parse } = await import('shell-quote');
|
||||
const parsedCommand = parse(command);
|
||||
const dockerCommand = parsedCommand[0];
|
||||
const dockerArgs = parsedCommand.slice(1);
|
||||
|
||||
if (dockerId && dockerCommand && dockerArgs) {
|
||||
const destinationDocker = await prisma.destinationDocker.findUnique({
|
||||
where: { id: dockerId }
|
||||
});
|
||||
if (!destinationDocker) {
|
||||
throw new Error('Destination docker not found');
|
||||
}
|
||||
let { remoteEngine, remoteIpAddress, engine } = destinationDocker;
|
||||
if (remoteEngine) {
|
||||
await createRemoteEngineConfiguration(dockerId);
|
||||
engine = `ssh://${remoteIpAddress}-remote`;
|
||||
} else {
|
||||
engine = 'unix:///var/run/docker.sock';
|
||||
}
|
||||
|
||||
if (env.CODESANDBOX_HOST) {
|
||||
if (command.startsWith('docker compose')) {
|
||||
command = command.replace(/docker compose/gi, 'docker-compose');
|
||||
}
|
||||
}
|
||||
if (sshCommand) {
|
||||
if (shell) {
|
||||
return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`);
|
||||
}
|
||||
//@ts-ignore
|
||||
return await execa('ssh', [`${remoteIpAddress}-remote`, dockerCommand, ...dockerArgs]);
|
||||
}
|
||||
if (stream) {
|
||||
return await new Promise(async (resolve, reject) => {
|
||||
let subprocess = null;
|
||||
if (shell) {
|
||||
//@ts-ignore
|
||||
subprocess = execaCommand(command, {
|
||||
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
|
||||
});
|
||||
} else {
|
||||
//@ts-ignore
|
||||
subprocess = execa(dockerCommand, dockerArgs, {
|
||||
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
|
||||
});
|
||||
}
|
||||
const logs: any[] = [];
|
||||
if (subprocess && subprocess.stdout && subprocess.stderr) {
|
||||
subprocess.stdout.on('data', async (data: string) => {
|
||||
const stdout = data.toString();
|
||||
const array = stdout.split('\n');
|
||||
for (const line of array) {
|
||||
if (line !== '\n' && line !== '') {
|
||||
const log: BuildLog = {
|
||||
line: `${line.replace('\n', '')}`,
|
||||
buildId,
|
||||
applicationId
|
||||
};
|
||||
logs.push(log);
|
||||
if (debug) {
|
||||
await saveBuildLog(log);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
subprocess.stderr.on('data', async (data: string) => {
|
||||
const stderr = data.toString();
|
||||
const array = stderr.split('\n');
|
||||
for (const line of array) {
|
||||
if (line !== '\n' && line !== '') {
|
||||
const log = {
|
||||
line: `${line.replace('\n', '')}`,
|
||||
buildId,
|
||||
applicationId
|
||||
};
|
||||
logs.push(log);
|
||||
if (debug) {
|
||||
await saveBuildLog(log);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
subprocess.on('exit', async (code: number) => {
|
||||
if (code === 0) {
|
||||
//@ts-ignore
|
||||
resolve(code);
|
||||
} else {
|
||||
if (!debug) {
|
||||
for (const log of logs) {
|
||||
await saveBuildLog(log);
|
||||
}
|
||||
}
|
||||
reject(code);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (shell) {
|
||||
return await execaCommand(command, {
|
||||
//@ts-ignore
|
||||
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
|
||||
});
|
||||
} else {
|
||||
//@ts-ignore
|
||||
return await execa(dockerCommand, dockerArgs, {
|
||||
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (shell) {
|
||||
return execaCommand(command, { shell: true });
|
||||
}
|
||||
//@ts-ignore
|
||||
return await execa(dockerCommand, dockerArgs);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createRemoteEngineConfiguration(id: string) {
|
||||
const homedir = os.homedir();
|
||||
const sshKeyFile = `/tmp/id_rsa-${id}`;
|
||||
const localPort = await getFreeSSHLocalPort(id);
|
||||
const {
|
||||
sshKey: { privateKey },
|
||||
network,
|
||||
remoteIpAddress,
|
||||
remotePort,
|
||||
remoteUser
|
||||
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
|
||||
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
|
||||
const config = sshConfig.parse('');
|
||||
const Host = `${remoteIpAddress}-remote`;
|
||||
|
||||
try {
|
||||
await executeCommand({ command: `ssh-keygen -R ${Host}` });
|
||||
await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` });
|
||||
await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` });
|
||||
} catch (error) {}
|
||||
|
||||
const found = config.find({ Host });
|
||||
const foundIp = config.find({ Host: remoteIpAddress });
|
||||
|
||||
if (found) config.remove({ Host });
|
||||
if (foundIp) config.remove({ Host: remoteIpAddress });
|
||||
|
||||
config.append({
|
||||
Host,
|
||||
Hostname: remoteIpAddress,
|
||||
Port: remotePort.toString(),
|
||||
User: remoteUser,
|
||||
StrictHostKeyChecking: 'no',
|
||||
IdentityFile: sshKeyFile,
|
||||
ControlMaster: 'auto',
|
||||
ControlPath: `${homedir}/.ssh/coolify-${remoteIpAddress}-%r@%h:%p`,
|
||||
ControlPersist: '10m'
|
||||
});
|
||||
|
||||
try {
|
||||
await fs.stat(`${homedir}/.ssh/`);
|
||||
} catch (error) {
|
||||
await fs.mkdir(`${homedir}/.ssh/`);
|
||||
}
|
||||
return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config));
|
||||
}
|
||||
96
apps/trpc-experimental/server/src/lib/importers/github.ts
Normal file
96
apps/trpc-experimental/server/src/lib/importers/github.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
import { prisma } from '../../prisma';
|
||||
import { saveBuildLog } from '../buildPacks/common';
|
||||
import { decrypt } from '../common';
|
||||
import { executeCommand } from '../executeCommand';
|
||||
|
||||
export default async function ({
|
||||
applicationId,
|
||||
workdir,
|
||||
githubAppId,
|
||||
repository,
|
||||
apiUrl,
|
||||
gitCommitHash,
|
||||
htmlUrl,
|
||||
branch,
|
||||
buildId,
|
||||
customPort,
|
||||
forPublic
|
||||
}: {
|
||||
applicationId: string;
|
||||
workdir: string;
|
||||
githubAppId: string;
|
||||
repository: string;
|
||||
apiUrl: string;
|
||||
gitCommitHash?: string;
|
||||
htmlUrl: string;
|
||||
branch: string;
|
||||
buildId: string;
|
||||
customPort: number;
|
||||
forPublic?: boolean;
|
||||
}): Promise<string> {
|
||||
const { default: got } = await import('got')
|
||||
const url = htmlUrl.replace('https://', '').replace('http://', '');
|
||||
if (forPublic) {
|
||||
await saveBuildLog({
|
||||
line: `Cloning ${repository}:${branch}...`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
if (gitCommitHash) {
|
||||
await saveBuildLog({
|
||||
line: `Checking out ${gitCommitHash} commit...`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
}
|
||||
await executeCommand({
|
||||
command:
|
||||
`git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir} && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `,
|
||||
shell: true
|
||||
});
|
||||
|
||||
} else {
|
||||
const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } });
|
||||
if (body.privateKey) body.privateKey = decrypt(body.privateKey);
|
||||
const { privateKey, appId, installationId } = body
|
||||
const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, '');
|
||||
|
||||
const payload = {
|
||||
iat: Math.round(new Date().getTime() / 1000),
|
||||
exp: Math.round(new Date().getTime() / 1000 + 60),
|
||||
iss: appId
|
||||
};
|
||||
const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, {
|
||||
algorithm: 'RS256'
|
||||
});
|
||||
const { token } = await got
|
||||
.post(`${apiUrl}/app/installations/${installationId}/access_tokens`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
Accept: 'application/vnd.github.machine-man-preview+json'
|
||||
}
|
||||
})
|
||||
.json();
|
||||
await saveBuildLog({
|
||||
line: `Cloning ${repository}:${branch}...`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
if (gitCommitHash) {
|
||||
await saveBuildLog({
|
||||
line: `Checking out ${gitCommitHash} commit...`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
}
|
||||
await executeCommand({
|
||||
command:
|
||||
`git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `,
|
||||
shell: true
|
||||
});
|
||||
}
|
||||
const { stdout: commit } = await executeCommand({ command: `cd ${workdir}/ && git rev-parse HEAD`, shell: true });
|
||||
return commit.replace('\n', '');
|
||||
}
|
||||
65
apps/trpc-experimental/server/src/lib/importers/gitlab.ts
Normal file
65
apps/trpc-experimental/server/src/lib/importers/gitlab.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { saveBuildLog } from "../buildPacks/common";
|
||||
import { executeCommand } from "../executeCommand";
|
||||
|
||||
export default async function ({
|
||||
applicationId,
|
||||
workdir,
|
||||
repodir,
|
||||
htmlUrl,
|
||||
gitCommitHash,
|
||||
repository,
|
||||
branch,
|
||||
buildId,
|
||||
privateSshKey,
|
||||
customPort,
|
||||
forPublic,
|
||||
customUser,
|
||||
}: {
|
||||
applicationId: string;
|
||||
workdir: string;
|
||||
repository: string;
|
||||
htmlUrl: string;
|
||||
branch: string;
|
||||
buildId: string;
|
||||
repodir: string;
|
||||
gitCommitHash: string;
|
||||
privateSshKey: string;
|
||||
customPort: number;
|
||||
forPublic: boolean;
|
||||
customUser: string;
|
||||
}): Promise<string> {
|
||||
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
|
||||
if (!forPublic) {
|
||||
await executeCommand({ command: `echo '${privateSshKey}' > ${repodir}/id.rsa`, shell: true });
|
||||
await executeCommand({ command: `chmod 600 ${repodir}/id.rsa` });
|
||||
}
|
||||
|
||||
await saveBuildLog({
|
||||
line: `Cloning ${repository}:${branch}...`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
if (gitCommitHash) {
|
||||
await saveBuildLog({
|
||||
line: `Checking out ${gitCommitHash} commit...`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
}
|
||||
if (forPublic) {
|
||||
await executeCommand({
|
||||
command:
|
||||
`git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await executeCommand({
|
||||
command:
|
||||
`git clone -q -b ${branch} ${customUser}@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const { stdout: commit } = await executeCommand({ command: `cd ${workdir}/ && git rev-parse HEAD`, shell: true });
|
||||
return commit.replace('\n', '');
|
||||
}
|
||||
4
apps/trpc-experimental/server/src/lib/importers/index.ts
Normal file
4
apps/trpc-experimental/server/src/lib/importers/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import github from './github';
|
||||
import gitlab from './gitlab';
|
||||
|
||||
export { github, gitlab };
|
||||
48
apps/trpc-experimental/server/src/lib/logging.ts
Normal file
48
apps/trpc-experimental/server/src/lib/logging.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { prisma } from '../prisma';
|
||||
import { encrypt, generateTimestamp, isDev } from './common';
|
||||
import { day } from './dayjs';
|
||||
|
||||
export type Line = string | { shortMessage: string; stderr: string };
|
||||
export type BuildLog = {
|
||||
line: Line;
|
||||
buildId?: string;
|
||||
applicationId?: string;
|
||||
};
|
||||
export const saveBuildLog = async ({ line, buildId, applicationId }: BuildLog): Promise<any> => {
|
||||
if (buildId === 'undefined' || buildId === 'null' || !buildId) return;
|
||||
if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return;
|
||||
const { default: got } = await import('got');
|
||||
if (typeof line === 'object' && line) {
|
||||
if (line.shortMessage) {
|
||||
line = line.shortMessage + '\n' + line.stderr;
|
||||
} else {
|
||||
line = JSON.stringify(line);
|
||||
}
|
||||
}
|
||||
if (line && typeof line === 'string' && line.includes('ghs_')) {
|
||||
const regex = /ghs_.*@/g;
|
||||
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
||||
}
|
||||
const addTimestamp = `[${generateTimestamp()}] ${line}`;
|
||||
const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
|
||||
|
||||
if (isDev) {
|
||||
console.debug(`[${applicationId}] ${addTimestamp}`);
|
||||
}
|
||||
try {
|
||||
return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, {
|
||||
json: {
|
||||
line: encrypt(line)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return await prisma.buildLog.create({
|
||||
data: {
|
||||
line: addTimestamp,
|
||||
buildId,
|
||||
time: Number(day().valueOf()),
|
||||
applicationId
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
47
apps/trpc-experimental/server/src/lib/ssh.ts
Normal file
47
apps/trpc-experimental/server/src/lib/ssh.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { prisma } from '../prisma';
|
||||
import { generateRangeArray } from './common';
|
||||
|
||||
export async function getFreeSSHLocalPort(id: string): Promise<number | boolean> {
|
||||
const { default: isReachable } = await import('is-port-reachable');
|
||||
const { remoteIpAddress, sshLocalPort } = await prisma.destinationDocker.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
if (sshLocalPort) {
|
||||
return Number(sshLocalPort);
|
||||
}
|
||||
|
||||
const data = await prisma.setting.findFirst();
|
||||
const { minPort, maxPort } = data;
|
||||
|
||||
const ports = await prisma.destinationDocker.findMany({
|
||||
where: { sshLocalPort: { not: null }, remoteIpAddress: { not: remoteIpAddress } }
|
||||
});
|
||||
|
||||
const alreadyConfigured = await prisma.destinationDocker.findFirst({
|
||||
where: {
|
||||
remoteIpAddress,
|
||||
id: { not: id },
|
||||
sshLocalPort: { not: null }
|
||||
}
|
||||
});
|
||||
if (alreadyConfigured?.sshLocalPort) {
|
||||
await prisma.destinationDocker.update({
|
||||
where: { id },
|
||||
data: { sshLocalPort: alreadyConfigured.sshLocalPort }
|
||||
});
|
||||
return Number(alreadyConfigured.sshLocalPort);
|
||||
}
|
||||
const range = generateRangeArray(minPort, maxPort);
|
||||
const availablePorts = range.filter((port) => !ports.map((p) => p.sshLocalPort).includes(port));
|
||||
for (const port of availablePorts) {
|
||||
const found = await isReachable(port, { host: 'localhost' });
|
||||
if (!found) {
|
||||
await prisma.destinationDocker.update({
|
||||
where: { id },
|
||||
data: { sshLocalPort: Number(port) }
|
||||
});
|
||||
return Number(port);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
20
apps/trpc-experimental/server/src/prisma.ts
Normal file
20
apps/trpc-experimental/server/src/prisma.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Instantiates a single instance PrismaClient and save it on the global object.
|
||||
* @link https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices
|
||||
*/
|
||||
import { env } from './env';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prismaGlobal = global as typeof global & {
|
||||
prisma?: PrismaClient;
|
||||
};
|
||||
|
||||
export const prisma: PrismaClient =
|
||||
prismaGlobal.prisma ||
|
||||
new PrismaClient({
|
||||
log: env.NODE_ENV !== 'development' ? ['query', 'error', 'warn'] : ['error']
|
||||
});
|
||||
|
||||
if (env.NODE_ENV !== 'production') {
|
||||
prismaGlobal.prisma = prisma;
|
||||
}
|
||||
17
apps/trpc-experimental/server/src/scheduler.ts
Normal file
17
apps/trpc-experimental/server/src/scheduler.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Bree from 'bree';
|
||||
import path from 'path';
|
||||
import Cabin from 'cabin';
|
||||
import TSBree from '@breejs/ts-worker';
|
||||
|
||||
export const isDev = process.env['NODE_ENV'] === 'development';
|
||||
|
||||
Bree.extend(TSBree);
|
||||
|
||||
const options: any = {
|
||||
defaultExtension: 'js',
|
||||
logger: false,
|
||||
jobs: [{ name: 'applicationBuildQueue' }]
|
||||
};
|
||||
if (isDev) options.root = path.join(__dirname, './jobs');
|
||||
|
||||
export const scheduler = new Bree(options);
|
||||
79
apps/trpc-experimental/server/src/server.ts
Normal file
79
apps/trpc-experimental/server/src/server.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
|
||||
import fastify from 'fastify';
|
||||
import { appRouter } from './trpc';
|
||||
import { createContext } from './trpc/context';
|
||||
import cors from '@fastify/cors';
|
||||
import * as path from 'node:path';
|
||||
import serve from '@fastify/static';
|
||||
import autoLoad from '@fastify/autoload';
|
||||
// import { prisma } from './prisma';
|
||||
import Graceful from '@ladjs/graceful';
|
||||
import { scheduler } from './scheduler';
|
||||
|
||||
const isDev = process.env['NODE_ENV'] === 'development';
|
||||
|
||||
export interface ServerOptions {
|
||||
dev?: boolean;
|
||||
port?: number;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
export function createServer(opts: ServerOptions) {
|
||||
const dev = opts.dev ?? true;
|
||||
const port = opts.port ?? 3000;
|
||||
const prefix = opts.prefix ?? '/trpc';
|
||||
const server = fastify({ logger: dev, trustProxy: true });
|
||||
server.register(cors);
|
||||
server.register(fastifyTRPCPlugin, {
|
||||
prefix,
|
||||
trpcOptions: {
|
||||
router: appRouter,
|
||||
createContext,
|
||||
onError({ error, type, path, input, ctx, req }) {
|
||||
console.error('Error:', error);
|
||||
if (error.code === 'INTERNAL_SERVER_ERROR') {
|
||||
// send to bug reporting
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Serve static files in production. Static files are generated by `yarn build` in the client folder by SvelteKit.
|
||||
if (!isDev) {
|
||||
server.register(serve, {
|
||||
root: path.join(__dirname, './public'),
|
||||
preCompressed: true
|
||||
});
|
||||
server.setNotFoundHandler(async function (request, reply) {
|
||||
if (request.raw.url && request.raw.url.startsWith('/api')) {
|
||||
return reply.status(404).send({
|
||||
success: false
|
||||
});
|
||||
}
|
||||
return reply.status(200).sendFile('index.html');
|
||||
});
|
||||
}
|
||||
server.register(autoLoad, {
|
||||
dir: path.join(__dirname, 'api'),
|
||||
options: { prefix: '/api' }
|
||||
});
|
||||
|
||||
const stop = () => server.close();
|
||||
const start = async () => {
|
||||
try {
|
||||
await server.listen({ host: '0.0.0.0', port });
|
||||
console.log('Coolify server is listening on port', port, 'at 0.0.0.0 🚀');
|
||||
const graceful = new Graceful({ brees: [scheduler] });
|
||||
graceful.listen();
|
||||
setInterval(async () => {
|
||||
if (!scheduler.workers.has('applicationBuildQueue')) {
|
||||
scheduler.run('applicationBuildQueue');
|
||||
}
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
server.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
return { server, start, stop };
|
||||
}
|
||||
1
apps/trpc-experimental/server/src/tags.json
Normal file
1
apps/trpc-experimental/server/src/tags.json
Normal file
File diff suppressed because one or more lines are too long
1
apps/trpc-experimental/server/src/templates.json
Normal file
1
apps/trpc-experimental/server/src/templates.json
Normal file
File diff suppressed because one or more lines are too long
22
apps/trpc-experimental/server/src/trpc/context.ts
Normal file
22
apps/trpc-experimental/server/src/trpc/context.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { inferAsyncReturnType } from '@trpc/server';
|
||||
import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { env } from '../env';
|
||||
export interface User {
|
||||
userId: string;
|
||||
teamId: string;
|
||||
permission: string;
|
||||
isAdmin: boolean;
|
||||
iat: number;
|
||||
}
|
||||
|
||||
export function createContext({ req }: CreateFastifyContextOptions) {
|
||||
const token = req.headers.authorization;
|
||||
let user: User | null = null;
|
||||
if (token) {
|
||||
user = jwt.verify(token, env.COOLIFY_SECRET_KEY) as User;
|
||||
}
|
||||
return { user, hostname: req.hostname };
|
||||
}
|
||||
|
||||
export type Context = inferAsyncReturnType<typeof createContext>;
|
||||
27
apps/trpc-experimental/server/src/trpc/index.ts
Normal file
27
apps/trpc-experimental/server/src/trpc/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { router } from './trpc';
|
||||
import type { Permission } from '@prisma/client';
|
||||
|
||||
import {
|
||||
settingsRouter,
|
||||
authRouter,
|
||||
dashboardRouter,
|
||||
applicationsRouter,
|
||||
servicesRouter,
|
||||
databasesRouter,
|
||||
sourcesRouter,
|
||||
destinationsRouter
|
||||
} from './routers';
|
||||
|
||||
export const appRouter = router({
|
||||
settings: settingsRouter,
|
||||
auth: authRouter,
|
||||
dashboard: dashboardRouter,
|
||||
applications: applicationsRouter,
|
||||
services: servicesRouter,
|
||||
databases: databasesRouter,
|
||||
sources: sourcesRouter,
|
||||
destinations: destinationsRouter
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
export type PrismaPermission = Permission;
|
||||
1388
apps/trpc-experimental/server/src/trpc/routers/applications/index.ts
Normal file
1388
apps/trpc-experimental/server/src/trpc/routers/applications/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,533 @@
|
||||
import cuid from 'cuid';
|
||||
import crypto from 'node:crypto';
|
||||
import { decrypt, isARM } from '../../../lib/common';
|
||||
|
||||
import { prisma } from '../../../prisma';
|
||||
|
||||
export async function deployApplication(
|
||||
id: string,
|
||||
teamId: string,
|
||||
forceRebuild: boolean,
|
||||
pullmergeRequestId: string | null = null,
|
||||
branch: string | null = null
|
||||
): Promise<string | Error> {
|
||||
const buildId = cuid();
|
||||
const application = await getApplicationFromDB(id, teamId);
|
||||
if (application) {
|
||||
if (!application?.configHash) {
|
||||
await generateConfigHash(
|
||||
id,
|
||||
application.buildPack,
|
||||
application.port,
|
||||
application.exposePort,
|
||||
application.installCommand,
|
||||
application.buildCommand,
|
||||
application.startCommand
|
||||
);
|
||||
}
|
||||
await prisma.application.update({ where: { id }, data: { updatedAt: new Date() } });
|
||||
if (application.gitSourceId) {
|
||||
await prisma.build.create({
|
||||
data: {
|
||||
id: buildId,
|
||||
applicationId: id,
|
||||
sourceBranch: branch,
|
||||
branch: application.branch,
|
||||
pullmergeRequestId: pullmergeRequestId?.toString(),
|
||||
forceRebuild,
|
||||
destinationDockerId: application.destinationDocker?.id,
|
||||
gitSourceId: application.gitSource?.id,
|
||||
githubAppId: application.gitSource?.githubApp?.id,
|
||||
gitlabAppId: application.gitSource?.gitlabApp?.id,
|
||||
status: 'queued',
|
||||
type: pullmergeRequestId
|
||||
? application.gitSource?.githubApp?.id
|
||||
? 'manual_pr'
|
||||
: 'manual_mr'
|
||||
: 'manual'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await prisma.build.create({
|
||||
data: {
|
||||
id: buildId,
|
||||
applicationId: id,
|
||||
branch: 'latest',
|
||||
forceRebuild,
|
||||
destinationDockerId: application.destinationDocker?.id,
|
||||
status: 'queued',
|
||||
type: 'manual'
|
||||
}
|
||||
});
|
||||
}
|
||||
return buildId;
|
||||
}
|
||||
throw { status: 500, message: 'Application cannot be deployed.' };
|
||||
}
|
||||
export async function generateConfigHash(
|
||||
id: string,
|
||||
buildPack: string,
|
||||
port: number,
|
||||
exposePort: number,
|
||||
installCommand: string,
|
||||
buildCommand: string,
|
||||
startCommand: string
|
||||
): Promise<any> {
|
||||
const configHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(
|
||||
JSON.stringify({
|
||||
buildPack,
|
||||
port,
|
||||
exposePort,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand
|
||||
})
|
||||
)
|
||||
.digest('hex');
|
||||
return await prisma.application.update({ where: { id }, data: { configHash } });
|
||||
}
|
||||
export async function getApplicationFromDB(id: string, teamId: string) {
|
||||
let application = await prisma.application.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: {
|
||||
destinationDocker: true,
|
||||
settings: true,
|
||||
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
||||
secrets: true,
|
||||
persistentStorage: true,
|
||||
connectedDatabase: true,
|
||||
previewApplication: true,
|
||||
dockerRegistry: true
|
||||
}
|
||||
});
|
||||
if (!application) {
|
||||
throw { status: 404, message: 'Application not found.' };
|
||||
}
|
||||
application = decryptApplication(application);
|
||||
const buildPack = application?.buildPack || null;
|
||||
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(buildPack);
|
||||
|
||||
// Set default build images
|
||||
if (application && !application.baseImage) {
|
||||
application.baseImage = baseImage;
|
||||
}
|
||||
if (application && !application.baseBuildImage) {
|
||||
application.baseBuildImage = baseBuildImage;
|
||||
}
|
||||
return { ...application, baseBuildImages, baseImages };
|
||||
}
|
||||
function decryptApplication(application: any) {
|
||||
if (application) {
|
||||
if (application?.gitSource?.githubApp?.clientSecret) {
|
||||
application.gitSource.githubApp.clientSecret =
|
||||
decrypt(application.gitSource.githubApp.clientSecret) || null;
|
||||
}
|
||||
if (application?.gitSource?.githubApp?.webhookSecret) {
|
||||
application.gitSource.githubApp.webhookSecret =
|
||||
decrypt(application.gitSource.githubApp.webhookSecret) || null;
|
||||
}
|
||||
if (application?.gitSource?.githubApp?.privateKey) {
|
||||
application.gitSource.githubApp.privateKey =
|
||||
decrypt(application.gitSource.githubApp.privateKey) || null;
|
||||
}
|
||||
if (application?.gitSource?.gitlabApp?.appSecret) {
|
||||
application.gitSource.gitlabApp.appSecret =
|
||||
decrypt(application.gitSource.gitlabApp.appSecret) || null;
|
||||
}
|
||||
if (application?.secrets.length > 0) {
|
||||
application.secrets = application.secrets.map((s: any) => {
|
||||
s.value = decrypt(s.value) || null;
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
return application;
|
||||
}
|
||||
}
|
||||
|
||||
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
|
||||
const nodeBased = [
|
||||
'react',
|
||||
'preact',
|
||||
'vuejs',
|
||||
'svelte',
|
||||
'gatsby',
|
||||
'astro',
|
||||
'eleventy',
|
||||
'node',
|
||||
'nestjs',
|
||||
'nuxtjs',
|
||||
'nextjs'
|
||||
];
|
||||
export function setDefaultBaseImage(
|
||||
buildPack: string | null,
|
||||
deploymentType: string | null = null
|
||||
) {
|
||||
const nodeVersions = [
|
||||
{
|
||||
value: 'node:lts',
|
||||
label: 'node:lts'
|
||||
},
|
||||
{
|
||||
value: 'node:18',
|
||||
label: 'node:18'
|
||||
},
|
||||
{
|
||||
value: 'node:17',
|
||||
label: 'node:17'
|
||||
},
|
||||
{
|
||||
value: 'node:16',
|
||||
label: 'node:16'
|
||||
},
|
||||
{
|
||||
value: 'node:14',
|
||||
label: 'node:14'
|
||||
},
|
||||
{
|
||||
value: 'node:12',
|
||||
label: 'node:12'
|
||||
}
|
||||
];
|
||||
const staticVersions = [
|
||||
{
|
||||
value: 'webdevops/nginx:alpine',
|
||||
label: 'webdevops/nginx:alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/apache:alpine',
|
||||
label: 'webdevops/apache:alpine'
|
||||
},
|
||||
{
|
||||
value: 'nginx:alpine',
|
||||
label: 'nginx:alpine'
|
||||
},
|
||||
{
|
||||
value: 'httpd:alpine',
|
||||
label: 'httpd:alpine (Apache)'
|
||||
}
|
||||
];
|
||||
const rustVersions = [
|
||||
{
|
||||
value: 'rust:latest',
|
||||
label: 'rust:latest'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60',
|
||||
label: 'rust:1.60'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-buster',
|
||||
label: 'rust:1.60-buster'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-bullseye',
|
||||
label: 'rust:1.60-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-slim-buster',
|
||||
label: 'rust:1.60-slim-buster'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-slim-bullseye',
|
||||
label: 'rust:1.60-slim-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-alpine3.14',
|
||||
label: 'rust:1.60-alpine3.14'
|
||||
},
|
||||
{
|
||||
value: 'rust:1.60-alpine3.15',
|
||||
label: 'rust:1.60-alpine3.15'
|
||||
}
|
||||
];
|
||||
const phpVersions = [
|
||||
{
|
||||
value: 'webdevops/php-apache:8.2',
|
||||
label: 'webdevops/php-apache:8.2'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.2',
|
||||
label: 'webdevops/php-nginx:8.2'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.1',
|
||||
label: 'webdevops/php-apache:8.1'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.1',
|
||||
label: 'webdevops/php-nginx:8.1'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.0',
|
||||
label: 'webdevops/php-apache:8.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.0',
|
||||
label: 'webdevops/php-nginx:8.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.4',
|
||||
label: 'webdevops/php-apache:7.4'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.4',
|
||||
label: 'webdevops/php-nginx:7.4'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.3',
|
||||
label: 'webdevops/php-apache:7.3'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.3',
|
||||
label: 'webdevops/php-nginx:7.3'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.2',
|
||||
label: 'webdevops/php-apache:7.2'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.2',
|
||||
label: 'webdevops/php-nginx:7.2'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.1',
|
||||
label: 'webdevops/php-apache:7.1'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.1',
|
||||
label: 'webdevops/php-nginx:7.1'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.0',
|
||||
label: 'webdevops/php-apache:7.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.0',
|
||||
label: 'webdevops/php-nginx:7.0'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:5.6',
|
||||
label: 'webdevops/php-apache:5.6'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:5.6',
|
||||
label: 'webdevops/php-nginx:5.6'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.2-alpine',
|
||||
label: 'webdevops/php-apache:8.2-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.2-alpine',
|
||||
label: 'webdevops/php-nginx:8.2-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.1-alpine',
|
||||
label: 'webdevops/php-apache:8.1-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.1-alpine',
|
||||
label: 'webdevops/php-nginx:8.1-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:8.0-alpine',
|
||||
label: 'webdevops/php-apache:8.0-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:8.0-alpine',
|
||||
label: 'webdevops/php-nginx:8.0-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.4-alpine',
|
||||
label: 'webdevops/php-apache:7.4-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.4-alpine',
|
||||
label: 'webdevops/php-nginx:7.4-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.3-alpine',
|
||||
label: 'webdevops/php-apache:7.3-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.3-alpine',
|
||||
label: 'webdevops/php-nginx:7.3-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.2-alpine',
|
||||
label: 'webdevops/php-apache:7.2-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-nginx:7.2-alpine',
|
||||
label: 'webdevops/php-nginx:7.2-alpine'
|
||||
},
|
||||
{
|
||||
value: 'webdevops/php-apache:7.1-alpine',
|
||||
label: 'webdevops/php-apache:7.1-alpine'
|
||||
},
|
||||
{
|
||||
value: 'php:8.1-fpm',
|
||||
label: 'php:8.1-fpm'
|
||||
},
|
||||
{
|
||||
value: 'php:8.0-fpm',
|
||||
label: 'php:8.0-fpm'
|
||||
},
|
||||
{
|
||||
value: 'php:8.1-fpm-alpine',
|
||||
label: 'php:8.1-fpm-alpine'
|
||||
},
|
||||
{
|
||||
value: 'php:8.0-fpm-alpine',
|
||||
label: 'php:8.0-fpm-alpine'
|
||||
}
|
||||
];
|
||||
const pythonVersions = [
|
||||
{
|
||||
value: 'python:3.10-alpine',
|
||||
label: 'python:3.10-alpine'
|
||||
},
|
||||
{
|
||||
value: 'python:3.10-buster',
|
||||
label: 'python:3.10-buster'
|
||||
},
|
||||
{
|
||||
value: 'python:3.10-bullseye',
|
||||
label: 'python:3.10-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.10-slim-bullseye',
|
||||
label: 'python:3.10-slim-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.9-alpine',
|
||||
label: 'python:3.9-alpine'
|
||||
},
|
||||
{
|
||||
value: 'python:3.9-buster',
|
||||
label: 'python:3.9-buster'
|
||||
},
|
||||
{
|
||||
value: 'python:3.9-bullseye',
|
||||
label: 'python:3.9-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.9-slim-bullseye',
|
||||
label: 'python:3.9-slim-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.8-alpine',
|
||||
label: 'python:3.8-alpine'
|
||||
},
|
||||
{
|
||||
value: 'python:3.8-buster',
|
||||
label: 'python:3.8-buster'
|
||||
},
|
||||
{
|
||||
value: 'python:3.8-bullseye',
|
||||
label: 'python:3.8-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.8-slim-bullseye',
|
||||
label: 'python:3.8-slim-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.7-alpine',
|
||||
label: 'python:3.7-alpine'
|
||||
},
|
||||
{
|
||||
value: 'python:3.7-buster',
|
||||
label: 'python:3.7-buster'
|
||||
},
|
||||
{
|
||||
value: 'python:3.7-bullseye',
|
||||
label: 'python:3.7-bullseye'
|
||||
},
|
||||
{
|
||||
value: 'python:3.7-slim-bullseye',
|
||||
label: 'python:3.7-slim-bullseye'
|
||||
}
|
||||
];
|
||||
const herokuVersions = [
|
||||
{
|
||||
value: 'heroku/builder:22',
|
||||
label: 'heroku/builder:22'
|
||||
},
|
||||
{
|
||||
value: 'heroku/buildpacks:20',
|
||||
label: 'heroku/buildpacks:20'
|
||||
},
|
||||
{
|
||||
value: 'heroku/builder-classic:22',
|
||||
label: 'heroku/builder-classic:22'
|
||||
}
|
||||
];
|
||||
let payload: any = {
|
||||
baseImage: null,
|
||||
baseBuildImage: null,
|
||||
baseImages: [],
|
||||
baseBuildImages: []
|
||||
};
|
||||
if (nodeBased.includes(buildPack)) {
|
||||
if (deploymentType === 'static') {
|
||||
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine';
|
||||
payload.baseImages = isARM(process.arch)
|
||||
? staticVersions.filter((version) => !version.value.includes('webdevops'))
|
||||
: staticVersions;
|
||||
payload.baseBuildImage = 'node:lts';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
} else {
|
||||
payload.baseImage = 'node:lts';
|
||||
payload.baseImages = nodeVersions;
|
||||
payload.baseBuildImage = 'node:lts';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
}
|
||||
}
|
||||
if (staticApps.includes(buildPack)) {
|
||||
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine';
|
||||
payload.baseImages = isARM(process.arch)
|
||||
? staticVersions.filter((version) => !version.value.includes('webdevops'))
|
||||
: staticVersions;
|
||||
payload.baseBuildImage = 'node:lts';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
}
|
||||
if (buildPack === 'python') {
|
||||
payload.baseImage = 'python:3.10-alpine';
|
||||
payload.baseImages = pythonVersions;
|
||||
}
|
||||
if (buildPack === 'rust') {
|
||||
payload.baseImage = 'rust:latest';
|
||||
payload.baseBuildImage = 'rust:latest';
|
||||
payload.baseImages = rustVersions;
|
||||
payload.baseBuildImages = rustVersions;
|
||||
}
|
||||
if (buildPack === 'deno') {
|
||||
payload.baseImage = 'denoland/deno:latest';
|
||||
}
|
||||
if (buildPack === 'php') {
|
||||
payload.baseImage = isARM(process.arch)
|
||||
? 'php:8.1-fpm-alpine'
|
||||
: 'webdevops/php-apache:8.2-alpine';
|
||||
payload.baseImages = isARM(process.arch)
|
||||
? phpVersions.filter((version) => !version.value.includes('webdevops'))
|
||||
: phpVersions;
|
||||
}
|
||||
if (buildPack === 'laravel') {
|
||||
payload.baseImage = isARM(process.arch)
|
||||
? 'php:8.1-fpm-alpine'
|
||||
: 'webdevops/php-apache:8.2-alpine';
|
||||
payload.baseImages = isARM(process.arch)
|
||||
? phpVersions.filter((version) => !version.value.includes('webdevops'))
|
||||
: phpVersions;
|
||||
payload.baseBuildImage = 'node:18';
|
||||
payload.baseBuildImages = nodeVersions;
|
||||
}
|
||||
if (buildPack === 'heroku') {
|
||||
payload.baseImage = 'heroku/buildpacks:20';
|
||||
payload.baseImages = herokuVersions;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
178
apps/trpc-experimental/server/src/trpc/routers/auth.ts
Normal file
178
apps/trpc-experimental/server/src/trpc/routers/auth.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { z } from 'zod';
|
||||
import { publicProcedure, router } from '../trpc';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { comparePassword, hashPassword, listSettings, uniqueName } from '../../lib/common';
|
||||
import { env } from '../../env';
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
import { prisma } from '../../prisma';
|
||||
import cuid from 'cuid';
|
||||
|
||||
export const authRouter = router({
|
||||
register: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { email, password } = input;
|
||||
const userFound = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: { teams: true, permission: true }
|
||||
});
|
||||
|
||||
if (userFound) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'User already exists.'
|
||||
});
|
||||
}
|
||||
const settings = await listSettings();
|
||||
if (!settings?.isRegistrationEnabled) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Registration is disabled.'
|
||||
});
|
||||
}
|
||||
const usersCount = await prisma.user.count();
|
||||
const uid = usersCount === 0 ? '0' : cuid();
|
||||
const permission = 'owner';
|
||||
const isAdmin = true;
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
// Create the first user as the owner
|
||||
if (usersCount === 0) {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: uid,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
type: 'email',
|
||||
teams: {
|
||||
create: {
|
||||
id: uid,
|
||||
name: uniqueName(),
|
||||
destinationDocker: { connect: { network: 'coolify' } }
|
||||
}
|
||||
},
|
||||
permission: { create: { teamId: uid, permission } }
|
||||
},
|
||||
include: { teams: true }
|
||||
});
|
||||
await prisma.setting.update({
|
||||
where: { id: '0' },
|
||||
data: { isRegistrationEnabled: false }
|
||||
});
|
||||
} else {
|
||||
// Create a new user and team
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: uid,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
type: 'email',
|
||||
teams: {
|
||||
create: {
|
||||
id: uid,
|
||||
name: uniqueName()
|
||||
}
|
||||
},
|
||||
permission: { create: { teamId: uid, permission } }
|
||||
},
|
||||
include: { teams: true }
|
||||
});
|
||||
}
|
||||
const payload = {
|
||||
userId: uid,
|
||||
teamId: uid,
|
||||
permission,
|
||||
isAdmin
|
||||
};
|
||||
return {
|
||||
...payload,
|
||||
token: jsonwebtoken.sign(payload, env.COOLIFY_SECRET_KEY)
|
||||
};
|
||||
}),
|
||||
login: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { email, password } = input;
|
||||
const userFound = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: { teams: true, permission: true }
|
||||
});
|
||||
|
||||
if (!userFound) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'User already exists.'
|
||||
});
|
||||
}
|
||||
if (userFound.type === 'email') {
|
||||
if (userFound.password === 'RESETME') {
|
||||
const hashedPassword = await hashPassword(password);
|
||||
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
|
||||
if (userFound.id === '0') {
|
||||
await prisma.user.update({
|
||||
where: { email: userFound.email },
|
||||
data: { password: 'RESETME' }
|
||||
});
|
||||
} else {
|
||||
await prisma.user.update({
|
||||
where: { email: userFound.email },
|
||||
data: { password: 'RESETTIMEOUT' }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await prisma.user.update({
|
||||
where: { email: userFound.email },
|
||||
data: { password: hashedPassword }
|
||||
});
|
||||
const payload = {
|
||||
userId: userFound.id,
|
||||
teamId: userFound.id,
|
||||
permission: userFound.permission,
|
||||
isAdmin: true
|
||||
};
|
||||
return {
|
||||
...payload,
|
||||
token: jsonwebtoken.sign(payload, env.COOLIFY_SECRET_KEY)
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!userFound.password) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Something went wrong. Please try again later.'
|
||||
});
|
||||
}
|
||||
const passwordMatch = comparePassword(password, userFound.password);
|
||||
if (!passwordMatch) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Incorrect password.'
|
||||
});
|
||||
}
|
||||
const payload = {
|
||||
userId: userFound.id,
|
||||
teamId: userFound.id,
|
||||
permission: userFound.permission,
|
||||
isAdmin: true
|
||||
};
|
||||
return {
|
||||
...payload,
|
||||
token: jsonwebtoken.sign(payload, env.COOLIFY_SECRET_KEY)
|
||||
};
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Not implemented yet.'
|
||||
});
|
||||
})
|
||||
});
|
||||
65
apps/trpc-experimental/server/src/trpc/routers/dashboard.ts
Normal file
65
apps/trpc-experimental/server/src/trpc/routers/dashboard.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { privateProcedure, router } from '../trpc';
|
||||
import { listSettings } from '../../lib/common';
|
||||
import { prisma } from '../../prisma';
|
||||
|
||||
export const dashboardRouter = router({
|
||||
resources: privateProcedure.query(async ({ ctx }) => {
|
||||
const id = ctx.user?.teamId === '0' ? undefined : ctx.user?.teamId;
|
||||
let applications = await prisma.application.findMany({
|
||||
where: { teams: { some: { id } } },
|
||||
include: { settings: true, destinationDocker: true, teams: true }
|
||||
});
|
||||
const databases = await prisma.database.findMany({
|
||||
where: { teams: { some: { id } } },
|
||||
include: { settings: true, destinationDocker: true, teams: true }
|
||||
});
|
||||
const services = await prisma.service.findMany({
|
||||
where: { teams: { some: { id } } },
|
||||
include: { destinationDocker: true, teams: true }
|
||||
});
|
||||
const gitSources = await prisma.gitSource.findMany({
|
||||
where: {
|
||||
OR: [{ teams: { some: { id } } }, { isSystemWide: true }]
|
||||
},
|
||||
include: { teams: true }
|
||||
});
|
||||
const destinations = await prisma.destinationDocker.findMany({
|
||||
where: { teams: { some: { id } } },
|
||||
include: { teams: true }
|
||||
});
|
||||
const settings = await listSettings();
|
||||
let foundUnconfiguredApplication = false;
|
||||
for (const application of applications) {
|
||||
if (
|
||||
((!application.buildPack || !application.branch) && !application.simpleDockerfile) ||
|
||||
!application.destinationDockerId ||
|
||||
(!application.settings?.isBot && !application?.fqdn && application.buildPack !== 'compose')
|
||||
) {
|
||||
foundUnconfiguredApplication = true;
|
||||
}
|
||||
}
|
||||
let foundUnconfiguredService = false;
|
||||
for (const service of services) {
|
||||
if (!service.fqdn) {
|
||||
foundUnconfiguredService = true;
|
||||
}
|
||||
}
|
||||
let foundUnconfiguredDatabase = false;
|
||||
for (const database of databases) {
|
||||
if (!database.version) {
|
||||
foundUnconfiguredDatabase = true;
|
||||
}
|
||||
}
|
||||
return {
|
||||
foundUnconfiguredApplication,
|
||||
foundUnconfiguredDatabase,
|
||||
foundUnconfiguredService,
|
||||
applications,
|
||||
databases,
|
||||
services,
|
||||
gitSources,
|
||||
destinations,
|
||||
settings
|
||||
};
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,379 @@
|
||||
import { z } from 'zod';
|
||||
import fs from 'fs/promises';
|
||||
import { privateProcedure, router } from '../../trpc';
|
||||
import {
|
||||
createDirectories,
|
||||
decrypt,
|
||||
encrypt,
|
||||
getContainerUsage,
|
||||
listSettings,
|
||||
startTraefikTCPProxy
|
||||
} from '../../../lib/common';
|
||||
import { prisma } from '../../../prisma';
|
||||
import { executeCommand } from '../../../lib/executeCommand';
|
||||
import {
|
||||
defaultComposeConfiguration,
|
||||
stopDatabaseContainer,
|
||||
stopTcpHttpProxy
|
||||
} from '../../../lib/docker';
|
||||
import {
|
||||
generateDatabaseConfiguration,
|
||||
getDatabaseVersions,
|
||||
makeLabelForStandaloneDatabase,
|
||||
updatePasswordInDb
|
||||
} from './lib';
|
||||
import yaml from 'js-yaml';
|
||||
import { getFreePublicPort } from '../services/lib';
|
||||
|
||||
export const databasesRouter = router({
|
||||
usage: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const teamId = ctx.user?.teamId;
|
||||
const { id } = input;
|
||||
let usage = {};
|
||||
|
||||
const database = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||
if (database.destinationDockerId) {
|
||||
[usage] = await Promise.all([getContainerUsage(database.destinationDocker.id, id)]);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
usage
|
||||
}
|
||||
};
|
||||
}),
|
||||
save: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const teamId = ctx.user?.teamId;
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
defaultDatabase,
|
||||
dbUser,
|
||||
dbUserPassword,
|
||||
rootUser,
|
||||
rootUserPassword,
|
||||
version,
|
||||
isRunning
|
||||
} = input;
|
||||
const database = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||
if (isRunning) {
|
||||
if (database.dbUserPassword !== dbUserPassword) {
|
||||
await updatePasswordInDb(database, dbUser, dbUserPassword, false);
|
||||
} else if (database.rootUserPassword !== rootUserPassword) {
|
||||
await updatePasswordInDb(database, rootUser, rootUserPassword, true);
|
||||
}
|
||||
}
|
||||
const encryptedDbUserPassword = dbUserPassword && encrypt(dbUserPassword);
|
||||
const encryptedRootUserPassword = rootUserPassword && encrypt(rootUserPassword);
|
||||
await prisma.database.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
defaultDatabase,
|
||||
dbUser,
|
||||
dbUserPassword: encryptedDbUserPassword,
|
||||
rootUser,
|
||||
rootUserPassword: encryptedRootUserPassword,
|
||||
version
|
||||
}
|
||||
});
|
||||
}),
|
||||
saveSettings: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
isPublic: z.boolean(),
|
||||
appendOnly: z.boolean().default(true)
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const teamId = ctx.user?.teamId;
|
||||
const { id, isPublic, appendOnly = true } = input;
|
||||
|
||||
let publicPort = null;
|
||||
|
||||
const {
|
||||
destinationDocker: { remoteEngine, engine, remoteIpAddress }
|
||||
} = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } });
|
||||
|
||||
if (isPublic) {
|
||||
publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
|
||||
}
|
||||
await prisma.database.update({
|
||||
where: { id },
|
||||
data: {
|
||||
settings: {
|
||||
upsert: { update: { isPublic, appendOnly }, create: { isPublic, appendOnly } }
|
||||
}
|
||||
}
|
||||
});
|
||||
const database = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
const { arch } = await listSettings();
|
||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||
|
||||
const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database;
|
||||
const { privatePort } = generateDatabaseConfiguration(database, arch);
|
||||
|
||||
if (destinationDockerId) {
|
||||
if (isPublic) {
|
||||
await prisma.database.update({ where: { id }, data: { publicPort } });
|
||||
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
||||
} else {
|
||||
await prisma.database.update({ where: { id }, data: { publicPort: null } });
|
||||
await stopTcpHttpProxy(id, destinationDocker, oldPublicPort);
|
||||
}
|
||||
}
|
||||
return { publicPort };
|
||||
}),
|
||||
saveSecret: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
isNew: z.boolean().default(true)
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
let { id, name, value, isNew } = input;
|
||||
|
||||
if (isNew) {
|
||||
const found = await prisma.databaseSecret.findFirst({ where: { name, databaseId: id } });
|
||||
if (found) {
|
||||
throw `Secret ${name} already exists.`;
|
||||
} else {
|
||||
value = encrypt(value.trim());
|
||||
await prisma.databaseSecret.create({
|
||||
data: { name, value, database: { connect: { id } } }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
value = encrypt(value.trim());
|
||||
const found = await prisma.databaseSecret.findFirst({ where: { databaseId: id, name } });
|
||||
|
||||
if (found) {
|
||||
await prisma.databaseSecret.updateMany({
|
||||
where: { databaseId: id, name },
|
||||
data: { value }
|
||||
});
|
||||
} else {
|
||||
await prisma.databaseSecret.create({
|
||||
data: { name, value, database: { connect: { id } } }
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
start: privateProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
const teamId = ctx.user?.teamId;
|
||||
const database = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, settings: true, databaseSecret: true }
|
||||
});
|
||||
const { arch } = await listSettings();
|
||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||
const {
|
||||
type,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
publicPort,
|
||||
settings: { isPublic },
|
||||
databaseSecret
|
||||
} = database;
|
||||
const { privatePort, command, environmentVariables, image, volume, ulimits } =
|
||||
generateDatabaseConfiguration(database, arch);
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const volumeName = volume.split(':')[0];
|
||||
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
if (databaseSecret.length > 0) {
|
||||
databaseSecret.forEach((secret) => {
|
||||
environmentVariables[secret.name] = decrypt(secret.value);
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image,
|
||||
command,
|
||||
environment: environmentVariables,
|
||||
volumes: [volume],
|
||||
ulimits,
|
||||
labels,
|
||||
...defaultComposeConfiguration(network)
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[volumeName]: {
|
||||
name: volumeName
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
await executeCommand({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker compose -f ${composeFileDestination} up -d`
|
||||
});
|
||||
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
||||
}),
|
||||
stop: privateProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
const teamId = ctx.user?.teamId;
|
||||
const database = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||
const everStarted = await stopDatabaseContainer(database);
|
||||
if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
|
||||
await prisma.database.update({
|
||||
where: { id },
|
||||
data: {
|
||||
settings: { upsert: { update: { isPublic: false }, create: { isPublic: false } } }
|
||||
}
|
||||
});
|
||||
await prisma.database.update({ where: { id }, data: { publicPort: null } });
|
||||
}),
|
||||
getDatabaseById: privateProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
const teamId = ctx.user?.teamId;
|
||||
const database = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
if (!database) {
|
||||
throw { status: 404, message: 'Database not found.' };
|
||||
}
|
||||
const settings = await listSettings();
|
||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||
const configuration = generateDatabaseConfiguration(database, settings.arch);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
privatePort: configuration?.privatePort,
|
||||
database,
|
||||
versions: await getDatabaseVersions(database.type, settings.arch),
|
||||
settings
|
||||
}
|
||||
};
|
||||
}),
|
||||
status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
|
||||
const id = input.id;
|
||||
const teamId = ctx.user?.teamId;
|
||||
|
||||
let isRunning = false;
|
||||
const database = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
if (database) {
|
||||
const { destinationDockerId, destinationDocker } = database;
|
||||
if (destinationDockerId) {
|
||||
try {
|
||||
const { stdout } = await executeCommand({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker inspect --format '{{json .State}}' ${id}`
|
||||
});
|
||||
|
||||
if (JSON.parse(stdout).Running) {
|
||||
isRunning = true;
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
isRunning
|
||||
}
|
||||
};
|
||||
}),
|
||||
cleanup: privateProcedure.query(async ({ ctx }) => {
|
||||
const teamId = ctx.user?.teamId;
|
||||
let databases = await prisma.database.findMany({
|
||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { settings: true, destinationDocker: true, teams: true }
|
||||
});
|
||||
for (const database of databases) {
|
||||
if (!database?.version) {
|
||||
const { id } = database;
|
||||
if (database.destinationDockerId) {
|
||||
const everStarted = await stopDatabaseContainer(database);
|
||||
if (everStarted)
|
||||
await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
|
||||
}
|
||||
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
|
||||
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
|
||||
await prisma.database.delete({ where: { id } });
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
delete: privateProcedure
|
||||
.input(z.object({ id: z.string(), force: z.boolean().default(false) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, force } = input;
|
||||
const teamId = ctx.user?.teamId;
|
||||
const database = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
if (!force) {
|
||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||
if (database.rootUserPassword)
|
||||
database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||
if (database.destinationDockerId) {
|
||||
const everStarted = await stopDatabaseContainer(database);
|
||||
if (everStarted)
|
||||
await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
|
||||
}
|
||||
}
|
||||
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
|
||||
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
|
||||
await prisma.database.delete({ where: { id } });
|
||||
return {};
|
||||
})
|
||||
});
|
||||
283
apps/trpc-experimental/server/src/trpc/routers/databases/lib.ts
Normal file
283
apps/trpc-experimental/server/src/trpc/routers/databases/lib.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import { base64Encode, isARM, version } from "../../../lib/common";
|
||||
import { executeCommand } from "../../../lib/executeCommand";
|
||||
import { prisma } from "../../../prisma";
|
||||
|
||||
export const supportedDatabaseTypesAndVersions = [
|
||||
{
|
||||
name: 'mongodb',
|
||||
fancyName: 'MongoDB',
|
||||
baseImage: 'bitnami/mongodb',
|
||||
baseImageARM: 'mongo',
|
||||
versions: ['5.0', '4.4', '4.2'],
|
||||
versionsARM: ['5.0', '4.4', '4.2']
|
||||
},
|
||||
{
|
||||
name: 'mysql',
|
||||
fancyName: 'MySQL',
|
||||
baseImage: 'bitnami/mysql',
|
||||
baseImageARM: 'mysql',
|
||||
versions: ['8.0', '5.7'],
|
||||
versionsARM: ['8.0', '5.7']
|
||||
},
|
||||
{
|
||||
name: 'mariadb',
|
||||
fancyName: 'MariaDB',
|
||||
baseImage: 'bitnami/mariadb',
|
||||
baseImageARM: 'mariadb',
|
||||
versions: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'],
|
||||
versionsARM: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2']
|
||||
},
|
||||
{
|
||||
name: 'postgresql',
|
||||
fancyName: 'PostgreSQL',
|
||||
baseImage: 'bitnami/postgresql',
|
||||
baseImageARM: 'postgres',
|
||||
versions: ['14.5.0', '13.8.0', '12.12.0', '11.17.0', '10.22.0'],
|
||||
versionsARM: ['14.5', '13.8', '12.12', '11.17', '10.22']
|
||||
},
|
||||
{
|
||||
name: 'redis',
|
||||
fancyName: 'Redis',
|
||||
baseImage: 'bitnami/redis',
|
||||
baseImageARM: 'redis',
|
||||
versions: ['7.0', '6.2', '6.0', '5.0'],
|
||||
versionsARM: ['7.0', '6.2', '6.0', '5.0']
|
||||
},
|
||||
{
|
||||
name: 'couchdb',
|
||||
fancyName: 'CouchDB',
|
||||
baseImage: 'bitnami/couchdb',
|
||||
baseImageARM: 'couchdb',
|
||||
versions: ['3.2.2', '3.1.2', '2.3.1'],
|
||||
versionsARM: ['3.2.2', '3.1.2', '2.3.1']
|
||||
},
|
||||
{
|
||||
name: 'edgedb',
|
||||
fancyName: 'EdgeDB',
|
||||
baseImage: 'edgedb/edgedb',
|
||||
versions: ['latest', '2.1', '2.0', '1.4']
|
||||
}
|
||||
];
|
||||
export function getDatabaseImage(type: string, arch: string): string {
|
||||
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
|
||||
if (found) {
|
||||
if (isARM(arch)) {
|
||||
return found.baseImageARM || found.baseImage;
|
||||
}
|
||||
return found.baseImage;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
export function generateDatabaseConfiguration(database: any, arch: string) {
|
||||
const { id, dbUser, dbUserPassword, rootUser, rootUserPassword, defaultDatabase, version, type } =
|
||||
database;
|
||||
const baseImage = getDatabaseImage(type, arch);
|
||||
if (type === 'mysql') {
|
||||
const configuration = {
|
||||
privatePort: 3306,
|
||||
environmentVariables: {
|
||||
MYSQL_USER: dbUser,
|
||||
MYSQL_PASSWORD: dbUserPassword,
|
||||
MYSQL_ROOT_PASSWORD: rootUserPassword,
|
||||
MYSQL_ROOT_USER: rootUser,
|
||||
MYSQL_DATABASE: defaultDatabase
|
||||
},
|
||||
image: `${baseImage}:${version}`,
|
||||
volume: `${id}-${type}-data:/bitnami/mysql/data`,
|
||||
ulimits: {}
|
||||
};
|
||||
if (isARM(arch)) {
|
||||
configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
|
||||
}
|
||||
return configuration;
|
||||
} else if (type === 'mariadb') {
|
||||
const configuration = {
|
||||
privatePort: 3306,
|
||||
environmentVariables: {
|
||||
MARIADB_ROOT_USER: rootUser,
|
||||
MARIADB_ROOT_PASSWORD: rootUserPassword,
|
||||
MARIADB_USER: dbUser,
|
||||
MARIADB_PASSWORD: dbUserPassword,
|
||||
MARIADB_DATABASE: defaultDatabase
|
||||
},
|
||||
image: `${baseImage}:${version}`,
|
||||
volume: `${id}-${type}-data:/bitnami/mariadb`,
|
||||
ulimits: {}
|
||||
};
|
||||
if (isARM(arch)) {
|
||||
configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
|
||||
}
|
||||
return configuration;
|
||||
} else if (type === 'mongodb') {
|
||||
const configuration = {
|
||||
privatePort: 27017,
|
||||
environmentVariables: {
|
||||
MONGODB_ROOT_USER: rootUser,
|
||||
MONGODB_ROOT_PASSWORD: rootUserPassword
|
||||
},
|
||||
image: `${baseImage}:${version}`,
|
||||
volume: `${id}-${type}-data:/bitnami/mongodb`,
|
||||
ulimits: {}
|
||||
};
|
||||
if (isARM(arch)) {
|
||||
configuration.environmentVariables = {
|
||||
MONGO_INITDB_ROOT_USERNAME: rootUser,
|
||||
MONGO_INITDB_ROOT_PASSWORD: rootUserPassword
|
||||
};
|
||||
configuration.volume = `${id}-${type}-data:/data/db`;
|
||||
}
|
||||
return configuration;
|
||||
} else if (type === 'postgresql') {
|
||||
const configuration = {
|
||||
privatePort: 5432,
|
||||
environmentVariables: {
|
||||
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
|
||||
POSTGRESQL_PASSWORD: dbUserPassword,
|
||||
POSTGRESQL_USERNAME: dbUser,
|
||||
POSTGRESQL_DATABASE: defaultDatabase
|
||||
},
|
||||
image: `${baseImage}:${version}`,
|
||||
volume: `${id}-${type}-data:/bitnami/postgresql`,
|
||||
ulimits: {}
|
||||
};
|
||||
if (isARM(arch)) {
|
||||
configuration.volume = `${id}-${type}-data:/var/lib/postgresql`;
|
||||
configuration.environmentVariables = {
|
||||
POSTGRES_PASSWORD: dbUserPassword,
|
||||
POSTGRES_USER: dbUser,
|
||||
POSTGRES_DB: defaultDatabase
|
||||
};
|
||||
}
|
||||
return configuration;
|
||||
} else if (type === 'redis') {
|
||||
const {
|
||||
settings: { appendOnly }
|
||||
} = database;
|
||||
const configuration = {
|
||||
privatePort: 6379,
|
||||
command: undefined,
|
||||
environmentVariables: {
|
||||
REDIS_PASSWORD: dbUserPassword,
|
||||
REDIS_AOF_ENABLED: appendOnly ? 'yes' : 'no'
|
||||
},
|
||||
image: `${baseImage}:${version}`,
|
||||
volume: `${id}-${type}-data:/bitnami/redis/data`,
|
||||
ulimits: {}
|
||||
};
|
||||
if (isARM(arch)) {
|
||||
configuration.volume = `${id}-${type}-data:/data`;
|
||||
configuration.command = `/usr/local/bin/redis-server --appendonly ${
|
||||
appendOnly ? 'yes' : 'no'
|
||||
} --requirepass ${dbUserPassword}`;
|
||||
}
|
||||
return configuration;
|
||||
} else if (type === 'couchdb') {
|
||||
const configuration = {
|
||||
privatePort: 5984,
|
||||
environmentVariables: {
|
||||
COUCHDB_PASSWORD: dbUserPassword,
|
||||
COUCHDB_USER: dbUser
|
||||
},
|
||||
image: `${baseImage}:${version}`,
|
||||
volume: `${id}-${type}-data:/bitnami/couchdb`,
|
||||
ulimits: {}
|
||||
};
|
||||
if (isARM(arch)) {
|
||||
configuration.volume = `${id}-${type}-data:/opt/couchdb/data`;
|
||||
}
|
||||
return configuration;
|
||||
} else if (type === 'edgedb') {
|
||||
const configuration = {
|
||||
privatePort: 5656,
|
||||
environmentVariables: {
|
||||
EDGEDB_SERVER_PASSWORD: rootUserPassword,
|
||||
EDGEDB_SERVER_USER: rootUser,
|
||||
EDGEDB_SERVER_DATABASE: defaultDatabase,
|
||||
EDGEDB_SERVER_TLS_CERT_MODE: 'generate_self_signed'
|
||||
},
|
||||
image: `${baseImage}:${version}`,
|
||||
volume: `${id}-${type}-data:/var/lib/edgedb/data`,
|
||||
ulimits: {}
|
||||
};
|
||||
return configuration;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export function getDatabaseVersions(type: string, arch: string): string[] {
|
||||
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
|
||||
if (found) {
|
||||
if (isARM(arch)) {
|
||||
return found.versionsARM || found.versions;
|
||||
}
|
||||
return found.versions;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
export async function updatePasswordInDb(database, user, newPassword, isRoot) {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
rootUser,
|
||||
rootUserPassword,
|
||||
dbUser,
|
||||
dbUserPassword,
|
||||
defaultDatabase,
|
||||
destinationDockerId,
|
||||
destinationDocker: { id: dockerId }
|
||||
} = database;
|
||||
if (destinationDockerId) {
|
||||
if (type === 'mysql') {
|
||||
await executeCommand({
|
||||
dockerId,
|
||||
command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"`
|
||||
});
|
||||
} else if (type === 'mariadb') {
|
||||
await executeCommand({
|
||||
dockerId,
|
||||
command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"`
|
||||
});
|
||||
} else if (type === 'postgresql') {
|
||||
if (isRoot) {
|
||||
await executeCommand({
|
||||
dockerId,
|
||||
command: `docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"`
|
||||
});
|
||||
} else {
|
||||
await executeCommand({
|
||||
dockerId,
|
||||
command: `docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"`
|
||||
});
|
||||
}
|
||||
} else if (type === 'mongodb') {
|
||||
await executeCommand({
|
||||
dockerId,
|
||||
command: `docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"`
|
||||
});
|
||||
} else if (type === 'redis') {
|
||||
await executeCommand({
|
||||
dockerId,
|
||||
command: `docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function makeLabelForStandaloneDatabase({ id, image, volume }) {
|
||||
const database = await prisma.database.findFirst({ where: { id } });
|
||||
delete database.destinationDockerId;
|
||||
delete database.createdAt;
|
||||
delete database.updatedAt;
|
||||
return [
|
||||
'coolify.managed=true',
|
||||
`coolify.version=${version}`,
|
||||
`coolify.type=standalone-database`,
|
||||
`coolify.name=${database.name}`,
|
||||
`coolify.configuration=${base64Encode(
|
||||
JSON.stringify({
|
||||
version,
|
||||
image,
|
||||
volume,
|
||||
...database
|
||||
})
|
||||
)}`
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { z } from 'zod';
|
||||
import { privateProcedure, router } from '../../trpc';
|
||||
import {
|
||||
listSettings,
|
||||
startTraefikProxy,
|
||||
startTraefikTCPProxy,
|
||||
stopTraefikProxy
|
||||
} from '../../../lib/common';
|
||||
import { prisma } from '../../../prisma';
|
||||
|
||||
import { executeCommand } from '../../../lib/executeCommand';
|
||||
import { checkContainer } from '../../../lib/docker';
|
||||
|
||||
export const destinationsRouter = router({
|
||||
restartProxy: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
await stopTraefikProxy(id);
|
||||
await startTraefikProxy(id);
|
||||
await prisma.destinationDocker.update({
|
||||
where: { id },
|
||||
data: { isCoolifyProxyUsed: true }
|
||||
});
|
||||
}),
|
||||
startProxy: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
await startTraefikProxy(id);
|
||||
}),
|
||||
stopProxy: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
await stopTraefikProxy(id);
|
||||
}),
|
||||
saveSettings: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
engine: z.string(),
|
||||
isCoolifyProxyUsed: z.boolean()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, engine, isCoolifyProxyUsed } = input;
|
||||
await prisma.destinationDocker.updateMany({
|
||||
where: { engine },
|
||||
data: { isCoolifyProxyUsed }
|
||||
});
|
||||
}),
|
||||
status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const destination = await prisma.destinationDocker.findUnique({ where: { id } });
|
||||
const { found: isRunning } = await checkContainer({
|
||||
dockerId: destination.id,
|
||||
container: 'coolify-proxy',
|
||||
remove: true
|
||||
});
|
||||
return {
|
||||
isRunning
|
||||
};
|
||||
}),
|
||||
save: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
htmlUrl: z.string(),
|
||||
apiUrl: z.string(),
|
||||
customPort: z.number(),
|
||||
customUser: z.string(),
|
||||
isSystemWide: z.boolean().default(false)
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx.user;
|
||||
let {
|
||||
id,
|
||||
name,
|
||||
network,
|
||||
engine,
|
||||
isCoolifyProxyUsed,
|
||||
remoteIpAddress,
|
||||
remoteUser,
|
||||
remotePort
|
||||
} = input;
|
||||
if (id === 'new') {
|
||||
if (engine) {
|
||||
const { stdout } = await await executeCommand({
|
||||
command: `docker network ls --filter 'name=^${network}$' --format '{{json .}}'`
|
||||
});
|
||||
if (stdout === '') {
|
||||
await await executeCommand({
|
||||
command: `docker network create --attachable ${network}`
|
||||
});
|
||||
}
|
||||
await prisma.destinationDocker.create({
|
||||
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
|
||||
});
|
||||
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
|
||||
const destination = destinations.find((destination) => destination.network === network);
|
||||
if (destinations.length > 0) {
|
||||
const proxyConfigured = destinations.find(
|
||||
(destination) =>
|
||||
destination.network !== network && destination.isCoolifyProxyUsed === true
|
||||
);
|
||||
if (proxyConfigured) {
|
||||
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
|
||||
}
|
||||
await prisma.destinationDocker.updateMany({
|
||||
where: { engine },
|
||||
data: { isCoolifyProxyUsed }
|
||||
});
|
||||
}
|
||||
if (isCoolifyProxyUsed) {
|
||||
await startTraefikProxy(destination.id);
|
||||
}
|
||||
return { id: destination.id };
|
||||
} else {
|
||||
const destination = await prisma.destinationDocker.create({
|
||||
data: {
|
||||
name,
|
||||
teams: { connect: { id: teamId } },
|
||||
engine,
|
||||
network,
|
||||
isCoolifyProxyUsed,
|
||||
remoteEngine: true,
|
||||
remoteIpAddress,
|
||||
remoteUser,
|
||||
remotePort: Number(remotePort)
|
||||
}
|
||||
});
|
||||
return { id: destination.id };
|
||||
}
|
||||
} else {
|
||||
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
|
||||
return {};
|
||||
}
|
||||
}),
|
||||
check: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
network: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { network } = input;
|
||||
const found = await prisma.destinationDocker.findFirst({ where: { network } });
|
||||
if (found) {
|
||||
throw {
|
||||
message: `Network already exists: ${network}`
|
||||
};
|
||||
}
|
||||
}),
|
||||
delete: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const { network, remoteVerified, engine, isCoolifyProxyUsed } =
|
||||
await prisma.destinationDocker.findUnique({ where: { id } });
|
||||
if (isCoolifyProxyUsed) {
|
||||
if (engine || remoteVerified) {
|
||||
const { stdout: found } = await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'`
|
||||
});
|
||||
if (found) {
|
||||
await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker network disconnect ${network} coolify-proxy`
|
||||
});
|
||||
await executeCommand({ dockerId: id, command: `docker network rm ${network}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
await prisma.destinationDocker.delete({ where: { id } });
|
||||
}),
|
||||
getDestinationById: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const { teamId } = ctx.user;
|
||||
const destination = await prisma.destinationDocker.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { sshKey: true, application: true, service: true, database: true }
|
||||
});
|
||||
if (!destination && id !== 'new') {
|
||||
throw { status: 404, message: `Destination not found.` };
|
||||
}
|
||||
const settings = await listSettings();
|
||||
return {
|
||||
destination,
|
||||
settings
|
||||
};
|
||||
})
|
||||
});
|
||||
8
apps/trpc-experimental/server/src/trpc/routers/index.ts
Normal file
8
apps/trpc-experimental/server/src/trpc/routers/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './auth';
|
||||
export * from './dashboard';
|
||||
export * from './settings';
|
||||
export * from './applications';
|
||||
export * from './services';
|
||||
export * from './databases';
|
||||
export * from './sources';
|
||||
export * from './destinations';
|
||||
912
apps/trpc-experimental/server/src/trpc/routers/services/index.ts
Normal file
912
apps/trpc-experimental/server/src/trpc/routers/services/index.ts
Normal file
@@ -0,0 +1,912 @@
|
||||
import { z } from 'zod';
|
||||
import yaml from 'js-yaml';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { privateProcedure, router } from '../../trpc';
|
||||
import {
|
||||
createDirectories,
|
||||
decrypt,
|
||||
encrypt,
|
||||
fixType,
|
||||
getTags,
|
||||
getTemplates,
|
||||
isARM,
|
||||
isDev,
|
||||
listSettings,
|
||||
makeLabelForServices,
|
||||
removeService
|
||||
} from '../../../lib/common';
|
||||
import { prisma } from '../../../prisma';
|
||||
import { executeCommand } from '../../../lib/executeCommand';
|
||||
import {
|
||||
generatePassword,
|
||||
getFreePublicPort,
|
||||
parseAndFindServiceTemplates,
|
||||
persistentVolumes,
|
||||
startServiceContainers,
|
||||
verifyAndDecryptServiceSecrets
|
||||
} from './lib';
|
||||
import { checkContainer, defaultComposeConfiguration, stopTcpHttpProxy } from '../../../lib/docker';
|
||||
import cuid from 'cuid';
|
||||
import { day } from '../../../lib/dayjs';
|
||||
|
||||
export const servicesRouter = router({
|
||||
getLogs: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
containerId: z.string(),
|
||||
since: z.number().optional().default(0)
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
let { id, containerId, since } = input;
|
||||
if (since !== 0) {
|
||||
since = day(since).unix();
|
||||
}
|
||||
const {
|
||||
destinationDockerId,
|
||||
destinationDocker: { id: dockerId }
|
||||
} = await prisma.service.findUnique({
|
||||
where: { id },
|
||||
include: { destinationDocker: true }
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
try {
|
||||
const { default: ansi } = await import('strip-ansi');
|
||||
const { stdout, stderr } = await executeCommand({
|
||||
dockerId,
|
||||
command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}`
|
||||
});
|
||||
const stripLogsStdout = stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => ansi(l))
|
||||
.filter((a) => a);
|
||||
const stripLogsStderr = stderr
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => ansi(l))
|
||||
.filter((a) => a);
|
||||
return {
|
||||
data: {
|
||||
logs: stripLogsStderr.concat(stripLogsStdout)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const { statusCode, stderr } = error;
|
||||
if (stderr.startsWith('Error: No such container')) {
|
||||
return {
|
||||
data: {
|
||||
logs: [],
|
||||
noContainer: true
|
||||
}
|
||||
};
|
||||
}
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
data: {
|
||||
logs: []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: 'No logs found.'
|
||||
};
|
||||
}),
|
||||
deleteStorage: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
storageId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { storageId } = input;
|
||||
await prisma.servicePersistentStorage.deleteMany({ where: { id: storageId } });
|
||||
}),
|
||||
saveStorage: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
path: z.string(),
|
||||
isNewStorage: z.boolean(),
|
||||
storageId: z.string().optional().nullable(),
|
||||
containerId: z.string().optional()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, path, isNewStorage, storageId, containerId } = input;
|
||||
|
||||
if (isNewStorage) {
|
||||
const volumeName = `${id}-custom${path.replace(/\//gi, '-')}`;
|
||||
const found = await prisma.servicePersistentStorage.findFirst({
|
||||
where: { path, containerId }
|
||||
});
|
||||
if (found) {
|
||||
throw {
|
||||
status: 500,
|
||||
message: 'Persistent storage already exists for this container and path.'
|
||||
};
|
||||
}
|
||||
await prisma.servicePersistentStorage.create({
|
||||
data: { path, volumeName, containerId, service: { connect: { id } } }
|
||||
});
|
||||
} else {
|
||||
await prisma.servicePersistentStorage.update({
|
||||
where: { id: storageId },
|
||||
data: { path, containerId }
|
||||
});
|
||||
}
|
||||
}),
|
||||
getStorages: privateProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const persistentStorages = await prisma.servicePersistentStorage.findMany({
|
||||
where: { serviceId: id }
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
persistentStorages
|
||||
}
|
||||
};
|
||||
}),
|
||||
deleteSecret: privateProcedure
|
||||
.input(z.object({ id: z.string(), name: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, name } = input;
|
||||
await prisma.serviceSecret.deleteMany({ where: { serviceId: id, name } });
|
||||
}),
|
||||
saveService: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
fqdn: z.string().optional(),
|
||||
exposePort: z.string().optional(),
|
||||
type: z.string(),
|
||||
serviceSetting: z.any(),
|
||||
version: z.string().optional()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const teamId = ctx.user?.teamId;
|
||||
let { id, name, fqdn, exposePort, type, serviceSetting, version } = input;
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
if (exposePort) exposePort = Number(exposePort);
|
||||
type = fixType(type);
|
||||
|
||||
const data = {
|
||||
fqdn,
|
||||
name,
|
||||
exposePort,
|
||||
version
|
||||
};
|
||||
const templates = await getTemplates();
|
||||
const service = await prisma.service.findUnique({ where: { id } });
|
||||
const foundTemplate = templates.find((t) => fixType(t.type) === fixType(service.type));
|
||||
for (const setting of serviceSetting) {
|
||||
let { id: settingId, name, value, changed = false, isNew = false, variableName } = setting;
|
||||
if (value) {
|
||||
if (changed) {
|
||||
await prisma.serviceSetting.update({ where: { id: settingId }, data: { value } });
|
||||
}
|
||||
if (isNew) {
|
||||
if (!variableName) {
|
||||
variableName = foundTemplate?.variables.find((v) => v.name === name).id;
|
||||
}
|
||||
await prisma.serviceSetting.create({
|
||||
data: { name, value, variableName, service: { connect: { id } } }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
await prisma.service.update({
|
||||
where: { id },
|
||||
data
|
||||
});
|
||||
}),
|
||||
createSecret: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
isBuildSecret: z.boolean().optional(),
|
||||
isPRMRSecret: z.boolean().optional(),
|
||||
isNew: z.boolean().optional()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
let { id, name, value, isNew } = input;
|
||||
if (isNew) {
|
||||
const found = await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } });
|
||||
if (found) {
|
||||
throw `Secret ${name} already exists.`;
|
||||
} else {
|
||||
value = encrypt(value.trim());
|
||||
await prisma.serviceSecret.create({
|
||||
data: { name, value, service: { connect: { id } } }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
value = encrypt(value.trim());
|
||||
const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } });
|
||||
|
||||
if (found) {
|
||||
await prisma.serviceSecret.updateMany({
|
||||
where: { serviceId: id, name },
|
||||
data: { value }
|
||||
});
|
||||
} else {
|
||||
await prisma.serviceSecret.create({
|
||||
data: { name, value, service: { connect: { id } } }
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
getSecrets: privateProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const teamId = ctx.user?.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
let secrets = await prisma.serviceSecret.findMany({
|
||||
where: { serviceId: id },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
const templates = await getTemplates();
|
||||
if (!templates) throw new Error('No templates found. Please contact support.');
|
||||
const foundTemplate = templates.find((t) => fixType(t.type) === service.type);
|
||||
secrets = secrets.map((secret) => {
|
||||
const foundVariable = foundTemplate?.variables?.find((v) => v.name === secret.name) || null;
|
||||
if (foundVariable) {
|
||||
secret.readOnly = foundVariable.readOnly;
|
||||
}
|
||||
secret.value = decrypt(secret.value);
|
||||
return secret;
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
secrets
|
||||
}
|
||||
};
|
||||
}),
|
||||
wordpress: privateProcedure
|
||||
.input(z.object({ id: z.string(), ftpEnabled: z.boolean() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const teamId = ctx.user?.teamId;
|
||||
const {
|
||||
service: {
|
||||
destinationDocker: { engine, remoteEngine, remoteIpAddress }
|
||||
}
|
||||
} = await prisma.wordpress.findUnique({
|
||||
where: { serviceId: id },
|
||||
include: { service: { include: { destinationDocker: true } } }
|
||||
});
|
||||
|
||||
const publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
|
||||
|
||||
let ftpUser = cuid();
|
||||
let ftpPassword = generatePassword({});
|
||||
|
||||
const hostkeyDir = isDev ? '/tmp/hostkeys' : '/app/ssl/hostkeys';
|
||||
try {
|
||||
const data = await prisma.wordpress.update({
|
||||
where: { serviceId: id },
|
||||
data: { ftpEnabled },
|
||||
include: { service: { include: { destinationDocker: true } } }
|
||||
});
|
||||
const {
|
||||
service: { destinationDockerId, destinationDocker },
|
||||
ftpPublicPort,
|
||||
ftpUser: user,
|
||||
ftpPassword: savedPassword,
|
||||
ftpHostKey,
|
||||
ftpHostKeyPrivate
|
||||
} = data;
|
||||
const { network, engine } = destinationDocker;
|
||||
if (ftpEnabled) {
|
||||
if (user) ftpUser = user;
|
||||
if (savedPassword) ftpPassword = decrypt(savedPassword);
|
||||
|
||||
// TODO: rewrite these to usable without shell
|
||||
const { stdout: password } = await executeCommand({
|
||||
command: `echo ${ftpPassword} | openssl passwd -1 -stdin`,
|
||||
shell: true
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
try {
|
||||
await fs.stat(hostkeyDir);
|
||||
} catch (error) {
|
||||
await executeCommand({ command: `mkdir -p ${hostkeyDir}` });
|
||||
}
|
||||
if (!ftpHostKey) {
|
||||
await executeCommand({
|
||||
command: `ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519`
|
||||
});
|
||||
const { stdout: ftpHostKey } = await executeCommand({
|
||||
command: `cat ${hostkeyDir}/${id}.ed25519`
|
||||
});
|
||||
await prisma.wordpress.update({
|
||||
where: { serviceId: id },
|
||||
data: { ftpHostKey: encrypt(ftpHostKey) }
|
||||
});
|
||||
} else {
|
||||
await executeCommand({
|
||||
command: `echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`,
|
||||
shell: true
|
||||
});
|
||||
}
|
||||
if (!ftpHostKeyPrivate) {
|
||||
await executeCommand({
|
||||
command: `ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`
|
||||
});
|
||||
const { stdout: ftpHostKeyPrivate } = await executeCommand({
|
||||
command: `cat ${hostkeyDir}/${id}.rsa`
|
||||
});
|
||||
await prisma.wordpress.update({
|
||||
where: { serviceId: id },
|
||||
data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) }
|
||||
});
|
||||
} else {
|
||||
await executeCommand({
|
||||
command: `echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`,
|
||||
shell: true
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.wordpress.update({
|
||||
where: { serviceId: id },
|
||||
data: {
|
||||
ftpPublicPort: publicPort,
|
||||
ftpUser: user ? undefined : ftpUser,
|
||||
ftpPassword: savedPassword ? undefined : encrypt(ftpPassword)
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const { found: isRunning } = await checkContainer({
|
||||
dockerId: destinationDocker.id,
|
||||
container: `${id}-ftp`
|
||||
});
|
||||
if (isRunning) {
|
||||
await executeCommand({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`,
|
||||
shell: true
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
const volumes = [
|
||||
`${id}-wordpress-data:/home/${ftpUser}/wordpress`,
|
||||
`${
|
||||
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
|
||||
}/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`,
|
||||
`${
|
||||
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
|
||||
}/${id}.rsa:/etc/ssh/ssh_host_rsa_key`,
|
||||
`${
|
||||
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
|
||||
}/${id}.sh:/etc/sftp.d/chmod.sh`
|
||||
];
|
||||
|
||||
const compose = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[`${id}-ftp`]: {
|
||||
image: `atmoz/sftp:alpine`,
|
||||
command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:33'`,
|
||||
extra_hosts: ['host.docker.internal:host-gateway'],
|
||||
container_name: `${id}-ftp`,
|
||||
volumes,
|
||||
networks: [network],
|
||||
depends_on: [],
|
||||
restart: 'always'
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[`${id}-wordpress-data`]: {
|
||||
external: true,
|
||||
name: `${id}-wordpress-data`
|
||||
}
|
||||
}
|
||||
};
|
||||
await fs.writeFile(
|
||||
`${hostkeyDir}/${id}.sh`,
|
||||
`#!/bin/bash\nchmod 600 /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_rsa_key\nuserdel -f xfs\nchown -R 33:33 /home/${ftpUser}/wordpress/`
|
||||
);
|
||||
await executeCommand({ command: `chmod +x ${hostkeyDir}/${id}.sh` });
|
||||
await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose));
|
||||
await executeCommand({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
|
||||
});
|
||||
}
|
||||
return {
|
||||
publicPort,
|
||||
ftpUser,
|
||||
ftpPassword
|
||||
};
|
||||
} else {
|
||||
await prisma.wordpress.update({
|
||||
where: { serviceId: id },
|
||||
data: { ftpPublicPort: null }
|
||||
});
|
||||
try {
|
||||
await executeCommand({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`,
|
||||
shell: true
|
||||
});
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
await stopTcpHttpProxy(id, destinationDocker, ftpPublicPort);
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
throw message;
|
||||
} finally {
|
||||
try {
|
||||
await executeCommand({
|
||||
command: `rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
}),
|
||||
start: privateProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const teamId = ctx.user?.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const arm = isARM(service.arch);
|
||||
const { type, destinationDockerId, destinationDocker, persistentStorage, exposePort } = service;
|
||||
|
||||
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 s in template.services) {
|
||||
let newEnvironments = [];
|
||||
if (arm) {
|
||||
if (template.services[s]?.environmentArm?.length > 0) {
|
||||
for (const environment of template.services[s].environmentArm) {
|
||||
let [env, ...value] = environment.split('=');
|
||||
value = value.join('=');
|
||||
if (!value.startsWith('$$secret') && value !== '') {
|
||||
newEnvironments.push(`${env}=${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (template.services[s]?.environment?.length > 0) {
|
||||
for (const environment of template.services[s].environment) {
|
||||
let [env, ...value] = environment.split('=');
|
||||
value = value.join('=');
|
||||
if (!value.startsWith('$$secret') && value !== '') {
|
||||
newEnvironments.push(`${env}=${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const secrets = await verifyAndDecryptServiceSecrets(id);
|
||||
for (const secret of secrets) {
|
||||
const { name, value } = secret;
|
||||
if (value) {
|
||||
const foundEnv = !!template.services[s].environment?.find((env) =>
|
||||
env.startsWith(`${name}=`)
|
||||
);
|
||||
const foundNewEnv = !!newEnvironments?.find((env) => env.startsWith(`${name}=`));
|
||||
if (foundEnv && !foundNewEnv) {
|
||||
newEnvironments.push(`${name}=${value}`);
|
||||
}
|
||||
if (!foundEnv && !foundNewEnv && s === id) {
|
||||
newEnvironments.push(`${name}=${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const customVolumes = await prisma.servicePersistentStorage.findMany({
|
||||
where: { serviceId: id }
|
||||
});
|
||||
let volumes = new Set();
|
||||
if (arm) {
|
||||
template.services[s]?.volumesArm &&
|
||||
template.services[s].volumesArm.length > 0 &&
|
||||
template.services[s].volumesArm.forEach((v) => volumes.add(v));
|
||||
} else {
|
||||
template.services[s]?.volumes &&
|
||||
template.services[s].volumes.length > 0 &&
|
||||
template.services[s].volumes.forEach((v) => volumes.add(v));
|
||||
}
|
||||
|
||||
// Workaround: old plausible analytics service wrong volume id name
|
||||
if (service.type === 'plausibleanalytics' && service.plausibleAnalytics?.id) {
|
||||
let temp = Array.from(volumes);
|
||||
temp.forEach((a) => {
|
||||
const t = a.replace(service.id, service.plausibleAnalytics.id);
|
||||
volumes.delete(a);
|
||||
volumes.add(t);
|
||||
});
|
||||
}
|
||||
|
||||
if (customVolumes.length > 0) {
|
||||
for (const customVolume of customVolumes) {
|
||||
const { volumeName, path, containerId } = customVolume;
|
||||
if (
|
||||
volumes &&
|
||||
volumes.size > 0 &&
|
||||
!volumes.has(`${volumeName}:${path}`) &&
|
||||
containerId === service
|
||||
) {
|
||||
volumes.add(`${volumeName}:${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
let ports = [];
|
||||
if (template.services[s].proxy?.length > 0) {
|
||||
for (const proxy of template.services[s].proxy) {
|
||||
if (proxy.hostPort) {
|
||||
ports.push(`${proxy.hostPort}:${proxy.port}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (template.services[s].ports?.length === 1) {
|
||||
for (const port of template.services[s].ports) {
|
||||
if (exposePort) {
|
||||
ports.push(`${exposePort}:${port}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let image = template.services[s].image;
|
||||
if (arm && template.services[s].imageArm) {
|
||||
image = template.services[s].imageArm;
|
||||
}
|
||||
config[s] = {
|
||||
container_name: s,
|
||||
build: template.services[s].build || undefined,
|
||||
command: template.services[s].command,
|
||||
entrypoint: template.services[s]?.entrypoint,
|
||||
image,
|
||||
expose: template.services[s].ports,
|
||||
ports: ports.length > 0 ? ports : undefined,
|
||||
volumes: Array.from(volumes),
|
||||
environment: newEnvironments,
|
||||
depends_on: template.services[s]?.depends_on,
|
||||
ulimits: template.services[s]?.ulimits,
|
||||
cap_drop: template.services[s]?.cap_drop,
|
||||
cap_add: template.services[s]?.cap_add,
|
||||
labels: makeLabelForServices(type),
|
||||
...defaultComposeConfiguration(network)
|
||||
};
|
||||
// Generate files for builds
|
||||
if (template.services[s]?.files?.length > 0) {
|
||||
if (!config[s].build) {
|
||||
config[s].build = {
|
||||
context: workdir,
|
||||
dockerfile: `Dockerfile.${s}`
|
||||
};
|
||||
}
|
||||
let Dockerfile = `
|
||||
FROM ${template.services[s].image}`;
|
||||
for (const file of template.services[s].files) {
|
||||
const { location, content } = file;
|
||||
const source = path.join(workdir, location);
|
||||
await fs.mkdir(path.dirname(source), { recursive: true });
|
||||
await fs.writeFile(source, content);
|
||||
Dockerfile += `
|
||||
COPY .${location} ${location}`;
|
||||
}
|
||||
await fs.writeFile(`${workdir}/Dockerfile.${s}`, Dockerfile);
|
||||
}
|
||||
}
|
||||
const { volumeMounts } = persistentVolumes(id, persistentStorage, config);
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: config,
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: volumeMounts
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
// TODO: TODO!
|
||||
let fastify = null;
|
||||
await startServiceContainers(fastify, id, teamId, destinationDocker.id, composeFileDestination);
|
||||
|
||||
// Workaround: Stop old minio proxies
|
||||
if (service.type === 'minio') {
|
||||
try {
|
||||
const { stdout: containers } = await executeCommand({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker container ls -a --filter 'name=${id}-' --format {{.ID}}`
|
||||
});
|
||||
if (containers) {
|
||||
const containerArray = containers.split('\n');
|
||||
if (containerArray.length > 0) {
|
||||
for (const container of containerArray) {
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker stop -t 0 ${container}`
|
||||
});
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker rm --force ${container}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
try {
|
||||
const { stdout: containers } = await executeCommand({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker container ls -a --filter 'name=${id}-' --format {{.ID}}`
|
||||
});
|
||||
if (containers) {
|
||||
const containerArray = containers.split('\n');
|
||||
if (containerArray.length > 0) {
|
||||
for (const container of containerArray) {
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker stop -t 0 ${container}`
|
||||
});
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker rm --force ${container}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
}),
|
||||
stop: privateProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const teamId = ctx.user?.teamId;
|
||||
const { destinationDockerId } = await getServiceFromDB({ id, teamId });
|
||||
if (destinationDockerId) {
|
||||
const { stdout: containers } = await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}`
|
||||
});
|
||||
if (containers) {
|
||||
const containerArray = containers.split('\n');
|
||||
if (containerArray.length > 0) {
|
||||
for (const container of containerArray) {
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker stop -t 0 ${container}`
|
||||
});
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker rm --force ${container}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}),
|
||||
getServices: privateProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const teamId = ctx.user?.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
if (!service) {
|
||||
throw { status: 404, message: 'Service not found.' };
|
||||
}
|
||||
let template = {};
|
||||
let tags = [];
|
||||
if (service.type) {
|
||||
template = await parseAndFindServiceTemplates(service);
|
||||
tags = await getTags(service.type);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
settings: await listSettings(),
|
||||
service,
|
||||
template,
|
||||
tags
|
||||
}
|
||||
};
|
||||
}),
|
||||
status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
|
||||
const id = input.id;
|
||||
const teamId = ctx.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw { status: 400, message: 'Team not found.' };
|
||||
}
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId } = service;
|
||||
let payload = {};
|
||||
if (destinationDockerId) {
|
||||
const { stdout: containers } = await executeCommand({
|
||||
dockerId: service.destinationDocker.id,
|
||||
command: `docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'`
|
||||
});
|
||||
if (containers) {
|
||||
const containersArray = containers.trim().split('\n');
|
||||
if (containersArray.length > 0 && containersArray[0] !== '') {
|
||||
const templates = await getTemplates();
|
||||
let template = templates.find((t: { type: string }) => t.type === service.type);
|
||||
const templateStr = JSON.stringify(template);
|
||||
if (templateStr) {
|
||||
template = JSON.parse(templateStr.replaceAll('$$id', service.id));
|
||||
}
|
||||
for (const container of containersArray) {
|
||||
let isRunning = false;
|
||||
let isExited = false;
|
||||
let isRestarting = false;
|
||||
let isExcluded = false;
|
||||
const containerObj = JSON.parse(container);
|
||||
const exclude = template?.services[containerObj.Names]?.exclude;
|
||||
if (exclude) {
|
||||
payload[containerObj.Names] = {
|
||||
status: {
|
||||
isExcluded: true,
|
||||
isRunning: false,
|
||||
isExited: false,
|
||||
isRestarting: false
|
||||
}
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
const status = containerObj.State;
|
||||
if (status === 'running') {
|
||||
isRunning = true;
|
||||
}
|
||||
if (status === 'exited') {
|
||||
isExited = true;
|
||||
}
|
||||
if (status === 'restarting') {
|
||||
isRestarting = true;
|
||||
}
|
||||
payload[containerObj.Names] = {
|
||||
status: {
|
||||
isExcluded,
|
||||
isRunning,
|
||||
isExited,
|
||||
isRestarting
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}),
|
||||
cleanup: privateProcedure.query(async ({ ctx }) => {
|
||||
const teamId = ctx.user?.teamId;
|
||||
let services = await prisma.service.findMany({
|
||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, teams: true }
|
||||
});
|
||||
for (const service of services) {
|
||||
if (!service.fqdn) {
|
||||
if (service.destinationDockerId) {
|
||||
const { stdout: containers } = await executeCommand({
|
||||
dockerId: service.destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}`
|
||||
});
|
||||
if (containers) {
|
||||
const containerArray = containers.split('\n');
|
||||
if (containerArray.length > 0) {
|
||||
for (const container of containerArray) {
|
||||
await executeCommand({
|
||||
dockerId: service.destinationDockerId,
|
||||
command: `docker stop -t 0 ${container}`
|
||||
});
|
||||
await executeCommand({
|
||||
dockerId: service.destinationDockerId,
|
||||
command: `docker rm --force ${container}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await removeService({ id: service.id });
|
||||
}
|
||||
}
|
||||
}),
|
||||
delete: privateProcedure
|
||||
.input(z.object({ force: z.boolean(), id: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
// todo: check if user is allowed to delete service
|
||||
const { id } = input;
|
||||
const teamId = ctx.user?.teamId;
|
||||
const { destinationDockerId } = await getServiceFromDB({ id, teamId });
|
||||
if (destinationDockerId) {
|
||||
const { stdout: containers } = await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}`
|
||||
});
|
||||
if (containers) {
|
||||
const containerArray = containers.split('\n');
|
||||
if (containerArray.length > 0) {
|
||||
for (const container of containerArray) {
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker stop -t 0 ${container}`
|
||||
});
|
||||
await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker rm --force ${container}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.serviceSetting.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.fider.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 } });
|
||||
await prisma.wordpress.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.glitchTip.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.moodle.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.appwrite.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.searxng.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.weblate.deleteMany({ where: { serviceId: id } });
|
||||
await prisma.taiga.deleteMany({ where: { serviceId: id } });
|
||||
|
||||
await prisma.service.delete({ where: { id } });
|
||||
return {};
|
||||
})
|
||||
});
|
||||
|
||||
export async function getServiceFromDB({
|
||||
id,
|
||||
teamId
|
||||
}: {
|
||||
id: string;
|
||||
teamId: string;
|
||||
}): Promise<any> {
|
||||
const settings = await prisma.setting.findFirst();
|
||||
const body = await prisma.service.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: {
|
||||
destinationDocker: true,
|
||||
persistentStorage: true,
|
||||
serviceSecret: true,
|
||||
serviceSetting: true,
|
||||
wordpress: true,
|
||||
plausibleAnalytics: true
|
||||
}
|
||||
});
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
// body.type = fixType(body.type);
|
||||
|
||||
if (body?.serviceSecret.length > 0) {
|
||||
body.serviceSecret = body.serviceSecret.map((s) => {
|
||||
s.value = decrypt(s.value);
|
||||
return s;
|
||||
});
|
||||
}
|
||||
if (body.wordpress) {
|
||||
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
|
||||
}
|
||||
|
||||
return { ...body, settings };
|
||||
}
|
||||
376
apps/trpc-experimental/server/src/trpc/routers/services/lib.ts
Normal file
376
apps/trpc-experimental/server/src/trpc/routers/services/lib.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import { asyncSleep, decrypt, fixType, generateRangeArray, getDomain, getTemplates } from '../../../lib/common';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { prisma } from '../../../prisma';
|
||||
import crypto from 'crypto';
|
||||
import { executeCommand } from '../../../lib/executeCommand';
|
||||
|
||||
export async function parseAndFindServiceTemplates(
|
||||
service: any,
|
||||
workdir?: string,
|
||||
isDeploy: boolean = false
|
||||
) {
|
||||
const templates = await getTemplates();
|
||||
const foundTemplate = templates.find((t) => fixType(t.type) === service.type);
|
||||
let parsedTemplate = {};
|
||||
if (foundTemplate) {
|
||||
if (!isDeploy) {
|
||||
for (const [key, value] of Object.entries(foundTemplate.services)) {
|
||||
const realKey = key.replace('$$id', service.id);
|
||||
let name = value.name;
|
||||
if (!name) {
|
||||
if (Object.keys(foundTemplate.services).length === 1) {
|
||||
name = foundTemplate.name || service.name.toLowerCase();
|
||||
} else {
|
||||
if (key === '$$id') {
|
||||
name =
|
||||
foundTemplate.name || key.replaceAll('$$id-', '') || service.name.toLowerCase();
|
||||
} else {
|
||||
name = key.replaceAll('$$id-', '') || service.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
parsedTemplate[realKey] = {
|
||||
value,
|
||||
name,
|
||||
documentation:
|
||||
value.documentation || foundTemplate.documentation || 'https://docs.coollabs.io',
|
||||
image: value.image,
|
||||
files: value?.files,
|
||||
environment: [],
|
||||
fqdns: [],
|
||||
hostPorts: [],
|
||||
proxy: {}
|
||||
};
|
||||
if (value.environment?.length > 0) {
|
||||
for (const env of value.environment) {
|
||||
let [envKey, ...envValue] = env.split('=');
|
||||
envValue = envValue.join('=');
|
||||
let variable = null;
|
||||
if (foundTemplate?.variables) {
|
||||
variable =
|
||||
foundTemplate?.variables.find((v) => v.name === envKey) ||
|
||||
foundTemplate?.variables.find((v) => v.id === envValue);
|
||||
}
|
||||
if (variable) {
|
||||
const id = variable.id.replaceAll('$$', '');
|
||||
const label = variable?.label;
|
||||
const description = variable?.description;
|
||||
const defaultValue = variable?.defaultValue;
|
||||
const main = variable?.main || '$$id';
|
||||
const type = variable?.type || 'input';
|
||||
const placeholder = variable?.placeholder || '';
|
||||
const readOnly = variable?.readOnly || false;
|
||||
const required = variable?.required || false;
|
||||
if (envValue.startsWith('$$config') || variable?.showOnConfiguration) {
|
||||
if (envValue.startsWith('$$config_coolify')) {
|
||||
continue;
|
||||
}
|
||||
parsedTemplate[realKey].environment.push({
|
||||
id,
|
||||
name: envKey,
|
||||
value: envValue,
|
||||
main,
|
||||
label,
|
||||
description,
|
||||
defaultValue,
|
||||
type,
|
||||
placeholder,
|
||||
required,
|
||||
readOnly
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value?.proxy && value.proxy.length > 0) {
|
||||
for (const proxyValue of value.proxy) {
|
||||
if (proxyValue.domain) {
|
||||
const variable = foundTemplate?.variables.find((v) => v.id === proxyValue.domain);
|
||||
if (variable) {
|
||||
const { id, name, label, description, defaultValue, required = false } = variable;
|
||||
const found = await prisma.serviceSetting.findFirst({
|
||||
where: { serviceId: service.id, variableName: proxyValue.domain }
|
||||
});
|
||||
parsedTemplate[realKey].fqdns.push({
|
||||
id,
|
||||
name,
|
||||
value: found?.value || '',
|
||||
label,
|
||||
description,
|
||||
defaultValue,
|
||||
required
|
||||
});
|
||||
}
|
||||
}
|
||||
if (proxyValue.hostPort) {
|
||||
const variable = foundTemplate?.variables.find((v) => v.id === proxyValue.hostPort);
|
||||
if (variable) {
|
||||
const { id, name, label, description, defaultValue, required = false } = variable;
|
||||
const found = await prisma.serviceSetting.findFirst({
|
||||
where: { serviceId: service.id, variableName: proxyValue.hostPort }
|
||||
});
|
||||
parsedTemplate[realKey].hostPorts.push({
|
||||
id,
|
||||
name,
|
||||
value: found?.value || '',
|
||||
label,
|
||||
description,
|
||||
defaultValue,
|
||||
required
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parsedTemplate = foundTemplate;
|
||||
}
|
||||
let strParsedTemplate = JSON.stringify(parsedTemplate);
|
||||
|
||||
// replace $$id and $$workdir
|
||||
strParsedTemplate = strParsedTemplate.replaceAll('$$id', service.id);
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(
|
||||
'$$core_version',
|
||||
service.version || foundTemplate.defaultVersion
|
||||
);
|
||||
|
||||
// replace $$workdir
|
||||
if (workdir) {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll('$$workdir', workdir);
|
||||
}
|
||||
|
||||
// replace $$config
|
||||
if (service.serviceSetting.length > 0) {
|
||||
for (const setting of service.serviceSetting) {
|
||||
const { value, variableName } = setting;
|
||||
const regex = new RegExp(`\\$\\$config_${variableName.replace('$$config_', '')}\"`, 'gi');
|
||||
if (value === '$$generate_fqdn') {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + '"' || '' + '"');
|
||||
} else if (value === '$$generate_fqdn_slash') {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + '/' + '"');
|
||||
} else if (value === '$$generate_domain') {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, getDomain(service.fqdn) + '"');
|
||||
} else if (service.destinationDocker?.network && value === '$$generate_network') {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(
|
||||
regex,
|
||||
service.destinationDocker.network + '"'
|
||||
);
|
||||
} else {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, value + '"');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replace $$secret
|
||||
if (service.serviceSecret.length > 0) {
|
||||
for (const secret of service.serviceSecret) {
|
||||
let { name, value } = secret;
|
||||
name = name.toLowerCase();
|
||||
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}`, 'gi');
|
||||
const regex = new RegExp(`\\$\\$secret_${name}`, 'gi');
|
||||
if (value) {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(
|
||||
regexHashed,
|
||||
bcrypt.hashSync(value.replaceAll('"', '\\"'), 10)
|
||||
);
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll('"', '\\"'));
|
||||
} else {
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, '');
|
||||
strParsedTemplate = strParsedTemplate.replaceAll(regex, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
parsedTemplate = JSON.parse(strParsedTemplate);
|
||||
}
|
||||
return parsedTemplate;
|
||||
}
|
||||
export function generatePassword({
|
||||
length = 24,
|
||||
symbols = false,
|
||||
isHex = false
|
||||
}: { length?: number; symbols?: boolean; isHex?: boolean } | null): string {
|
||||
if (isHex) {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
const password = generator.generate({
|
||||
length,
|
||||
numbers: true,
|
||||
strict: true,
|
||||
symbols
|
||||
});
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
export async function getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress }) {
|
||||
const { default: isReachable } = await import('is-port-reachable');
|
||||
const data = await prisma.setting.findFirst();
|
||||
const { minPort, maxPort } = data;
|
||||
if (remoteEngine) {
|
||||
const dbUsed = await (
|
||||
await prisma.database.findMany({
|
||||
where: {
|
||||
publicPort: { not: null },
|
||||
id: { not: id },
|
||||
destinationDocker: { remoteIpAddress }
|
||||
},
|
||||
select: { publicPort: true }
|
||||
})
|
||||
).map((a) => a.publicPort);
|
||||
const wpFtpUsed = await (
|
||||
await prisma.wordpress.findMany({
|
||||
where: {
|
||||
ftpPublicPort: { not: null },
|
||||
id: { not: id },
|
||||
service: { destinationDocker: { remoteIpAddress } }
|
||||
},
|
||||
select: { ftpPublicPort: true }
|
||||
})
|
||||
).map((a) => a.ftpPublicPort);
|
||||
const wpUsed = await (
|
||||
await prisma.wordpress.findMany({
|
||||
where: {
|
||||
mysqlPublicPort: { not: null },
|
||||
id: { not: id },
|
||||
service: { destinationDocker: { remoteIpAddress } }
|
||||
},
|
||||
select: { mysqlPublicPort: true }
|
||||
})
|
||||
).map((a) => a.mysqlPublicPort);
|
||||
const minioUsed = await (
|
||||
await prisma.minio.findMany({
|
||||
where: {
|
||||
publicPort: { not: null },
|
||||
id: { not: id },
|
||||
service: { destinationDocker: { remoteIpAddress } }
|
||||
},
|
||||
select: { publicPort: true }
|
||||
})
|
||||
).map((a) => a.publicPort);
|
||||
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
|
||||
const range = generateRangeArray(minPort, maxPort);
|
||||
const availablePorts = range.filter((port) => !usedPorts.includes(port));
|
||||
for (const port of availablePorts) {
|
||||
const found = await isReachable(port, { host: remoteIpAddress });
|
||||
if (!found) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
const dbUsed = await (
|
||||
await prisma.database.findMany({
|
||||
where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { engine } },
|
||||
select: { publicPort: true }
|
||||
})
|
||||
).map((a) => a.publicPort);
|
||||
const wpFtpUsed = await (
|
||||
await prisma.wordpress.findMany({
|
||||
where: {
|
||||
ftpPublicPort: { not: null },
|
||||
id: { not: id },
|
||||
service: { destinationDocker: { engine } }
|
||||
},
|
||||
select: { ftpPublicPort: true }
|
||||
})
|
||||
).map((a) => a.ftpPublicPort);
|
||||
const wpUsed = await (
|
||||
await prisma.wordpress.findMany({
|
||||
where: {
|
||||
mysqlPublicPort: { not: null },
|
||||
id: { not: id },
|
||||
service: { destinationDocker: { engine } }
|
||||
},
|
||||
select: { mysqlPublicPort: true }
|
||||
})
|
||||
).map((a) => a.mysqlPublicPort);
|
||||
const minioUsed = await (
|
||||
await prisma.minio.findMany({
|
||||
where: {
|
||||
publicPort: { not: null },
|
||||
id: { not: id },
|
||||
service: { destinationDocker: { engine } }
|
||||
},
|
||||
select: { publicPort: true }
|
||||
})
|
||||
).map((a) => a.publicPort);
|
||||
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
|
||||
const range = generateRangeArray(minPort, maxPort);
|
||||
const availablePorts = range.filter((port) => !usedPorts.includes(port));
|
||||
for (const port of availablePorts) {
|
||||
const found = await isReachable(port, { host: 'localhost' });
|
||||
if (!found) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyAndDecryptServiceSecrets(id: string) {
|
||||
const secrets = await prisma.serviceSecret.findMany({ where: { serviceId: id } })
|
||||
let decryptedSecrets = secrets.map(secret => {
|
||||
const { name, value } = secret
|
||||
if (value) {
|
||||
let rawValue = decrypt(value)
|
||||
rawValue = rawValue.replaceAll(/\$/gi, '$$$')
|
||||
return { name, value: rawValue }
|
||||
}
|
||||
return { name, value }
|
||||
|
||||
})
|
||||
return decryptedSecrets
|
||||
}
|
||||
|
||||
export function persistentVolumes(id, persistentStorage, config) {
|
||||
let volumeSet = new Set();
|
||||
if (Object.keys(config).length > 0) {
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (value.volumes) {
|
||||
for (const volume of value.volumes) {
|
||||
if (!volume.startsWith('/')) {
|
||||
volumeSet.add(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const volumesArray = Array.from(volumeSet);
|
||||
const persistentVolume =
|
||||
persistentStorage?.map((storage) => {
|
||||
return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
|
||||
}) || [];
|
||||
|
||||
let volumes = [...persistentVolume];
|
||||
if (volumesArray) volumes = [...volumesArray, ...volumes];
|
||||
const composeVolumes =
|
||||
(volumes.length > 0 &&
|
||||
volumes.map((volume) => {
|
||||
return {
|
||||
[`${volume.split(':')[0]}`]: {
|
||||
name: volume.split(':')[0]
|
||||
}
|
||||
};
|
||||
})) ||
|
||||
[];
|
||||
|
||||
const volumeMounts = Object.assign({}, ...composeVolumes) || {};
|
||||
return { volumeMounts };
|
||||
}
|
||||
|
||||
export async function startServiceContainers(fastify, id, teamId, dockerId, composeFileDestination) {
|
||||
try {
|
||||
// fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Pulling images...' })
|
||||
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} pull` })
|
||||
} catch (error) { }
|
||||
// fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Building images...' })
|
||||
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` })
|
||||
// fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Creating containers...' })
|
||||
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} create` })
|
||||
// fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Starting containers...' })
|
||||
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} start` })
|
||||
await asyncSleep(1000);
|
||||
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` })
|
||||
// fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 0 })
|
||||
}
|
||||
80
apps/trpc-experimental/server/src/trpc/routers/settings.ts
Normal file
80
apps/trpc-experimental/server/src/trpc/routers/settings.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// import { z } from 'zod';
|
||||
import { publicProcedure, privateProcedure, router } from '../trpc';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { getCurrentUser, getTeamInvitation, listSettings, version } from '../../lib/common';
|
||||
import { env } from '../../env';
|
||||
import type { Permission, TeamInvitation } from '@prisma/client';
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
|
||||
export const settingsRouter = router({
|
||||
getBaseSettings: publicProcedure.query(async () => {
|
||||
const settings = await listSettings();
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
isRegistrationEnabled: settings?.isRegistrationEnabled
|
||||
}
|
||||
};
|
||||
}),
|
||||
getInstanceSettings: privateProcedure.query(async ({ ctx }) => {
|
||||
try {
|
||||
const settings = await listSettings();
|
||||
|
||||
let isAdmin = false;
|
||||
let permission = null;
|
||||
let token = null;
|
||||
let pendingInvitations: TeamInvitation[] = [];
|
||||
|
||||
if (!settings) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'An unexpected error occurred, please try again later.'
|
||||
});
|
||||
}
|
||||
if (ctx.user) {
|
||||
const currentUser = await getCurrentUser(ctx.user.userId);
|
||||
if (currentUser) {
|
||||
const foundPermission = currentUser.permission.find(
|
||||
(p: Permission) => p.teamId === ctx.user?.teamId
|
||||
)?.permission;
|
||||
if (foundPermission) {
|
||||
permission = foundPermission;
|
||||
isAdmin = foundPermission === 'owner' || foundPermission === 'admin';
|
||||
}
|
||||
const payload = {
|
||||
userId: ctx.user?.userId,
|
||||
teamId: ctx.user?.teamId,
|
||||
permission,
|
||||
isAdmin,
|
||||
iat: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
token = jsonwebtoken.sign(payload, env.COOLIFY_SECRET_KEY);
|
||||
}
|
||||
pendingInvitations = await getTeamInvitation(ctx.user.userId);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
token,
|
||||
userId: ctx.user?.userId,
|
||||
teamId: ctx.user?.teamId,
|
||||
permission,
|
||||
isAdmin,
|
||||
ipv4: ctx.user?.teamId ? settings.ipv4 : null,
|
||||
ipv6: ctx.user?.teamId ? settings.ipv6 : null,
|
||||
version,
|
||||
whiteLabeled: env.COOLIFY_WHITE_LABELED === 'true',
|
||||
whiteLabeledIcon: env.COOLIFY_WHITE_LABELED_ICON,
|
||||
isRegistrationEnabled: settings.isRegistrationEnabled,
|
||||
pendingInvitations
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'An unexpected error occurred, please try again later.',
|
||||
cause: error
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
223
apps/trpc-experimental/server/src/trpc/routers/sources/index.ts
Normal file
223
apps/trpc-experimental/server/src/trpc/routers/sources/index.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { z } from 'zod';
|
||||
import { privateProcedure, router } from '../../trpc';
|
||||
import { decrypt, encrypt } from '../../../lib/common';
|
||||
import { prisma } from '../../../prisma';
|
||||
|
||||
import cuid from 'cuid';
|
||||
|
||||
export const sourcesRouter = router({
|
||||
save: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
htmlUrl: z.string(),
|
||||
apiUrl: z.string(),
|
||||
customPort: z.number(),
|
||||
customUser: z.string(),
|
||||
isSystemWide: z.boolean().default(false)
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
let { id, name, htmlUrl, apiUrl, customPort, customUser, isSystemWide } = input;
|
||||
if (customPort) customPort = Number(customPort);
|
||||
await prisma.gitSource.update({
|
||||
where: { id },
|
||||
data: { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide }
|
||||
});
|
||||
}),
|
||||
newGitHubApp: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
htmlUrl: z.string(),
|
||||
apiUrl: z.string(),
|
||||
organization: z.string(),
|
||||
customPort: z.number(),
|
||||
isSystemWide: z.boolean().default(false)
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { teamId } = ctx.user;
|
||||
let { id, name, htmlUrl, apiUrl, organization, customPort, isSystemWide } = input;
|
||||
|
||||
if (customPort) customPort = Number(customPort);
|
||||
if (id === 'new') {
|
||||
const newId = cuid();
|
||||
await prisma.gitSource.create({
|
||||
data: {
|
||||
id: newId,
|
||||
name,
|
||||
htmlUrl,
|
||||
apiUrl,
|
||||
organization,
|
||||
customPort,
|
||||
isSystemWide,
|
||||
type: 'github',
|
||||
teams: { connect: { id: teamId } }
|
||||
}
|
||||
});
|
||||
return {
|
||||
id: newId
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
newGitLabApp: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
htmlUrl: z.string(),
|
||||
apiUrl: z.string(),
|
||||
oauthId: z.number(),
|
||||
appId: z.string(),
|
||||
appSecret: z.string(),
|
||||
groupName: z.string().optional().nullable(),
|
||||
customPort: z.number().optional().nullable(),
|
||||
customUser: z.string().optional().nullable()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx.user;
|
||||
let {
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
htmlUrl,
|
||||
apiUrl,
|
||||
oauthId,
|
||||
appId,
|
||||
appSecret,
|
||||
groupName,
|
||||
customPort,
|
||||
customUser
|
||||
} = input;
|
||||
|
||||
if (oauthId) oauthId = Number(oauthId);
|
||||
if (customPort) customPort = Number(customPort);
|
||||
const encryptedAppSecret = encrypt(appSecret);
|
||||
|
||||
if (id === 'new') {
|
||||
const newId = cuid();
|
||||
await prisma.gitSource.create({
|
||||
data: {
|
||||
id: newId,
|
||||
type,
|
||||
apiUrl,
|
||||
htmlUrl,
|
||||
name,
|
||||
customPort,
|
||||
customUser,
|
||||
teams: { connect: { id: teamId } }
|
||||
}
|
||||
});
|
||||
await prisma.gitlabApp.create({
|
||||
data: {
|
||||
teams: { connect: { id: teamId } },
|
||||
appId,
|
||||
oauthId,
|
||||
groupName,
|
||||
appSecret: encryptedAppSecret,
|
||||
gitSource: { connect: { id: newId } }
|
||||
}
|
||||
});
|
||||
return {
|
||||
status: 201,
|
||||
id: newId
|
||||
};
|
||||
} else {
|
||||
await prisma.gitSource.update({
|
||||
where: { id },
|
||||
data: { type, apiUrl, htmlUrl, name, customPort, customUser }
|
||||
});
|
||||
await prisma.gitlabApp.update({
|
||||
where: { id },
|
||||
data: {
|
||||
appId,
|
||||
oauthId,
|
||||
groupName,
|
||||
appSecret: encryptedAppSecret
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
delete: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const source = await prisma.gitSource.delete({
|
||||
where: { id },
|
||||
include: { githubApp: true, gitlabApp: true }
|
||||
});
|
||||
if (source.githubAppId) {
|
||||
await prisma.githubApp.delete({ where: { id: source.githubAppId } });
|
||||
}
|
||||
if (source.gitlabAppId) {
|
||||
await prisma.gitlabApp.delete({ where: { id: source.gitlabAppId } });
|
||||
}
|
||||
}),
|
||||
getSourceById: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const { teamId } = ctx.user;
|
||||
const settings = await prisma.setting.findFirst({});
|
||||
|
||||
if (id === 'new') {
|
||||
return {
|
||||
source: {
|
||||
name: null,
|
||||
type: null,
|
||||
htmlUrl: null,
|
||||
apiUrl: null,
|
||||
organization: null,
|
||||
customPort: 22,
|
||||
customUser: 'git'
|
||||
},
|
||||
settings
|
||||
};
|
||||
}
|
||||
|
||||
const source = await prisma.gitSource.findFirst({
|
||||
where: {
|
||||
id,
|
||||
OR: [
|
||||
{ teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
{ isSystemWide: true }
|
||||
]
|
||||
},
|
||||
include: { githubApp: true, gitlabApp: true }
|
||||
});
|
||||
if (!source) {
|
||||
throw { status: 404, message: 'Source not found.' };
|
||||
}
|
||||
|
||||
if (source?.githubApp?.clientSecret)
|
||||
source.githubApp.clientSecret = decrypt(source.githubApp.clientSecret);
|
||||
if (source?.githubApp?.webhookSecret)
|
||||
source.githubApp.webhookSecret = decrypt(source.githubApp.webhookSecret);
|
||||
if (source?.githubApp?.privateKey)
|
||||
source.githubApp.privateKey = decrypt(source.githubApp.privateKey);
|
||||
if (source?.gitlabApp?.appSecret)
|
||||
source.gitlabApp.appSecret = decrypt(source.gitlabApp.appSecret);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
source,
|
||||
settings
|
||||
}
|
||||
};
|
||||
})
|
||||
});
|
||||
33
apps/trpc-experimental/server/src/trpc/trpc.ts
Normal file
33
apps/trpc-experimental/server/src/trpc/trpc.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import superjson from 'superjson';
|
||||
import type { Context } from './context';
|
||||
|
||||
const t = initTRPC.context<Context>().create({
|
||||
transformer: superjson,
|
||||
errorFormatter({ shape }) {
|
||||
return shape;
|
||||
}
|
||||
});
|
||||
const logger = t.middleware(async ({ path, type, next }) => {
|
||||
const start = Date.now();
|
||||
const result = await next();
|
||||
const durationMs = Date.now() - start;
|
||||
result.ok
|
||||
? console.log('OK request timing:', { path, type, durationMs })
|
||||
: console.log('Non-OK request timing', { path, type, durationMs });
|
||||
return result;
|
||||
});
|
||||
|
||||
const isAdmin = t.middleware(async ({ ctx, next }) => {
|
||||
if (!ctx.user) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
user: ctx.user
|
||||
}
|
||||
});
|
||||
});
|
||||
export const router = t.router;
|
||||
export const privateProcedure = t.procedure.use(isAdmin);
|
||||
export const publicProcedure = t.procedure;
|
||||
Reference in New Issue
Block a user